LibJS: Implement Function.prototype.bind()

This commit is contained in:
Jack Karamanian 2020-04-19 15:03:02 -05:00 committed by Andreas Kling
parent b3800829da
commit 1fa0c7304d
Notes: sideshowbarker 2024-07-19 07:25:18 +09:00
10 changed files with 317 additions and 6 deletions

View file

@ -114,9 +114,8 @@ Value CallExpression::execute(Interpreter& interpreter) const
auto& function = static_cast<Function&>(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())

View file

@ -52,6 +52,7 @@
namespace JS {
class ASTNode;
class BoundFunction;
class Cell;
class DeferGC;
class Error;

View file

@ -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 \

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2020, Jack Karamanian <karamanian.jack@gmail.com>
* 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 <LibJS/Interpreter.h>
#include <LibJS/Runtime/BoundFunction.h>
#include <LibJS/Runtime/GlobalObject.h>
namespace JS {
BoundFunction::BoundFunction(Function& target_function, Value bound_this, Vector<Value> 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);
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020, Jack Karamanian <karamanian.jack@gmail.com>
* 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 <LibJS/Runtime/Function.h>
namespace JS {
class BoundFunction final : public Function {
public:
BoundFunction(Function& target_function, Value bound_this, Vector<Value> 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;
};
}

View file

@ -25,7 +25,9 @@
*/
#include <LibJS/Interpreter.h>
#include <LibJS/Runtime/BoundFunction.h>
#include <LibJS/Runtime/Function.h>
#include <LibJS/Runtime/GlobalObject.h>
namespace JS {
@ -41,6 +43,50 @@ Function::Function(Object& prototype, Optional<Value> bound_this, Vector<Value>
{
}
BoundFunction* Function::bind(Value bound_this_value, Vector<Value> arguments)
{
Function& target_function = is_bound_function() ? static_cast<BoundFunction&>(*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<i32>(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<BoundFunction>(target_function, bound_this_object, move(all_bound_arguments), computed_length, constructor_prototype);
}
void Function::visit_children(Visitor& visitor)
{
Object::visit_children(visitor);

View file

@ -42,6 +42,8 @@ public:
virtual void visit_children(Visitor&) override;
BoundFunction* bind(Value bound_this_value, Vector<Value> arguments);
Optional<Value> bound_this() const
{
return m_bound_this;

View file

@ -28,6 +28,7 @@
#include <AK/StringBuilder.h>
#include <LibJS/AST.h>
#include <LibJS/Interpreter.h>
#include <LibJS/Runtime/BoundFunction.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Function.h>
#include <LibJS/Runtime/FunctionPrototype.h>
@ -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<TypeError>("Not a Function object");
auto& this_function = static_cast<Function&>(*this_object);
auto bound_this_arg = interpreter.argument(0);
Vector<Value> 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<ScriptFunction*>(this_object)->parameters();

View file

@ -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; }

View file

@ -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);
}