Преглед на файлове

LibJS: Implement parsing and execution of optional chains

Ali Mohammad Pur преди 3 години
родител
ревизия
72ddaa31e3

+ 108 - 32
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<MemberExpression>(*m_callee)) {
-        auto& member_expression = static_cast<MemberExpression const&>(*m_callee);
-        Value callee;
-        Value this_value;
-
-        if (is<SuperExpression>(member_expression.object())) {
-            auto super_base = interpreter.current_function_environment()->get_super_base();
-            if (super_base.is_nullish()) {
-                vm.throw_exception<TypeError>(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<Identifier>(*m_callee) && static_cast<Identifier const&>(*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::ReferenceAndValue> 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<JS::Reference> 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<Expression> {
+                return create_ast_node<CallExpression>(source_range(),
+                    create_ast_node<SyntheticReferenceExpression>(source_range(), *base_reference, base),
+                    call.arguments);
+            },
+            [&](ComputedReference const& ref) -> NonnullRefPtr<Expression> {
+                return create_ast_node<MemberExpression>(source_range(),
+                    create_ast_node<SyntheticReferenceExpression>(source_range(), *base_reference, base),
+                    ref.expression,
+                    true);
+            },
+            [&](MemberReference const& ref) -> NonnullRefPtr<Expression> {
+                return create_ast_node<MemberExpression>(source_range(),
+                    create_ast_node<SyntheticReferenceExpression>(source_range(), *base_reference, base),
+                    ref.identifier,
+                    false);
+            });
+        if (is<CallExpression>(*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;

+ 63 - 1
Userland/Libraries/LibJS/AST.h

@@ -17,6 +17,7 @@
 #include <AK/Vector.h>
 #include <LibJS/Forward.h>
 #include <LibJS/Runtime/PropertyName.h>
+#include <LibJS/Runtime/Reference.h>
 #include <LibJS/Runtime/Value.h>
 #include <LibJS/SourceRange.h>
 #include <LibRegex/Regex.h>
@@ -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<CallExpression::Argument> arguments;
+        Mode mode;
+    };
+    struct ComputedReference {
+        NonnullRefPtr<Expression> expression;
+        Mode mode;
+    };
+    struct MemberReference {
+        NonnullRefPtr<Identifier> identifier;
+        Mode mode;
+    };
+
+    using Reference = Variant<Call, ComputedReference, MemberReference>;
+
+    OptionalChain(SourceRange source_range, NonnullRefPtr<Expression> base, Vector<Reference> 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<ReferenceAndValue> to_reference_and_value(Interpreter&, GlobalObject&) const;
+
+    NonnullRefPtr<Expression> m_base;
+    Vector<Reference> 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<typename C>
 void BindingPattern::for_each_bound_name(C&& callback) const
 {

+ 98 - 9
Userland/Libraries/LibJS/Parser.cpp

@@ -1521,6 +1521,15 @@ NonnullRefPtr<Expression> Parser::parse_secondary_expression(NonnullRefPtr<Expre
         return parse_assignment_expression(AssignmentOp::NullishAssignment, move(lhs), min_precedence, associativity);
     case TokenType::QuestionMark:
         return parse_conditional_expression(move(lhs));
+    case TokenType::QuestionMarkPeriod:
+        // FIXME: This should allow `(new Foo)?.bar', but as our parser strips parenthesis,
+        //        we can't really tell if `lhs' was parenthesized at this point.
+        if (is<NewExpression>(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<Identifier> Parser::parse_identifier()
         token.value());
 }
 
-NonnullRefPtr<Expression> Parser::parse_call_expression(NonnullRefPtr<Expression> lhs)
+Vector<CallExpression::Argument> Parser::parse_arguments()
 {
-    auto rule_start = push_start();
-    if (!m_state.allow_super_constructor_call && is<SuperExpression>(*lhs))
-        syntax_error("'super' keyword unexpected here");
-
-    consume(TokenType::ParenOpen);
-
     Vector<CallExpression::Argument> arguments;
 
+    consume(TokenType::ParenOpen);
     while (match_expression() || match(TokenType::TripleDot)) {
         if (match(TokenType::TripleDot)) {
             consume();
@@ -1643,6 +1647,16 @@ NonnullRefPtr<Expression> Parser::parse_call_expression(NonnullRefPtr<Expression
     }
 
     consume(TokenType::ParenClose);
+    return arguments;
+}
+
+NonnullRefPtr<Expression> Parser::parse_call_expression(NonnullRefPtr<Expression> lhs)
+{
+    auto rule_start = push_start();
+    if (!m_state.allow_super_constructor_call && is<SuperExpression>(*lhs))
+        syntax_error("'super' keyword unexpected here");
+
+    auto arguments = parse_arguments();
 
     if (is<SuperExpression>(*lhs))
         return create_ast_node<SuperCall>({ m_state.current_token.filename(), rule_start.position(), position() }, move(arguments));
@@ -1655,7 +1669,7 @@ NonnullRefPtr<NewExpression> 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<CallExpression::Argument> arguments;
 
@@ -2372,6 +2386,80 @@ NonnullRefPtr<ConditionalExpression> Parser::parse_conditional_expression(Nonnul
     return create_ast_node<ConditionalExpression>({ m_state.current_token.filename(), rule_start.position(), position() }, move(test), move(consequent), move(alternate));
 }
 
+NonnullRefPtr<OptionalChain> Parser::parse_optional_chain(NonnullRefPtr<Expression> base)
+{
+    auto rule_start = push_start();
+    Vector<OptionalChain::Reference> 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<Identifier>({ 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<Identifier>({ 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<OptionalChain>(
+        { m_state.current_token.filename(), rule_start.position(), position() },
+        move(base),
+        move(chain));
+}
+
 NonnullRefPtr<TryStatement> Parser::parse_try_statement()
 {
     auto rule_start = push_start();
@@ -2788,7 +2876,8 @@ bool Parser::match_secondary_expression(const Vector<TokenType>& 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

+ 3 - 0
Userland/Libraries/LibJS/Parser.h

@@ -76,6 +76,7 @@ public:
     NonnullRefPtr<WithStatement> parse_with_statement();
     NonnullRefPtr<DebuggerStatement> parse_debugger_statement();
     NonnullRefPtr<ConditionalExpression> parse_conditional_expression(NonnullRefPtr<Expression> test);
+    NonnullRefPtr<OptionalChain> parse_optional_chain(NonnullRefPtr<Expression> base);
     NonnullRefPtr<Expression> parse_expression(int min_precedence, Associativity associate = Associativity::Right, const Vector<TokenType>& forbidden = {});
     PrimaryExpressionParseResult parse_primary_expression();
     NonnullRefPtr<Expression> parse_unary_prefixed_expression();
@@ -100,6 +101,8 @@ public:
     RefPtr<Statement> try_parse_labelled_statement(AllowLabelledFunction allow_function);
     RefPtr<MetaProperty> try_parse_new_target_expression();
 
+    Vector<CallExpression::Argument> parse_arguments();
+
     struct Error {
         String message;
         Optional<Position> position;

+ 6 - 0
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&);

+ 40 - 0
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();
+});