diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 48015838903..686fde556b6 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -114,9 +114,8 @@ Value CallExpression::execute(Interpreter& interpreter) const auto& function = static_cast(callee.as_object()); MarkedValueList arguments(interpreter.heap()); - for (auto bound_argument : function.bound_arguments()) { - arguments.append(bound_argument); - } + arguments.values().append(function.bound_arguments()); + for (size_t i = 0; i < m_arguments.size(); ++i) { auto value = m_arguments[i].execute(interpreter); if (interpreter.exception()) diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index abf94639f9f..35ebe608a5c 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -52,6 +52,7 @@ namespace JS { class ASTNode; +class BoundFunction; class Cell; class DeferGC; class Error; diff --git a/Libraries/LibJS/Makefile b/Libraries/LibJS/Makefile index 686110098cf..1434b82f84c 100644 --- a/Libraries/LibJS/Makefile +++ b/Libraries/LibJS/Makefile @@ -12,6 +12,7 @@ OBJS = \ Runtime/BooleanConstructor.o \ Runtime/BooleanObject.o \ Runtime/BooleanPrototype.o \ + Runtime/BoundFunction.o \ Runtime/Cell.o \ Runtime/ConsoleObject.o \ Runtime/Date.o \ diff --git a/Libraries/LibJS/Runtime/BoundFunction.cpp b/Libraries/LibJS/Runtime/BoundFunction.cpp new file mode 100644 index 00000000000..6ecc286cf67 --- /dev/null +++ b/Libraries/LibJS/Runtime/BoundFunction.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, Jack Karamanian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +namespace JS { + +BoundFunction::BoundFunction(Function& target_function, Value bound_this, Vector arguments, i32 length, Object* constructor_prototype) + : Function::Function(*interpreter().global_object().function_prototype(), bound_this, move(arguments)) + , m_target_function(&target_function) + , m_constructor_prototype(constructor_prototype) + , m_name(String::format("bound %s", target_function.name().characters())) +{ + put("length", Value(length)); +} + +BoundFunction::~BoundFunction() +{ +} + +Value BoundFunction::call(Interpreter& interpreter) +{ + return m_target_function->call(interpreter); +} + +Value BoundFunction::construct(Interpreter& interpreter) +{ + if (auto this_value = interpreter.this_value(); m_constructor_prototype && this_value.is_object()) { + this_value.as_object().set_prototype(m_constructor_prototype); + } + return m_target_function->construct(interpreter); +} + +LexicalEnvironment* BoundFunction::create_environment() +{ + return m_target_function->create_environment(); +} + +void BoundFunction::visit_children(Visitor& visitor) +{ + Function::visit_children(visitor); + visitor.visit(m_target_function); + visitor.visit(m_constructor_prototype); +} + +} diff --git a/Libraries/LibJS/Runtime/BoundFunction.h b/Libraries/LibJS/Runtime/BoundFunction.h new file mode 100644 index 00000000000..27387843df1 --- /dev/null +++ b/Libraries/LibJS/Runtime/BoundFunction.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, Jack Karamanian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace JS { + +class BoundFunction final : public Function { +public: + BoundFunction(Function& target_function, Value bound_this, Vector arguments, i32 length, Object* constructor_prototype); + + virtual ~BoundFunction(); + + virtual Value call(Interpreter& interpreter) override; + + virtual Value construct(Interpreter& interpreter) override; + + virtual LexicalEnvironment* create_environment() override; + + virtual void visit_children(Visitor&) override; + + virtual const FlyString& name() const override + { + return m_name; + } + + Function& target_function() const + { + return *m_target_function; + } + +private: + virtual bool is_bound_function() const override { return true; } + virtual const char* class_name() const override { return "BoundFunction"; } + + Function* m_target_function = nullptr; + Object* m_constructor_prototype = nullptr; + FlyString m_name; +}; + +} diff --git a/Libraries/LibJS/Runtime/Function.cpp b/Libraries/LibJS/Runtime/Function.cpp index d5f5e813b97..249a2b8d6f0 100644 --- a/Libraries/LibJS/Runtime/Function.cpp +++ b/Libraries/LibJS/Runtime/Function.cpp @@ -25,7 +25,9 @@ */ #include +#include #include +#include namespace JS { @@ -41,6 +43,50 @@ Function::Function(Object& prototype, Optional bound_this, Vector { } +BoundFunction* Function::bind(Value bound_this_value, Vector arguments) +{ + + Function& target_function = is_bound_function() ? static_cast(*this).target_function() : *this; + + auto bound_this_object + = [bound_this_value, this]() -> Value { + if (bound_this().has_value()) { + return bound_this().value(); + } + switch (bound_this_value.type()) { + case Value::Type::Undefined: + case Value::Type::Null: + // FIXME: Null or undefined should be passed through in strict mode. + return &interpreter().global_object(); + default: + return bound_this_value.to_object(interpreter().heap()); + } + }(); + + i32 computed_length = 0; + auto length_property = get("length"); + if (interpreter().exception()) { + return nullptr; + } + if (length_property.has_value() && length_property.value().is_number()) { + computed_length = max(0, length_property.value().to_i32() - static_cast(arguments.size())); + } + + Object* constructor_prototype = nullptr; + auto prototype_property = target_function.get("prototype"); + if (interpreter().exception()) { + return nullptr; + } + if (prototype_property.has_value() && prototype_property.value().is_object()) { + constructor_prototype = &prototype_property.value().as_object(); + } + + auto all_bound_arguments = bound_arguments(); + all_bound_arguments.append(move(arguments)); + + return interpreter().heap().allocate(target_function, bound_this_object, move(all_bound_arguments), computed_length, constructor_prototype); +} + void Function::visit_children(Visitor& visitor) { Object::visit_children(visitor); diff --git a/Libraries/LibJS/Runtime/Function.h b/Libraries/LibJS/Runtime/Function.h index f42ab49b677..eb1cd2b9f28 100644 --- a/Libraries/LibJS/Runtime/Function.h +++ b/Libraries/LibJS/Runtime/Function.h @@ -42,6 +42,8 @@ public: virtual void visit_children(Visitor&) override; + BoundFunction* bind(Value bound_this_value, Vector arguments); + Optional bound_this() const { return m_bound_this; diff --git a/Libraries/LibJS/Runtime/FunctionPrototype.cpp b/Libraries/LibJS/Runtime/FunctionPrototype.cpp index 7b44c4e2e13..3e9f0e7ad59 100644 --- a/Libraries/LibJS/Runtime/FunctionPrototype.cpp +++ b/Libraries/LibJS/Runtime/FunctionPrototype.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -84,8 +85,19 @@ Value FunctionPrototype::bind(Interpreter& interpreter) auto* this_object = interpreter.this_value().to_object(interpreter.heap()); if (!this_object) return {}; - // FIXME: Implement me :^) - ASSERT_NOT_REACHED(); + if (!this_object->is_function()) + return interpreter.throw_exception("Not a Function object"); + + auto& this_function = static_cast(*this_object); + auto bound_this_arg = interpreter.argument(0); + + Vector arguments; + if (interpreter.argument_count() > 1) { + arguments = interpreter.call_frame().arguments; + arguments.remove(0); + } + + return this_function.bind(bound_this_arg, move(arguments)); } Value FunctionPrototype::call(Interpreter& interpreter) @@ -117,7 +129,7 @@ Value FunctionPrototype::to_string(Interpreter& interpreter) String function_parameters = ""; String function_body; - if (this_object->is_native_function()) { + if (this_object->is_native_function() || this_object->is_bound_function()) { function_body = String::format(" [%s]", this_object->class_name()); } else { auto& parameters = static_cast(this_object)->parameters(); diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index ad3766a7d89..219e27e97fc 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -72,6 +72,7 @@ public: virtual bool is_error() const { return false; } virtual bool is_function() const { return false; } virtual bool is_native_function() const { return false; } + virtual bool is_bound_function() const { return false; } virtual bool is_native_property() const { return false; } virtual bool is_string_object() const { return false; } diff --git a/Libraries/LibJS/Tests/Function.prototype.bind.js b/Libraries/LibJS/Tests/Function.prototype.bind.js new file mode 100644 index 00000000000..77d5a76faf0 --- /dev/null +++ b/Libraries/LibJS/Tests/Function.prototype.bind.js @@ -0,0 +1,112 @@ +load("test-common.js"); + +try { + assert(Function.prototype.bind.length === 1); + + var charAt = String.prototype.charAt.bind("bar"); + assert(charAt(0) + charAt(1) + charAt(2) === "bar"); + + function getB() { + return this.toUpperCase().charAt(0); + } + assert(getB.bind("bar")() === "B"); + + function sum(a, b, c) { + return a + b + c; + } + + // Arguments should be able to be bound to a function. + var boundSum = sum.bind(null, 10, 5); + assert(isNaN(boundSum())); + + assert(boundSum(5) === 20); + assert(boundSum(5, 6, 7) === 20); + + // Arguments should be appended to a BoundFunction's bound arguments. + assert(boundSum.bind(null, 5)() === 20); + + // A BoundFunction's length property should be adjusted based on the number + // of bound arguments. + assert(sum.length === 3); + assert(boundSum.length === 1); + assert(boundSum.bind(null, 5).length === 0); + assert(boundSum.bind(null, 5, 6, 7, 8).length === 0); + + function identity() { + return this; + } + + // It should capture the global object if the |this| value is null or undefined. + assert(identity.bind()() === globalThis); + assert(identity.bind(null)() === globalThis); + assert(identity.bind(undefined)() === globalThis); + + function Foo() { + assert(identity.bind()() === globalThis); + assert(identity.bind(this)() === this); + } + new Foo(); + + // Primitive |this| values should be converted to objects. + assert(identity.bind("foo")() instanceof String); + assert(identity.bind(123)() instanceof Number); + assert(identity.bind(true)() instanceof Boolean); + + // It should retain |this| values passed to it. + var obj = { foo: "bar" }; + + assert(identity.bind(obj)() === obj); + + // The bound |this| can not be changed once set + assert(identity.bind("foo").bind(123)() instanceof String); + + // The bound |this| value should have no effect on a constructor. + function Bar() { + this.x = 3; + this.y = 4; + } + Bar.prototype.baz = "baz"; + + var BoundBar = Bar.bind({ u: 5, v: 6 }); + + var bar = new BoundBar(); + assert(bar.x === 3); + assert(bar.y === 4); + assert(typeof bar.u === "undefined"); + assert(typeof bar.v === "undefined"); + // Objects constructed from BoundFunctions should retain the prototype of the original function. + assert(bar.baz === "baz"); + // BoundFunctions should not have a prototype property. + assert(typeof BoundBar.prototype === "undefined"); + + // Function.prototype.bind should not accept non-function values. + assertThrowsError(() => { + Function.prototype.bind.call("foo"); + }, { + error: TypeError, + message: "Not a Function object" + }); + + // A constructor's arguments should be able to be bound. + var Make5 = Number.bind(null, 5); + assert(Make5() === 5); + assert(new Make5().valueOf() === 5); + + // FIXME: Uncomment me when strict mode is implemented + // function strictIdentity() { + // return this; + // } + + // assert(strictIdentity.bind()() === undefined); + // assert(strictIdentity.bind(null)() === null); + // assert(strictIdentity.bind(undefined)() === undefined); + // })(); + + // FIXME: Uncomment me when arrow functions have the correct |this| value. + // // Arrow functions can not have their |this| value set. + // assert((() => this).bind("foo")() === globalThis) + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +}