From 39a1c9d82764d448ba684c544ba6d14e09062274 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Mon, 2 Nov 2020 21:27:42 +0000 Subject: [PATCH] LibJS: Implement 'new.target' This adds a new MetaProperty AST node which will be used for 'new.target' and 'import.meta' meta properties. The parser now distinguishes between "in function context" and "in arrow function context" (which is required for this). When encountering TokenType::New we will attempt to parse it as meta property and resort to regular new expression parsing if that fails, much like the parsing of labelled statements. --- Libraries/LibJS/AST.cpp | 22 ++++++++++ Libraries/LibJS/AST.h | 21 ++++++++++ Libraries/LibJS/Parser.cpp | 40 +++++++++++++++---- Libraries/LibJS/Parser.h | 8 ++-- Libraries/LibJS/Runtime/ScriptFunction.cpp | 2 + .../Tests/functions/function-new-target.js | 26 ++++++++++++ 6 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 Libraries/LibJS/Tests/functions/function-new-target.js diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index fec8aeba072..b40b87e1f00 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -1655,6 +1655,28 @@ Value MemberExpression::execute(Interpreter& interpreter, GlobalObject& global_o return object_result->get(property_name).value_or(js_undefined()); } +void MetaProperty::dump(int indent) const +{ + String name; + if (m_type == MetaProperty::Type::NewTarget) + name = "new.target"; + else if (m_type == MetaProperty::Type::ImportMeta) + name = "import.meta"; + else + ASSERT_NOT_REACHED(); + print_indent(indent); + printf("%s %s\n", class_name(), name.characters()); +} + +Value MetaProperty::execute(Interpreter& interpreter, GlobalObject&) const +{ + if (m_type == MetaProperty::Type::NewTarget) + return interpreter.vm().get_new_target().value_or(js_undefined()); + if (m_type == MetaProperty::Type::ImportMeta) + TODO(); + ASSERT_NOT_REACHED(); +} + Value StringLiteral::execute(Interpreter& interpreter, GlobalObject&) const { return js_string(interpreter.heap(), m_value); diff --git a/Libraries/LibJS/AST.h b/Libraries/LibJS/AST.h index 6de9d534b06..9f5a8deb611 100644 --- a/Libraries/LibJS/AST.h +++ b/Libraries/LibJS/AST.h @@ -1084,6 +1084,27 @@ private: bool m_computed { false }; }; +class MetaProperty final : public Expression { +public: + enum class Type { + NewTarget, + ImportMeta, + }; + + MetaProperty(Type type) + : m_type(type) + { + } + + virtual Value execute(Interpreter&, GlobalObject&) const override; + virtual void dump(int indent) const override; + +private: + virtual const char* class_name() const override { return "MetaProperty"; } + + Type m_type; +}; + class ConditionalExpression final : public Expression { public: ConditionalExpression(NonnullRefPtr test, NonnullRefPtr consequent, NonnullRefPtr alternate) diff --git a/Libraries/LibJS/Parser.cpp b/Libraries/LibJS/Parser.cpp index 79d47cf889f..b04530ff8f2 100644 --- a/Libraries/LibJS/Parser.cpp +++ b/Libraries/LibJS/Parser.cpp @@ -401,7 +401,7 @@ RefPtr Parser::try_parse_arrow_function_expression(bool expe bool is_strict = false; auto function_body_result = [&]() -> RefPtr { - TemporaryChange change(m_parser_state.m_in_function_context, true); + TemporaryChange change(m_parser_state.m_in_arrow_function_context, true); if (match(TokenType::CurlyOpen)) { // Parse a function body with statements return parse_block_statement(is_strict); @@ -454,6 +454,26 @@ RefPtr Parser::try_parse_labelled_statement() return statement; } +RefPtr Parser::try_parse_new_target_expression() +{ + save_state(); + ArmedScopeGuard state_rollback_guard = [&] { + load_state(); + }; + + consume(TokenType::New); + if (!match(TokenType::Period)) + return {}; + consume(); + if (!match(TokenType::Identifier)) + return {}; + if (consume().value() != "target") + return {}; + + state_rollback_guard.disarm(); + return create_ast_node(MetaProperty::Type::NewTarget); +} + NonnullRefPtr Parser::parse_class_declaration() { return create_ast_node(parse_class_expression(true)); @@ -593,9 +613,8 @@ NonnullRefPtr Parser::parse_primary_expression() consume(TokenType::ParenOpen); if (match(TokenType::ParenClose) || match(TokenType::Identifier) || match(TokenType::TripleDot)) { auto arrow_function_result = try_parse_arrow_function_expression(true); - if (!arrow_function_result.is_null()) { + if (!arrow_function_result.is_null()) return arrow_function_result.release_nonnull(); - } } auto expression = parse_expression(0); consume(TokenType::ParenClose); @@ -613,9 +632,8 @@ NonnullRefPtr Parser::parse_primary_expression() return create_ast_node(); case TokenType::Identifier: { auto arrow_function_result = try_parse_arrow_function_expression(false); - if (!arrow_function_result.is_null()) { + if (!arrow_function_result.is_null()) return arrow_function_result.release_nonnull(); - } return create_ast_node(consume().value()); } case TokenType::NumericLiteral: @@ -639,8 +657,16 @@ NonnullRefPtr Parser::parse_primary_expression() return parse_regexp_literal(); case TokenType::TemplateLiteralStart: return parse_template_literal(false); - case TokenType::New: + case TokenType::New: { + auto new_start = position(); + auto new_target_result = try_parse_new_target_expression(); + if (!new_target_result.is_null()) { + if (!m_parser_state.m_in_function_context) + syntax_error("'new.target' not allowed outside of a function", new_start); + return new_target_result.release_nonnull(); + } return parse_new_expression(); + } default: expected("primary expression"); consume(); @@ -1191,7 +1217,7 @@ NonnullRefPtr Parser::parse_new_expression() NonnullRefPtr Parser::parse_return_statement() { - if (!m_parser_state.m_in_function_context) + if (!m_parser_state.m_in_function_context && !m_parser_state.m_in_arrow_function_context) syntax_error("'return' not allowed outside of a function"); consume(TokenType::Return); diff --git a/Libraries/LibJS/Parser.h b/Libraries/LibJS/Parser.h index 1d9d190ccf4..ac719569cfb 100644 --- a/Libraries/LibJS/Parser.h +++ b/Libraries/LibJS/Parser.h @@ -81,7 +81,6 @@ public: NonnullRefPtr parse_while_statement(); NonnullRefPtr parse_debugger_statement(); NonnullRefPtr parse_conditional_expression(NonnullRefPtr test); - NonnullRefPtr parse_expression(int min_precedence, Associativity associate = Associativity::Right, Vector forbidden = {}); NonnullRefPtr parse_primary_expression(); NonnullRefPtr parse_unary_prefixed_expression(); @@ -93,13 +92,15 @@ public: NonnullRefPtr parse_secondary_expression(NonnullRefPtr, int min_precedence, Associativity associate = Associativity::Right); NonnullRefPtr parse_call_expression(NonnullRefPtr); NonnullRefPtr parse_new_expression(); - RefPtr try_parse_arrow_function_expression(bool expect_parens); - RefPtr try_parse_labelled_statement(); NonnullRefPtr parse_class_declaration(); NonnullRefPtr parse_class_expression(bool expect_class_name); NonnullRefPtr parse_property_key(); NonnullRefPtr parse_assignment_expression(AssignmentOp, NonnullRefPtr lhs, int min_precedence, Associativity); + RefPtr try_parse_arrow_function_expression(bool expect_parens); + RefPtr try_parse_labelled_statement(); + RefPtr try_parse_new_target_expression(); + struct Position { size_t line; size_t column; @@ -181,6 +182,7 @@ private: bool m_allow_super_property_lookup { false }; bool m_allow_super_constructor_call { false }; bool m_in_function_context { false }; + bool m_in_arrow_function_context { false }; bool m_in_break_context { false }; bool m_in_continue_context { false }; bool m_string_legacy_octal_escape_sequence_in_scope { false }; diff --git a/Libraries/LibJS/Runtime/ScriptFunction.cpp b/Libraries/LibJS/Runtime/ScriptFunction.cpp index c3575b8c481..92737a1cbbd 100644 --- a/Libraries/LibJS/Runtime/ScriptFunction.cpp +++ b/Libraries/LibJS/Runtime/ScriptFunction.cpp @@ -105,6 +105,8 @@ LexicalEnvironment* ScriptFunction::create_environment() auto* environment = heap().allocate(global_object(), move(variables), m_parent_environment, LexicalEnvironment::EnvironmentRecordType::Function); environment->set_home_object(home_object()); environment->set_current_function(*this); + if (m_is_arrow_function) + environment->set_new_target(m_parent_environment->new_target()); return environment; } diff --git a/Libraries/LibJS/Tests/functions/function-new-target.js b/Libraries/LibJS/Tests/functions/function-new-target.js new file mode 100644 index 00000000000..8b5667d3f07 --- /dev/null +++ b/Libraries/LibJS/Tests/functions/function-new-target.js @@ -0,0 +1,26 @@ +test("basic functionality", () => { + function foo() { + return new.target; + } + expect(foo()).toBeUndefined(); + expect(new foo()).toEqual(foo); + + function bar() { + const baz = () => new.target; + return baz(); + } + expect(bar()).toBeUndefined(); + expect(new bar()).toEqual(bar); + + class baz { + constructor() { + this.newTarget = new.target; + } + } + expect(new baz().newTarget).toEqual(baz); +}); + +// FIXME: This does not work as expected as toEval() places the code inside a function :| +test.skip("syntax error outside of function", () => { + expect("new.target").not.toEval(); +});