From 72ddaa31e3b0e542107b60c625c50fc00768da75 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Pur Date: Tue, 14 Sep 2021 06:56:31 +0430 Subject: [PATCH] LibJS: Implement parsing and execution of optional chains --- Userland/Libraries/LibJS/AST.cpp | 140 ++++++++++++++---- Userland/Libraries/LibJS/AST.h | 64 +++++++- Userland/Libraries/LibJS/Parser.cpp | 107 +++++++++++-- Userland/Libraries/LibJS/Parser.h | 3 + Userland/Libraries/LibJS/Runtime/Reference.h | 6 + .../LibJS/Tests/syntax/optional-chaining.js | 40 +++++ 6 files changed, 318 insertions(+), 42 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/syntax/optional-chaining.js diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index 7974961b78..aad0fe71c4 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -125,44 +125,26 @@ Value ExpressionStatement::execute(Interpreter& interpreter, GlobalObject& globa return m_expression->execute(interpreter, global_object); } -CallExpression::ThisAndCallee CallExpression::compute_this_and_callee(Interpreter& interpreter, GlobalObject& global_object) const +CallExpression::ThisAndCallee CallExpression::compute_this_and_callee(Interpreter& interpreter, GlobalObject& global_object, Reference const& callee_reference) const { auto& vm = interpreter.vm(); - if (is(*m_callee)) { - auto& member_expression = static_cast(*m_callee); - Value callee; - Value this_value; - - if (is(member_expression.object())) { - auto super_base = interpreter.current_function_environment()->get_super_base(); - if (super_base.is_nullish()) { - vm.throw_exception(global_object, ErrorType::ObjectPrototypeNullOrUndefinedOnSuperPropertyAccess, super_base.to_string_without_side_effects()); - return {}; - } - auto property_name = member_expression.computed_property_name(interpreter, global_object); - if (!property_name.is_valid()) - return {}; - auto reference = Reference { super_base, move(property_name), super_base, vm.in_strict_mode() }; - callee = reference.get_value(global_object); - if (vm.exception()) - return {}; - this_value = &vm.this_value(global_object).as_object(); - } else { - auto reference = member_expression.to_reference(interpreter, global_object); - if (vm.exception()) - return {}; - callee = reference.get_value(global_object); - if (vm.exception()) - return {}; - this_value = reference.get_this_value(); - } + if (callee_reference.is_property_reference()) { + auto this_value = callee_reference.get_this_value(); + auto callee = callee_reference.get_value(global_object); + if (vm.exception()) + return {}; return { this_value, callee }; } // [[Call]] will handle that in non-strict mode the this value becomes the global object - return { js_undefined(), m_callee->execute(interpreter, global_object) }; + return { + js_undefined(), + callee_reference.is_unresolvable() + ? m_callee->execute(interpreter, global_object) + : callee_reference.get_value(global_object) + }; } // 13.3.8.1 Runtime Semantics: ArgumentListEvaluation, https://tc39.es/ecma262/#sec-runtime-semantics-argumentlistevaluation @@ -233,7 +215,11 @@ Value CallExpression::execute(Interpreter& interpreter, GlobalObject& global_obj { InterpreterNodeScope node_scope { interpreter, *this }; auto& vm = interpreter.vm(); - auto [this_value, callee] = compute_this_and_callee(interpreter, global_object); + auto callee_reference = m_callee->to_reference(interpreter, global_object); + if (vm.exception()) + return {}; + + auto [this_value, callee] = compute_this_and_callee(interpreter, global_object, callee_reference); if (vm.exception()) return {}; @@ -251,7 +237,11 @@ Value CallExpression::execute(Interpreter& interpreter, GlobalObject& global_obj auto& function = callee.as_function(); - if (&function == global_object.eval_function() && is(*m_callee) && static_cast(*m_callee).string() == vm.names.eval.as_string()) { + if (&function == global_object.eval_function() + && callee_reference.is_environment_reference() + && callee_reference.name().is_string() + && callee_reference.name().as_string() == vm.names.eval.as_string()) { + auto script_value = arg_list.size() == 0 ? js_undefined() : arg_list[0]; return perform_eval(script_value, global_object, vm.in_strict_mode() ? CallerMode::Strict : CallerMode::NonStrict, EvalMode::Direct); } @@ -2011,6 +2001,92 @@ Value MemberExpression::execute(Interpreter& interpreter, GlobalObject& global_o return reference.get_value(global_object); } +void OptionalChain::dump(int indent) const +{ + print_indent(indent); + outln("{}", class_name()); + m_base->dump(indent + 1); + for (auto& reference : m_references) { + reference.visit( + [&](Call const& call) { + print_indent(indent + 1); + outln("Call({})", call.mode == Mode::Optional ? "Optional" : "Not Optional"); + for (auto& argument : call.arguments) + argument.value->dump(indent + 2); + }, + [&](ComputedReference const& ref) { + print_indent(indent + 1); + outln("ComputedReference({})", ref.mode == Mode::Optional ? "Optional" : "Not Optional"); + ref.expression->dump(indent + 2); + }, + [&](MemberReference const& ref) { + print_indent(indent + 1); + outln("MemberReference({})", ref.mode == Mode::Optional ? "Optional" : "Not Optional"); + ref.identifier->dump(indent + 2); + }); + } +} + +Optional OptionalChain::to_reference_and_value(JS::Interpreter& interpreter, JS::GlobalObject& global_object) const +{ + // Note: This is wrapped in an optional to allow base_reference = ... + Optional base_reference = m_base->to_reference(interpreter, global_object); + auto base = base_reference->is_unresolvable() ? m_base->execute(interpreter, global_object) : base_reference->get_value(global_object); + if (interpreter.exception()) + return {}; + + for (auto& reference : m_references) { + auto is_optional = reference.visit([](auto& ref) { return ref.mode; }) == Mode::Optional; + if (is_optional && base.is_nullish()) + return ReferenceAndValue { {}, js_undefined() }; + + auto expression = reference.visit( + [&](Call const& call) -> NonnullRefPtr { + return create_ast_node(source_range(), + create_ast_node(source_range(), *base_reference, base), + call.arguments); + }, + [&](ComputedReference const& ref) -> NonnullRefPtr { + return create_ast_node(source_range(), + create_ast_node(source_range(), *base_reference, base), + ref.expression, + true); + }, + [&](MemberReference const& ref) -> NonnullRefPtr { + return create_ast_node(source_range(), + create_ast_node(source_range(), *base_reference, base), + ref.identifier, + false); + }); + if (is(*expression)) { + base_reference = JS::Reference {}; + base = expression->execute(interpreter, global_object); + } else { + base_reference = expression->to_reference(interpreter, global_object); + base = base_reference->get_value(global_object); + } + if (interpreter.exception()) + return {}; + } + + return ReferenceAndValue { base_reference.release_value(), base }; +} + +Value OptionalChain::execute(Interpreter& interpreter, GlobalObject& global_object) const +{ + InterpreterNodeScope node_scope { interpreter, *this }; + if (auto result = to_reference_and_value(interpreter, global_object); result.has_value()) + return result.release_value().value; + return {}; +} + +JS::Reference OptionalChain::to_reference(Interpreter& interpreter, GlobalObject& global_object) const +{ + if (auto result = to_reference_and_value(interpreter, global_object); result.has_value()) + return result.release_value().reference; + return {}; +} + void MetaProperty::dump(int indent) const { String name; diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index b400640bce..2b3557ca49 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -1069,7 +1070,7 @@ private: Value callee; }; - ThisAndCallee compute_this_and_callee(Interpreter&, GlobalObject&) const; + ThisAndCallee compute_this_and_callee(Interpreter&, GlobalObject&, Reference const&) const; }; class NewExpression final : public CallExpression { @@ -1384,6 +1385,50 @@ private: bool m_computed { false }; }; +class OptionalChain final : public Expression { +public: + enum class Mode { + Optional, + NotOptional, + }; + + struct Call { + Vector arguments; + Mode mode; + }; + struct ComputedReference { + NonnullRefPtr expression; + Mode mode; + }; + struct MemberReference { + NonnullRefPtr identifier; + Mode mode; + }; + + using Reference = Variant; + + OptionalChain(SourceRange source_range, NonnullRefPtr base, Vector references) + : Expression(source_range) + , m_base(move(base)) + , m_references(move(references)) + { + } + + virtual Value execute(Interpreter& interpreter, GlobalObject& global_object) const override; + virtual JS::Reference to_reference(Interpreter& interpreter, GlobalObject& global_object) const override; + virtual void dump(int indent) const override; + +private: + struct ReferenceAndValue { + JS::Reference reference; + Value value; + }; + Optional to_reference_and_value(Interpreter&, GlobalObject&) const; + + NonnullRefPtr m_base; + Vector m_references; +}; + class MetaProperty final : public Expression { public: enum class Type { @@ -1576,6 +1621,23 @@ public: virtual void generate_bytecode(Bytecode::Generator&) const override; }; +class SyntheticReferenceExpression final : public Expression { +public: + explicit SyntheticReferenceExpression(SourceRange source_range, Reference reference, Value value) + : Expression(source_range) + , m_reference(move(reference)) + , m_value(value) + { + } + + virtual Value execute(Interpreter&, GlobalObject&) const override { return m_value; } + virtual Reference to_reference(Interpreter&, GlobalObject&) const override { return m_reference; } + +private: + Reference m_reference; + Value m_value; +}; + template void BindingPattern::for_each_bound_name(C&& callback) const { diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index b4c9d76693..096ab55e02 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -1521,6 +1521,15 @@ NonnullRefPtr Parser::parse_secondary_expression(NonnullRefPtr(lhs.ptr())) { + syntax_error("'new' cannot be used with optional chaining", position()); + consume(); + return lhs; + } + return parse_optional_chain(move(lhs)); default: expected("secondary expression"); consume(); @@ -1620,16 +1629,11 @@ NonnullRefPtr Parser::parse_identifier() token.value()); } -NonnullRefPtr Parser::parse_call_expression(NonnullRefPtr lhs) +Vector Parser::parse_arguments() { - auto rule_start = push_start(); - if (!m_state.allow_super_constructor_call && is(*lhs)) - syntax_error("'super' keyword unexpected here"); - - consume(TokenType::ParenOpen); - Vector arguments; + consume(TokenType::ParenOpen); while (match_expression() || match(TokenType::TripleDot)) { if (match(TokenType::TripleDot)) { consume(); @@ -1643,6 +1647,16 @@ NonnullRefPtr Parser::parse_call_expression(NonnullRefPtr Parser::parse_call_expression(NonnullRefPtr lhs) +{ + auto rule_start = push_start(); + if (!m_state.allow_super_constructor_call && is(*lhs)) + syntax_error("'super' keyword unexpected here"); + + auto arguments = parse_arguments(); if (is(*lhs)) return create_ast_node({ m_state.current_token.filename(), rule_start.position(), position() }, move(arguments)); @@ -1655,7 +1669,7 @@ NonnullRefPtr Parser::parse_new_expression() auto rule_start = push_start(); consume(TokenType::New); - auto callee = parse_expression(g_operator_precedence.get(TokenType::New), Associativity::Right, { TokenType::ParenOpen }); + auto callee = parse_expression(g_operator_precedence.get(TokenType::New), Associativity::Right, { TokenType::ParenOpen, TokenType::QuestionMarkPeriod }); Vector arguments; @@ -2372,6 +2386,80 @@ NonnullRefPtr Parser::parse_conditional_expression(Nonnul return create_ast_node({ m_state.current_token.filename(), rule_start.position(), position() }, move(test), move(consequent), move(alternate)); } +NonnullRefPtr Parser::parse_optional_chain(NonnullRefPtr base) +{ + auto rule_start = push_start(); + Vector chain; + do { + if (match(TokenType::QuestionMarkPeriod)) { + consume(TokenType::QuestionMarkPeriod); + switch (m_state.current_token.type()) { + case TokenType::ParenOpen: + chain.append(OptionalChain::Call { parse_arguments(), OptionalChain::Mode::Optional }); + break; + case TokenType::BracketOpen: + consume(); + chain.append(OptionalChain::ComputedReference { parse_expression(0), OptionalChain::Mode::Optional }); + consume(TokenType::BracketClose); + break; + case TokenType::TemplateLiteralStart: + // 13.3.1.1 - Static Semantics: Early Errors + // OptionalChain : + // ?. TemplateLiteral + // OptionalChain TemplateLiteral + // This is a hard error. + syntax_error("Invalid tagged template literal after ?.", position()); + break; + default: + if (match_identifier_name()) { + auto start = position(); + auto identifier = consume(); + chain.append(OptionalChain::MemberReference { + create_ast_node({ m_state.current_token.filename(), start, position() }, identifier.value()), + OptionalChain::Mode::Optional, + }); + } else { + syntax_error("Invalid optional chain reference after ?.", position()); + } + break; + } + } else if (match(TokenType::ParenOpen)) { + chain.append(OptionalChain::Call { parse_arguments(), OptionalChain::Mode::NotOptional }); + } else if (match(TokenType::Period)) { + consume(); + if (match_identifier_name()) { + auto start = position(); + auto identifier = consume(); + chain.append(OptionalChain::MemberReference { + create_ast_node({ m_state.current_token.filename(), start, position() }, identifier.value()), + OptionalChain::Mode::NotOptional, + }); + } else { + expected("an identifier"); + break; + } + } else if (match(TokenType::TemplateLiteralStart)) { + // 13.3.1.1 - Static Semantics: Early Errors + // OptionalChain : + // ?. TemplateLiteral + // OptionalChain TemplateLiteral + syntax_error("Invalid tagged template literal after optional chain", position()); + break; + } else if (match(TokenType::BracketOpen)) { + consume(); + chain.append(OptionalChain::ComputedReference { parse_expression(2), OptionalChain::Mode::NotOptional }); + consume(TokenType::BracketClose); + } else { + break; + } + } while (!done()); + + return create_ast_node( + { m_state.current_token.filename(), rule_start.position(), position() }, + move(base), + move(chain)); +} + NonnullRefPtr Parser::parse_try_statement() { auto rule_start = push_start(); @@ -2788,7 +2876,8 @@ bool Parser::match_secondary_expression(const Vector& forbidden) cons || type == TokenType::DoublePipe || type == TokenType::DoublePipeEquals || type == TokenType::DoubleQuestionMark - || type == TokenType::DoubleQuestionMarkEquals; + || type == TokenType::DoubleQuestionMarkEquals + || type == TokenType::QuestionMarkPeriod; } bool Parser::match_statement() const diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index b4000ab774..d961353664 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -76,6 +76,7 @@ public: NonnullRefPtr parse_with_statement(); NonnullRefPtr parse_debugger_statement(); NonnullRefPtr parse_conditional_expression(NonnullRefPtr test); + NonnullRefPtr parse_optional_chain(NonnullRefPtr base); NonnullRefPtr parse_expression(int min_precedence, Associativity associate = Associativity::Right, const Vector& forbidden = {}); PrimaryExpressionParseResult parse_primary_expression(); NonnullRefPtr parse_unary_prefixed_expression(); @@ -100,6 +101,8 @@ public: RefPtr try_parse_labelled_statement(AllowLabelledFunction allow_function); RefPtr try_parse_new_target_expression(); + Vector parse_arguments(); + struct Error { String message; Optional position; diff --git a/Userland/Libraries/LibJS/Runtime/Reference.h b/Userland/Libraries/LibJS/Runtime/Reference.h index 6c663335ee..8d3fa3e260 100644 --- a/Userland/Libraries/LibJS/Runtime/Reference.h +++ b/Userland/Libraries/LibJS/Runtime/Reference.h @@ -97,6 +97,12 @@ public: return !m_this_value.is_empty(); } + // Note: Non-standard helper. + bool is_environment_reference() const + { + return m_base_type == BaseType::Environment; + } + void put_value(GlobalObject&, Value); Value get_value(GlobalObject&, bool throw_if_undefined = true) const; bool delete_(GlobalObject&); diff --git a/Userland/Libraries/LibJS/Tests/syntax/optional-chaining.js b/Userland/Libraries/LibJS/Tests/syntax/optional-chaining.js new file mode 100644 index 0000000000..bbd221b6e1 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/syntax/optional-chaining.js @@ -0,0 +1,40 @@ +test("parse optional-chaining", () => { + expect(`a?.b`).toEval(); + expect(`a?.4:.5`).toEval(); + expect(`a?.[b]`).toEval(); + expect(`a?.b[b]`).toEval(); + expect(`a?.b(c)`).toEval(); + expect(`a?.b?.(c, d)`).toEval(); + expect(`a?.b?.()`).toEval(); + expect("a?.b``").not.toEval(); + expect("a?.b?.``").not.toEval(); + expect("new Foo?.bar").not.toEval(); + expect("new (Foo?.bar)").toEval(); + // FIXME: This should pass. + // expect("(new Foo)?.bar").toEval(); +}); + +test("evaluate optional-chaining", () => { + for (let nullishObject of [null, undefined]) { + expect((() => nullishObject?.b)()).toBeUndefined(); + } + + expect( + (() => { + let a = {}; + return a?.foo?.bar?.baz; + })() + ).toBeUndefined(); + + expect( + (() => { + let a = { foo: { bar: () => 42 } }; + return `${a?.foo?.bar?.()}-${a?.foo?.baz?.()}`; + })() + ).toBe("42-undefined"); + + expect(() => { + let a = { foo: { bar: () => 42 } }; + return a.foo?.baz.nonExistentProperty; + }).toThrow(); +});