Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibJS: Add new host hook, specify template literals and more #14904

Merged
merged 6 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 113 additions & 21 deletions Userland/Libraries/LibJS/AST.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,15 @@ ThrowCompletionOr<CallExpression::ThisAndCallee> CallExpression::compute_this_an
return ThisAndCallee { this_value, callee };
}

Value this_value = js_undefined();
if (callee_reference.is_environment_reference()) {
if (Object* base_object = callee_reference.base_environment().with_base_object(); base_object != nullptr)
this_value = base_object;
}

// [[Call]] will handle that in non-strict mode the this value becomes the global object
return ThisAndCallee {
js_undefined(),
this_value,
callee_reference.is_unresolvable()
? TRY(m_callee->execute(interpreter, global_object)).release_value()
: TRY(callee_reference.get_value(global_object))
Expand Down Expand Up @@ -3534,32 +3540,118 @@ Completion TaggedTemplateLiteral::execute(Interpreter& interpreter, GlobalObject
{
InterpreterNodeScope node_scope { interpreter, *this };

auto& vm = interpreter.vm();
// NOTE: This is both
// MemberExpression : MemberExpression TemplateLiteral
// CallExpression : CallExpression TemplateLiteral
// As the only difference is the first step.

// 1. Let tagRef be ? Evaluation of MemberExpression.
// 1. Let tagRef be ? Evaluation of CallExpression.

// 2. Let tagFunc be ? GetValue(tagRef).
auto tag = TRY(m_tag->execute(interpreter, global_object)).release_value();

// 3. Let thisCall be this CallExpression.
// 3. Let thisCall be this MemberExpression.
// FIXME: 4. Let tailCall be IsInTailPosition(thisCall).

// NOTE: A tagged template is a function call where the arguments of the call are derived from a
// TemplateLiteral (13.2.8). The actual arguments include a template object (13.2.8.3)
// and the values produced by evaluating the expressions embedded within the TemplateLiteral.
auto template_ = TRY(get_template_object(interpreter, global_object));
MarkedVector<Value> arguments(interpreter.vm().heap());
arguments.append(template_);

auto& expressions = m_template_literal->expressions();
auto* strings = MUST(Array::create(global_object, 0));
MarkedVector<Value> arguments(vm.heap());
arguments.append(strings);
for (size_t i = 0; i < expressions.size(); ++i) {
auto value = TRY(expressions[i].execute(interpreter, global_object)).release_value();
// tag`${foo}` -> "", foo, "" -> tag(["", ""], foo)
// tag`foo${bar}baz${qux}` -> "foo", bar, "baz", qux, "" -> tag(["foo", "baz", ""], bar, qux)
if (i % 2 == 0) {
strings->indexed_properties().append(value);
} else {
arguments.append(value);
}
}

auto* raw_strings = MUST(Array::create(global_object, 0));
for (auto& raw_string : m_template_literal->raw_strings()) {
auto value = TRY(raw_string.execute(interpreter, global_object)).release_value();
raw_strings->indexed_properties().append(value);
}
strings->define_direct_property(vm.names.raw, raw_strings, 0);
// tag`${foo}` -> "", foo, "" -> tag(["", ""], foo)
// tag`foo${bar}baz${qux}` -> "foo", bar, "baz", qux, "" -> tag(["foo", "baz", ""], bar, qux)
// So we want all the odd expressions
for (size_t i = 1; i < expressions.size(); i += 2)
arguments.append(TRY(expressions[i].execute(interpreter, global_object)).release_value());

// 5. Return ? EvaluateCall(tagFunc, tagRef, TemplateLiteral, tailCall).
return call(global_object, tag, js_undefined(), move(arguments));
}

// 13.2.8.3 GetTemplateObject ( templateLiteral ), https://tc39.es/ecma262/#sec-gettemplateobject
ThrowCompletionOr<Value> TaggedTemplateLiteral::get_template_object(Interpreter& interpreter, GlobalObject& global_object) const
{
// 1. Let realm be the current Realm Record.
auto* realm = global_object.associated_realm();

// 2. Let templateRegistry be realm.[[TemplateMap]].
// 3. For each element e of templateRegistry, do
// a. If e.[[Site]] is the same Parse Node as templateLiteral, then
// i. Return e.[[Array]].
// NOTE: Instead of caching on the realm we cache on the Parse Node side as
// this makes it easier to track whether it is the same parse node.
if (auto cached_value_or_end = m_cached_values.find(realm); cached_value_or_end != m_cached_values.end())
return Value { cached_value_or_end->value.cell() };

// 4. Let rawStrings be TemplateStrings of templateLiteral with argument true.
auto& raw_strings = m_template_literal->raw_strings();

// 5. Let cookedStrings be TemplateStrings of templateLiteral with argument false.
auto& expressions = m_template_literal->expressions();

// 6. Let count be the number of elements in the List cookedStrings.
// NOTE: Only the even expression in expression are the cooked strings
// so we use rawStrings for the size here
VERIFY(raw_strings.size() == (expressions.size() + 1) / 2);
auto count = raw_strings.size();

// 7. Assert: count ≤ 2^32 - 1.
VERIFY(count <= 0xffffffff);

// 8. Let template be ! ArrayCreate(count).
// NOTE: We don't set count since we push the values using append which
// would then append after count. Same for 9.
auto* template_ = MUST(Array::create(global_object, 0));

// 9. Let rawObj be ! ArrayCreate(count).
auto* raw_obj = MUST(Array::create(global_object, 0));

// 10. Let index be 0.
// 11. Repeat, while index < count,
for (size_t i = 0; i < count; ++i) {
auto cooked_string_index = i * 2;
// a. Let prop be ! ToString(𝔽(index)).
// b. Let cookedValue be cookedStrings[index].
auto cooked_value = TRY(expressions[cooked_string_index].execute(interpreter, global_object)).release_value();

// NOTE: If the string contains invalid escapes we get a null expression here,
// which we then convert to the expected `undefined` TV. See
// 12.9.6.1 Static Semantics: TV, https://tc39.es/ecma262/#sec-static-semantics-tv
if (cooked_value.is_null())
cooked_value = js_undefined();

// c. Perform ! DefinePropertyOrThrow(template, prop, PropertyDescriptor { [[Value]]: cookedValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false }).
template_->indexed_properties().append(cooked_value);

// d. Let rawValue be the String value rawStrings[index].
// e. Perform ! DefinePropertyOrThrow(rawObj, prop, PropertyDescriptor { [[Value]]: rawValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false }).
raw_obj->indexed_properties().append(TRY(raw_strings[i].execute(interpreter, global_object)).release_value());

// f. Set index to index + 1.
}

// 12. Perform ! SetIntegrityLevel(rawObj, frozen).
MUST(raw_obj->set_integrity_level(Object::IntegrityLevel::Frozen));

// 13. Perform ! DefinePropertyOrThrow(template, "raw", PropertyDescriptor { [[Value]]: rawObj, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }).
template_->define_direct_property(interpreter.vm().names.raw, raw_obj, 0);

// 14. Perform ! SetIntegrityLevel(template, frozen).
MUST(template_->set_integrity_level(Object::IntegrityLevel::Frozen));

// 15. Append the Record { [[Site]]: templateLiteral, [[Array]]: template } to templateRegistry.
m_cached_values.set(realm, make_handle(template_));

// 16. Return template.
return template_;
}

void TryStatement::dump(int indent) const
{
ASTNode::dump(indent);
Expand Down
3 changes: 3 additions & 0 deletions Userland/Libraries/LibJS/AST.h
Original file line number Diff line number Diff line change
Expand Up @@ -1769,9 +1769,12 @@ class TaggedTemplateLiteral final : public Expression {
virtual void dump(int indent) const override;
virtual Bytecode::CodeGenerationErrorOr<void> generate_bytecode(Bytecode::Generator&) const override;

ThrowCompletionOr<Value> get_template_object(Interpreter& interpreter, GlobalObject& global_object) const;

private:
NonnullRefPtr<Expression> const m_tag;
NonnullRefPtr<TemplateLiteral> const m_template_literal;
mutable HashMap<Realm*, Handle<Array>> m_cached_values;
};

class MemberExpression final : public Expression {
Expand Down
30 changes: 23 additions & 7 deletions Userland/Libraries/LibJS/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1811,16 +1811,18 @@ NonnullRefPtr<ArrayExpression> Parser::parse_array_expression()
return create_ast_node<ArrayExpression>({ m_state.current_token.filename(), rule_start.position(), position() }, move(elements));
}

NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token const& token, bool in_template_literal)
NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token const& token, StringLiteralType string_literal_type, bool* contains_invalid_escape)
{
auto rule_start = push_start();
auto status = Token::StringValueStatus::Ok;
auto string = token.string_value(status);
// NOTE: Tagged templates should not fail on invalid strings as their raw contents can still be accessed.
if (status != Token::StringValueStatus::Ok) {
String message;
if (status == Token::StringValueStatus::LegacyOctalEscapeSequence) {
m_state.string_legacy_octal_escape_sequence_in_scope = true;
if (in_template_literal)
// It is a Syntax Error if the [Tagged] parameter was not set and Template{Head, Middle, Tail} Contains NotEscapeSequence.
if (string_literal_type != StringLiteralType::Normal)
message = "Octal escape sequence not allowed in template literal";
else if (m_state.strict_mode)
message = "Octal escape sequence in string literal not allowed in strict mode";
Expand All @@ -1833,11 +1835,17 @@ NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token const& token, bo
VERIFY_NOT_REACHED();
}

if (!message.is_empty())
syntax_error(message, Position { token.line_number(), token.line_column() });
if (!message.is_empty()) {
if (contains_invalid_escape != nullptr) {
VERIFY(string_literal_type == StringLiteralType::TaggedTemplate);
*contains_invalid_escape = true;
} else {
syntax_error(message, Position { token.line_number(), token.line_column() });
}
}
}

auto is_use_strict_directive = !in_template_literal && (token.value() == "'use strict'" || token.value() == "\"use strict\"");
auto is_use_strict_directive = string_literal_type == StringLiteralType::Normal && (token.value() == "'use strict'" || token.value() == "\"use strict\"");

return create_ast_node<StringLiteral>({ m_state.current_token.filename(), rule_start.position(), position() }, string, is_use_strict_directive);
}
Expand All @@ -1863,7 +1871,15 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal(bool is_tagged)
while (!done() && !match(TokenType::TemplateLiteralEnd) && !match(TokenType::UnterminatedTemplateLiteral)) {
if (match(TokenType::TemplateLiteralString)) {
auto token = consume();
expressions.append(parse_string_literal(token, true));
bool contains_invalid_escape = false;
auto parsed_string_value = parse_string_literal(token,
is_tagged ? StringLiteralType::TaggedTemplate : StringLiteralType::NonTaggedTemplate,
is_tagged ? &contains_invalid_escape : nullptr);
// An invalid string leads to a cooked value of `undefined` but still gives the raw string.
if (contains_invalid_escape)
expressions.append(create_ast_node<NullLiteral>({ m_state.current_token.filename(), rule_start.position(), position() }));
else
expressions.append(move(parsed_string_value));
if (is_tagged)
raw_strings.append(create_ast_node<StringLiteral>({ m_state.current_token.filename(), rule_start.position(), position() }, token.raw_template_value()));
} else if (match(TokenType::TemplateLiteralExprStart)) {
Expand Down Expand Up @@ -4011,7 +4027,7 @@ FlyString Parser::consume_string_value()
{
VERIFY(match(TokenType::StringLiteral));
auto string_token = consume();
FlyString value = parse_string_literal(string_token, false)->value();
FlyString value = parse_string_literal(string_token)->value();

// This also checks IsStringWellFormedUnicode which makes sure there is no unpaired surrogate
// Surrogates are at least 3 bytes
Expand Down
9 changes: 8 additions & 1 deletion Userland/Libraries/LibJS/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,14 @@ class Parser {
NonnullRefPtr<RegExpLiteral> parse_regexp_literal();
NonnullRefPtr<ObjectExpression> parse_object_expression();
NonnullRefPtr<ArrayExpression> parse_array_expression();
NonnullRefPtr<StringLiteral> parse_string_literal(Token const& token, bool in_template_literal = false);

enum class StringLiteralType {
Normal,
NonTaggedTemplate,
TaggedTemplate
};

NonnullRefPtr<StringLiteral> parse_string_literal(Token const& token, StringLiteralType string_literal_type = StringLiteralType::Normal, bool* contains_invalid_escape = nullptr);
NonnullRefPtr<TemplateLiteral> parse_template_literal(bool is_tagged);
ExpressionResult parse_secondary_expression(NonnullRefPtr<Expression>, int min_precedence, Associativity associate = Associativity::Right, ForbiddenTokens forbidden = {});
NonnullRefPtr<Expression> parse_call_expression(NonnullRefPtr<Expression>);
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibJS/Runtime/FunctionPrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::bind)
auto* function = TRY(BoundFunction::create(global_object, target, this_argument, move(arguments)));

// 4. Let argCount be the number of elements in args.
auto arg_count = vm.argument_count() - 1;
auto arg_count = vm.argument_count() > 0 ? vm.argument_count() - 1 : 0;

// 5. Perform ? CopyNameAndLength(F, Target, "bound", argCount).
TRY(copy_name_and_length(global_object, *function, target, "bound"sv, arg_count));
Expand Down
26 changes: 26 additions & 0 deletions Userland/Libraries/LibJS/Runtime/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -462,23 +462,49 @@ PrivateElement* Object::private_element_find(PrivateName const& name)
// 7.3.28 PrivateFieldAdd ( O, P, value ), https://tc39.es/ecma262/#sec-privatefieldadd
ThrowCompletionOr<void> Object::private_field_add(PrivateName const& name, Value value)
{
// 1. If the host is a web browser, then
// a. Perform ? HostEnsureCanAddPrivateElement(O).
// NOTE: Since LibJS has no way of knowing whether it is in a browser we just always call the hook.
TRY(vm().host_ensure_can_add_private_element(*this));

// 2. Let entry be PrivateElementFind(O, P).
// 3. If entry is not empty, throw a TypeError exception.
if (auto* entry = private_element_find(name); entry)
return vm().throw_completion<TypeError>(global_object(), ErrorType::PrivateFieldAlreadyDeclared, name.description);

if (!m_private_elements)
m_private_elements = make<Vector<PrivateElement>>();

// 4. Append PrivateElement { [[Key]]: P, [[Kind]]: field, [[Value]]: value } to O.[[PrivateElements]].
m_private_elements->empend(name, PrivateElement::Kind::Field, value);

// 5. Return unused.
return {};
}

// 7.3.29 PrivateMethodOrAccessorAdd ( O, method ), https://tc39.es/ecma262/#sec-privatemethodoraccessoradd
ThrowCompletionOr<void> Object::private_method_or_accessor_add(PrivateElement element)
{
// 1. Assert: method.[[Kind]] is either method or accessor.
VERIFY(element.kind == PrivateElement::Kind::Method || element.kind == PrivateElement::Kind::Accessor);

// 2. If the host is a web browser, then
// a. Perform ? HostEnsureCanAddPrivateElement(O).
// NOTE: Since LibJS has no way of knowing whether it is in a browser we just always call the hook.
TRY(vm().host_ensure_can_add_private_element(*this));

// 3. Let entry be PrivateElementFind(O, method.[[Key]]).
// 4. If entry is not empty, throw a TypeError exception.
if (auto* entry = private_element_find(element.key); entry)
return vm().throw_completion<TypeError>(global_object(), ErrorType::PrivateFieldAlreadyDeclared, element.key.description);

if (!m_private_elements)
m_private_elements = make<Vector<PrivateElement>>();

// 5. Append method to O.[[PrivateElements]].
m_private_elements->append(move(element));

// 6. Return unused.
return {};
}

Expand Down
15 changes: 15 additions & 0 deletions Userland/Libraries/LibJS/Runtime/VM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ VM::VM(OwnPtr<CustomData> custom_data)
return {};
};

host_ensure_can_add_private_element = [](Object&) -> ThrowCompletionOr<void> {
// The host-defined abstract operation HostEnsureCanAddPrivateElement takes argument O (an Object)
// and returns either a normal completion containing unused or a throw completion.
// It allows host environments to prevent the addition of private elements to particular host-defined exotic objects.
// An implementation of HostEnsureCanAddPrivateElement must conform to the following requirements:
// - If O is not a host-defined exotic object, this abstract operation must return NormalCompletion(unused) and perform no other steps.
// - Any two calls of this abstract operation with the same argument must return the same kind of Completion Record.
// The default implementation of HostEnsureCanAddPrivateElement is to return NormalCompletion(unused).
return {};

// This abstract operation is only invoked by ECMAScript hosts that are web browsers.
// NOTE: Since LibJS has no way of knowing whether the current environment is a browser we always
// call HostEnsureCanAddPrivateElement when needed.
};

#define __JS_ENUMERATE(SymbolName, snake_name) \
m_well_known_symbol_##snake_name = js_symbol(*this, "Symbol." #SymbolName, false);
JS_ENUMERATE_WELL_KNOWN_SYMBOLS
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibJS/Runtime/VM.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ class VM : public RefCounted<VM> {
Function<void(Function<ThrowCompletionOr<Value>()>, Realm*)> host_enqueue_promise_job;
Function<JobCallback(FunctionObject&)> host_make_job_callback;
Function<ThrowCompletionOr<void>(Realm&)> host_ensure_can_compile_strings;
Function<ThrowCompletionOr<void>(Object&)> host_ensure_can_add_private_element;

private:
explicit VM(OwnPtr<CustomData>);
Expand Down
5 changes: 5 additions & 0 deletions Userland/Libraries/LibJS/Runtime/Value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,11 @@ ThrowCompletionOr<Value> Value::to_number(GlobalObject& global_object) const
auto parsed_double = strtod(string.characters(), &endptr);
if (*endptr)
return js_nan();
// NOTE: Per the spec only exactly [+-]Infinity should result in infinity
// but strtod gives infinity for any case-insensitive 'infinity' or 'inf' string.
if (isinf(parsed_double) && string.contains('i', AK::CaseSensitivity::CaseInsensitive))
return js_nan();

return Value(parsed_double);
}
case SYMBOL_TAG:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ describe("bound function |this|", () => {
test("arrow functions cannot be bound", () => {
expect((() => this).bind("foo")()).toBe(globalThis);
});

test("length of original function is used for bound function", () => {
[0, 1, 2147483647, 2147483648, 2147483649].forEach(value => {
function emptyFunction() {}

Object.defineProperty(emptyFunction, "length", { value });

expect(emptyFunction.bind().length).toBe(value);
expect(emptyFunction.bind(null).length).toBe(value);
expect(emptyFunction.bind(null, 0).length).toBe(Math.max(0, value - 1));
expect(emptyFunction.bind(null, 0, 1, 2).length).toBe(Math.max(0, value - 3));
});
});
});

describe("bound function constructors", () => {
Expand Down
Loading