diff options
author | davidot <david.tuin@gmail.com> | 2021-08-28 17:11:05 +0200 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-09-01 13:39:14 +0100 |
commit | def8b44c4083f2d79bdb7b44701d6a353cb52394 (patch) | |
tree | aad665b8885c15f53acd6aa35048d17b867bfcb9 | |
parent | 3b6a8d1d53e4d530ff827e2ab3e61388ff62cb8b (diff) | |
download | serenity-def8b44c4083f2d79bdb7b44701d6a353cb52394.zip |
LibJS: Add support for public fields in classes
-rw-r--r-- | Userland/Libraries/LibJS/AST.cpp | 67 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/AST.h | 28 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Parser.cpp | 41 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Parser.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/FunctionObject.cpp | 20 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/FunctionObject.h | 12 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/VM.cpp | 18 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/VM.h | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/classes/class-public-fields.js | 87 |
9 files changed, 265 insertions, 11 deletions
diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index 78d1694d98..8cc535d1fd 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -310,7 +310,8 @@ Value SuperCall::execute(Interpreter& interpreter, GlobalObject& global_object) [[maybe_unused]] auto& f = this_er.function_object(); // 11. Perform ? InitializeInstanceElements(result, F). - // FIXME: This is missing here. + VERIFY(result.is_object()); + vm.initialize_instance_elements(result.as_object(), f); // 12. Return result. return result; @@ -867,6 +868,12 @@ Value ClassMethod::execute(Interpreter& interpreter, GlobalObject& global_object return m_function->execute(interpreter, global_object); } +Value ClassField::execute(Interpreter& interpreter, GlobalObject&) const +{ + InterpreterNodeScope node_scope { interpreter, *this }; + return {}; +} + Value ClassExpression::execute(Interpreter& interpreter, GlobalObject& global_object) const { InterpreterNodeScope node_scope { interpreter, *this }; @@ -923,7 +930,7 @@ Value ClassExpression::execute(Interpreter& interpreter, GlobalObject& global_ob interpreter.vm().throw_exception<TypeError>(global_object, ErrorType::NotAnObject, "Class prototype"); return {}; } - for (const auto& method : m_methods) { + for (auto const& method : m_methods) { auto method_value = method.execute(interpreter, global_object); if (interpreter.exception()) return {}; @@ -960,6 +967,40 @@ Value ClassExpression::execute(Interpreter& interpreter, GlobalObject& global_ob return {}; } + for (auto& field : m_fields) { + auto key = field.key().execute(interpreter, global_object); + if (interpreter.exception()) + return {}; + + auto property_key = key.to_property_key(global_object); + if (interpreter.exception()) + return {}; + + FunctionObject* initializer = nullptr; + if (field.initializer()) { + auto copy_initializer = field.initializer(); + auto body = create_ast_node<ExpressionStatement>(field.initializer()->source_range(), copy_initializer.release_nonnull()); + // FIXME: A potential optimization is not creating the functions here since these are never directly accessible. + initializer = OrdinaryFunctionObject::create(interpreter.global_object(), property_key.to_display_string(), *body, {}, 0, interpreter.lexical_environment(), FunctionKind::Regular, false); + initializer->set_home_object(field.is_static() ? class_constructor : &class_prototype.as_object()); + } + + if (field.is_static()) { + Value field_value = js_undefined(); + if (initializer) { + field_value = interpreter.vm().call(*initializer, class_constructor_value); + if (interpreter.exception()) + return {}; + } + + class_constructor->create_data_property_or_throw(property_key, field_value); + if (interpreter.exception()) + return {}; + } else { + class_constructor->add_field(property_key, initializer); + } + } + return class_constructor; } @@ -1186,6 +1227,11 @@ void ClassExpression::dump(int indent) const outln("(Methods)"); for (auto& method : m_methods) method.dump(indent + 1); + + print_indent(indent); + outln("(Fields)"); + for (auto& field : m_fields) + field.dump(indent + 1); } void ClassMethod::dump(int indent) const @@ -1219,6 +1265,23 @@ void ClassMethod::dump(int indent) const m_function->dump(indent + 1); } +void ClassField::dump(int indent) const +{ + ASTNode::dump(indent); + print_indent(indent); + outln("(Key)"); + m_key->dump(indent + 1); + + print_indent(indent); + outln("Static: {}", m_is_static); + + if (m_initializer) { + print_indent(indent); + outln("(Initializer)"); + m_initializer->dump(indent + 1); + } +} + void StringLiteral::dump(int indent) const { print_indent(indent); diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 838450e8a4..b400640bce 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -933,6 +933,30 @@ private: bool m_is_static; }; +class ClassField final : public ASTNode { +public: + ClassField(SourceRange source_range, NonnullRefPtr<Expression> key, RefPtr<Expression> init, bool is_static) + : ASTNode(source_range) + , m_key(move(key)) + , m_initializer(move(init)) + , m_is_static(is_static) + { + } + + Expression const& key() const { return *m_key; } + bool is_static() const { return m_is_static; } + RefPtr<Expression> const& initializer() const { return m_initializer; } + RefPtr<Expression>& initializer() { return m_initializer; } + + virtual Value execute(Interpreter&, GlobalObject&) const override; + virtual void dump(int indent) const override; + +private: + NonnullRefPtr<Expression> m_key; + RefPtr<Expression> m_initializer; + bool m_is_static; +}; + class SuperExpression final : public Expression { public: explicit SuperExpression(SourceRange source_range) @@ -948,12 +972,13 @@ public: class ClassExpression final : public Expression { public: - ClassExpression(SourceRange source_range, String name, RefPtr<FunctionExpression> constructor, RefPtr<Expression> super_class, NonnullRefPtrVector<ClassMethod> methods) + ClassExpression(SourceRange source_range, String name, RefPtr<FunctionExpression> constructor, RefPtr<Expression> super_class, NonnullRefPtrVector<ClassMethod> methods, NonnullRefPtrVector<ClassField> fields) : Expression(source_range) , m_name(move(name)) , m_constructor(move(constructor)) , m_super_class(move(super_class)) , m_methods(move(methods)) + , m_fields(move(fields)) { } @@ -968,6 +993,7 @@ private: RefPtr<FunctionExpression> m_constructor; RefPtr<Expression> m_super_class; NonnullRefPtrVector<ClassMethod> m_methods; + NonnullRefPtrVector<ClassField> m_fields; }; class ClassDeclaration final : public Declaration { diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index 8dfe614e6a..d1528e64f9 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -643,6 +643,7 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ consume(TokenType::Class); NonnullRefPtrVector<ClassMethod> methods; + NonnullRefPtrVector<ClassField> fields; RefPtr<Expression> super_class; RefPtr<FunctionExpression> constructor; @@ -693,8 +694,8 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ is_generator = true; } + StringView name; if (match_property_key()) { - StringView name; if (!is_generator && m_state.current_token.original_value() == "static"sv) { if (match(TokenType::Identifier)) { consume(); @@ -707,12 +708,12 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ } if (match(TokenType::Identifier)) { - auto identifier_name = m_state.current_token.value(); + auto identifier_name = m_state.current_token.original_value(); - if (identifier_name == "get") { + if (identifier_name == "get"sv) { method_kind = ClassMethod::Kind::Getter; consume(); - } else if (identifier_name == "set") { + } else if (identifier_name == "set"sv) { method_kind = ClassMethod::Kind::Setter; consume(); } @@ -740,7 +741,7 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ // It is a Syntax Error if PropName of MethodDefinition is "prototype". if (is_static && name == "prototype"sv) syntax_error("Classes may not have a static property named 'prototype'"); - } else if (match(TokenType::ParenOpen) && (is_static || method_kind != ClassMethod::Kind::Method)) { + } else if ((match(TokenType::ParenOpen) || match(TokenType::Equals)) && (is_static || method_kind != ClassMethod::Kind::Method)) { switch (method_kind) { case ClassMethod::Kind::Method: VERIFY(is_static); @@ -762,7 +763,7 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ } // Constructor may be a StringLiteral or an Identifier. - if (!is_static && name == "constructor") { + if (!is_static && name == "constructor"sv) { if (method_kind != ClassMethod::Kind::Method) syntax_error("Class constructor may not be an accessor"); if (!constructor.is_null()) @@ -792,9 +793,29 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ } else { syntax_error("No key for class method"); } - } else { + } else if (is_generator) { expected("ParenOpen"); consume(); + } else if (property_key.is_null()) { + expected("property key"); + consume(); + } else { + if (name == "constructor"sv) + syntax_error("Class cannot have field named 'constructor'"); + + RefPtr<Expression> initializer; + + if (match(TokenType::Equals)) { + consume(); + + TemporaryChange super_property_access_rollback(m_state.allow_super_property_lookup, true); + TemporaryChange field_initializer_rollback(m_state.in_class_field_initializer, true); + initializer = parse_expression(2); + } + + fields.append(create_ast_node<ClassField>({ m_state.current_token.filename(), rule_start.position(), position() }, property_key.release_nonnull(), move(initializer), is_static)); + + consume_or_insert_semicolon(); } } @@ -821,7 +842,7 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_ } } - return create_ast_node<ClassExpression>({ m_state.current_token.filename(), rule_start.position(), position() }, move(class_name), move(constructor), move(super_class), move(methods)); + return create_ast_node<ClassExpression>({ m_state.current_token.filename(), rule_start.position(), position() }, move(class_name), move(constructor), move(super_class), move(methods), move(fields)); } Parser::PrimaryExpressionParseResult Parser::parse_primary_expression() @@ -1535,6 +1556,7 @@ NonnullRefPtr<AssignmentExpression> Parser::parse_assignment_expression(Assignme parser.m_state.in_break_context = m_state.in_break_context; parser.m_state.in_continue_context = m_state.in_continue_context; parser.m_state.string_legacy_octal_escape_sequence_in_scope = m_state.string_legacy_octal_escape_sequence_in_scope; + parser.m_state.in_class_field_initializer = m_state.in_class_field_initializer; auto result = parser.parse_binding_pattern(); if (parser.has_errors()) @@ -1577,6 +1599,8 @@ NonnullRefPtr<Identifier> Parser::parse_identifier() { auto identifier_start = position(); auto token = consume_identifier(); + if (m_state.in_class_field_initializer && token.value() == "arguments"sv) + syntax_error("'arguments' is not allowed in class field initializer"); return create_ast_node<Identifier>( { m_state.current_token.filename(), identifier_start, position() }, token.value()); @@ -1757,6 +1781,7 @@ NonnullRefPtr<FunctionNodeType> Parser::parse_function_node(u8 parse_options) TemporaryChange super_constructor_call_rollback(m_state.allow_super_constructor_call, !!(parse_options & FunctionNodeParseOptions::AllowSuperConstructorCall)); TemporaryChange break_context_rollback(m_state.in_break_context, false); TemporaryChange continue_context_rollback(m_state.in_continue_context, false); + TemporaryChange class_field_initializer_rollback(m_state.in_class_field_initializer, false); ScopePusher scope(*this, ScopePusher::Var, Parser::Scope::Function); diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index 12cf4d089e..41b6cef2c8 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -258,6 +258,7 @@ private: bool in_break_context { false }; bool in_continue_context { false }; bool string_legacy_octal_escape_sequence_in_scope { false }; + bool in_class_field_initializer { false }; ParserState(Lexer, Program::Type); }; diff --git a/Userland/Libraries/LibJS/Runtime/FunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/FunctionObject.cpp index bf4e02b0f6..da1f410294 100644 --- a/Userland/Libraries/LibJS/Runtime/FunctionObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/FunctionObject.cpp @@ -66,6 +66,23 @@ BoundFunction* FunctionObject::bind(Value bound_this_value, Vector<Value> argume return heap().allocate<BoundFunction>(global_object(), global_object(), target_function, bound_this_object, move(all_bound_arguments), computed_length, constructor_prototype); } +void FunctionObject::add_field(StringOrSymbol property_key, FunctionObject* initializer) +{ + m_fields.empend(property_key, initializer); +} + +// 7.3.31 DefineField ( receiver, fieldRecord ), https://tc39.es/ecma262/#sec-definefield +void FunctionObject::InstanceField::define_field(VM& vm, Object& receiver) const +{ + Value init_value = js_undefined(); + if (initializer) { + init_value = vm.call(*initializer, receiver.value_of()); + if (vm.exception()) + return; + } + receiver.create_data_property_or_throw(name, init_value); +} + void FunctionObject::visit_edges(Visitor& visitor) { Object::visit_edges(visitor); @@ -75,6 +92,9 @@ void FunctionObject::visit_edges(Visitor& visitor) for (auto argument : m_bound_arguments) visitor.visit(argument); + + for (auto& field : m_fields) + visitor.visit(field.initializer); } } diff --git a/Userland/Libraries/LibJS/Runtime/FunctionObject.h b/Userland/Libraries/LibJS/Runtime/FunctionObject.h index 5b089877b1..3cfa762166 100644 --- a/Userland/Libraries/LibJS/Runtime/FunctionObject.h +++ b/Userland/Libraries/LibJS/Runtime/FunctionObject.h @@ -63,6 +63,17 @@ public: // This is for IsSimpleParameterList (static semantics) bool has_simple_parameter_list() const { return m_has_simple_parameter_list; } + // [[Fields]] + struct InstanceField { + StringOrSymbol name; + FunctionObject* initializer { nullptr }; + + void define_field(VM& vm, Object& receiver) const; + }; + + Vector<InstanceField> const& fields() const { return m_fields; } + void add_field(StringOrSymbol property_key, FunctionObject* initializer); + protected: virtual void visit_edges(Visitor&) override; @@ -79,6 +90,7 @@ private: ConstructorKind m_constructor_kind = ConstructorKind::Base; ThisMode m_this_mode { ThisMode::Global }; bool m_has_simple_parameter_list { false }; + Vector<InstanceField> m_fields; }; } diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index f8ebdab071..a0e0acceb8 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -457,6 +457,16 @@ static void append_bound_and_passed_arguments(MarkedValueList& arguments, Vector } } +// 7.3.32 InitializeInstanceElements ( O, constructor ), https://tc39.es/ecma262/#sec-initializeinstanceelements +void VM::initialize_instance_elements(Object& object, FunctionObject& constructor) +{ + for (auto& field : constructor.fields()) { + field.define_field(*this, object); + if (exception()) + return; + } +} + Value VM::construct(FunctionObject& function, FunctionObject& new_target, Optional<MarkedValueList> arguments) { auto& global_object = function.global_object(); @@ -494,6 +504,14 @@ Value VM::construct(FunctionObject& function, FunctionObject& new_target, Option // If we are a Derived constructor, |this| has not been constructed before super is called. callee_context.this_value = this_argument; + + if (function.constructor_kind() == FunctionObject::ConstructorKind::Base) { + VERIFY(this_argument.is_object()); + initialize_instance_elements(this_argument.as_object(), function); + if (exception()) + return {}; + } + auto result = function.construct(new_target); pop_execution_context(); diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index 7b2981c686..4b25bb3e3b 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -267,6 +267,8 @@ public: Function<void(const Promise&)> on_promise_unhandled_rejection; Function<void(const Promise&)> on_promise_rejection_handled; + void initialize_instance_elements(Object& object, FunctionObject& constructor); + private: VM(); diff --git a/Userland/Libraries/LibJS/Tests/classes/class-public-fields.js b/Userland/Libraries/LibJS/Tests/classes/class-public-fields.js new file mode 100644 index 0000000000..86c1e86de7 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/classes/class-public-fields.js @@ -0,0 +1,87 @@ +test("basic functionality", () => { + class A { + number = 3; + + string = "foo"; + + uninitialized; + } + + const a = new A(); + expect(a.number).toBe(3); + expect(a.string).toBe("foo"); + expect(a.uninitialized).toBeUndefined(); +}); + +test("extended name syntax", () => { + class A { + "field with space" = 1; + + 12 = "twelve"; + + [`he${"llo"}`] = 3; + } + + const a = new A(); + expect(a["field with space"]).toBe(1); + expect(a[12]).toBe("twelve"); + expect(a.hello).toBe(3); +}); + +test("initializer has correct this value", () => { + class A { + this_val = this; + + this_name = this.this_val; + } + + const a = new A(); + expect(a.this_val).toBe(a); + expect(a.this_name).toBe(a); +}); + +test("static fields", () => { + class A { + static simple = 1; + simple = 2; + + static "with space" = 3; + + static 24 = "two dozen"; + + static [`he${"llo"}`] = "friends"; + + static this_val = this; + static this_name = this.name; + static this_val2 = this.this_val; + } + + expect(A.simple).toBe(1); + expect(A["with space"]).toBe(3); + expect(A[24]).toBe("two dozen"); + expect(A.hello).toBe("friends"); + + expect(A.this_val).toBe(A); + expect(A.this_name).toBe("A"); + expect(A.this_val2).toBe(A); + + const a = new A(); + expect(a.simple).toBe(2); +}); + +test("with super class", () => { + class A { + super_field = 3; + } + + class B extends A { + references_super_field = super.super_field; + arrow_ref_super = () => (super.super_field = 4); + } + + const b = new B(); + expect(b.super_field).toBe(3); + expect(b.references_super_field).toBeUndefined(); + b.arrow_ref_super(); + expect(b.super_field).toBe(4); +}); |