diff options
author | Matthew Olsson <matthewcolsson@gmail.com> | 2020-05-02 11:46:39 -0700 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-05-03 00:44:57 +0200 |
commit | 5e66f1900b1576f8645bb52d3ac6468ea69e7e25 (patch) | |
tree | 81195c81486f5e82fdf3d404d7a2f372edcb0f6b /Libraries | |
parent | 751f813f6a75befa324d1a2ad7b89f4d27081c05 (diff) | |
download | serenity-5e66f1900b1576f8645bb52d3ac6468ea69e7e25.zip |
LibJS: Add function default arguments
Adds the ability for function arguments to have default values. This
works for standard functions as well as arrow functions. Default values
are not printed in a <function>.toString() call, as nodes cannot print
their source string representation.
Diffstat (limited to 'Libraries')
-rw-r--r-- | Libraries/LibJS/AST.cpp | 22 | ||||
-rw-r--r-- | Libraries/LibJS/AST.h | 15 | ||||
-rw-r--r-- | Libraries/LibJS/Parser.cpp | 29 | ||||
-rw-r--r-- | Libraries/LibJS/Parser.h | 2 | ||||
-rw-r--r-- | Libraries/LibJS/Runtime/FunctionPrototype.cpp | 13 | ||||
-rw-r--r-- | Libraries/LibJS/Runtime/ScriptFunction.cpp | 17 | ||||
-rw-r--r-- | Libraries/LibJS/Runtime/ScriptFunction.h | 9 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/function-default-parameters.js | 112 |
8 files changed, 184 insertions, 35 deletions
diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index ab91564887..6afdc51961 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -670,17 +670,27 @@ void NullLiteral::dump(int indent) const void FunctionNode::dump(int indent, const char* class_name) const { - StringBuilder parameters_builder; - parameters_builder.join(',', parameters()); - print_indent(indent); - printf("%s '%s(%s)'\n", class_name, name().characters(), parameters_builder.build().characters()); + printf("%s '%s'\n", class_name, name().characters()); + if (!m_parameters.is_empty()) { + print_indent(indent + 1); + printf("(Parameters)\n"); + + for (auto& parameter : m_parameters) { + print_indent(indent + 2); + printf("%s\n", parameter.name.characters()); + if (parameter.default_value) { + parameter.default_value->dump(indent + 3); + } + } + } if (!m_variables.is_empty()) { print_indent(indent + 1); printf("(Variables)\n"); + + for (auto& variable : m_variables) + variable.dump(indent + 2); } - for (auto& variable : m_variables) - variable.dump(indent + 2); print_indent(indent + 1); printf("(Body)\n"); body().dump(indent + 2); diff --git a/Libraries/LibJS/AST.h b/Libraries/LibJS/AST.h index cfb74594d8..c41c0050d6 100644 --- a/Libraries/LibJS/AST.h +++ b/Libraries/LibJS/AST.h @@ -149,12 +149,17 @@ class Declaration : public Statement { class FunctionNode { public: + struct Parameter { + FlyString name; + RefPtr<Expression> default_value; + }; + const FlyString& name() const { return m_name; } const Statement& body() const { return *m_body; } - const Vector<FlyString>& parameters() const { return m_parameters; }; + const Vector<Parameter>& parameters() const { return m_parameters; }; protected: - FunctionNode(const FlyString& name, NonnullRefPtr<Statement> body, Vector<FlyString> parameters, NonnullRefPtrVector<VariableDeclaration> variables) + FunctionNode(const FlyString& name, NonnullRefPtr<Statement> body, Vector<Parameter> parameters, NonnullRefPtrVector<VariableDeclaration> variables) : m_name(name) , m_body(move(body)) , m_parameters(move(parameters)) @@ -169,7 +174,7 @@ protected: private: FlyString m_name; NonnullRefPtr<Statement> m_body; - const Vector<FlyString> m_parameters; + const Vector<Parameter> m_parameters; NonnullRefPtrVector<VariableDeclaration> m_variables; }; @@ -179,7 +184,7 @@ class FunctionDeclaration final public: static bool must_have_name() { return true; } - FunctionDeclaration(const FlyString& name, NonnullRefPtr<Statement> body, Vector<FlyString> parameters, NonnullRefPtrVector<VariableDeclaration> variables) + FunctionDeclaration(const FlyString& name, NonnullRefPtr<Statement> body, Vector<Parameter> parameters, NonnullRefPtrVector<VariableDeclaration> variables) : FunctionNode(name, move(body), move(parameters), move(variables)) { } @@ -196,7 +201,7 @@ class FunctionExpression final : public Expression public: static bool must_have_name() { return false; } - FunctionExpression(const FlyString& name, NonnullRefPtr<Statement> body, Vector<FlyString> parameters, NonnullRefPtrVector<VariableDeclaration> variables) + FunctionExpression(const FlyString& name, NonnullRefPtr<Statement> body, Vector<Parameter> parameters, NonnullRefPtrVector<VariableDeclaration> variables) : FunctionNode(name, move(body), move(parameters), move(variables)) { } diff --git a/Libraries/LibJS/Parser.cpp b/Libraries/LibJS/Parser.cpp index 05d2c81bb7..c73b267a1a 100644 --- a/Libraries/LibJS/Parser.cpp +++ b/Libraries/LibJS/Parser.cpp @@ -279,14 +279,19 @@ RefPtr<FunctionExpression> Parser::try_parse_arrow_function_expression(bool expe load_state(); }; - Vector<FlyString> parameters; + Vector<FunctionNode::Parameter> parameters; bool parse_failed = false; while (true) { if (match(TokenType::Comma)) { consume(TokenType::Comma); } else if (match(TokenType::Identifier)) { - auto token = consume(TokenType::Identifier); - parameters.append(token.value()); + auto parameter_name = consume(TokenType::Identifier).value(); + RefPtr<Expression> default_value; + if (match(TokenType::Equals)) { + consume(TokenType::Equals); + default_value = parse_expression(0); + } + parameters.append({ parameter_name, default_value }); } else if (match(TokenType::ParenClose)) { if (expect_parens) { consume(TokenType::ParenClose); @@ -775,10 +780,15 @@ NonnullRefPtr<FunctionNodeType> Parser::parse_function_node(bool needs_function_ name = consume(TokenType::Identifier).value(); } consume(TokenType::ParenOpen); - Vector<FlyString> parameters; + Vector<FunctionNode::Parameter> parameters; while (match(TokenType::Identifier)) { - auto parameter = consume(TokenType::Identifier).value(); - parameters.append(parameter); + auto parameter_name = consume(TokenType::Identifier).value(); + RefPtr<Expression> default_value; + if (match(TokenType::Equals)) { + consume(TokenType::Equals); + default_value = parse_expression(0); + } + parameters.append({ parameter_name, default_value }); if (match(TokenType::ParenClose)) { break; } @@ -1233,14 +1243,13 @@ void Parser::syntax_error(const String& message, size_t line, size_t column) void Parser::save_state() { - m_saved_state = m_parser_state; + m_saved_state.append(m_parser_state); } void Parser::load_state() { - ASSERT(m_saved_state.has_value()); - m_parser_state = m_saved_state.value(); - m_saved_state.clear(); + ASSERT(!m_saved_state.is_empty()); + m_parser_state = m_saved_state.take_last(); } } diff --git a/Libraries/LibJS/Parser.h b/Libraries/LibJS/Parser.h index 248005defa..e11429399b 100644 --- a/Libraries/LibJS/Parser.h +++ b/Libraries/LibJS/Parser.h @@ -108,6 +108,6 @@ private: }; ParserState m_parser_state; - Optional<ParserState> m_saved_state; + Vector<ParserState> m_saved_state; }; } diff --git a/Libraries/LibJS/Runtime/FunctionPrototype.cpp b/Libraries/LibJS/Runtime/FunctionPrototype.cpp index 7be81921c5..d6f5d3a5fa 100644 --- a/Libraries/LibJS/Runtime/FunctionPrototype.cpp +++ b/Libraries/LibJS/Runtime/FunctionPrototype.cpp @@ -136,9 +136,18 @@ Value FunctionPrototype::to_string(Interpreter& interpreter) if (this_object->is_native_function() || this_object->is_bound_function()) { function_body = String::format(" [%s]", this_object->class_name()); } else { - auto& parameters = static_cast<ScriptFunction*>(this_object)->parameters(); StringBuilder parameters_builder; - parameters_builder.join(", ", parameters); + auto first = true; + for (auto& parameter : static_cast<ScriptFunction*>(this_object)->parameters()) { + if (!first) + parameters_builder.append(", "); + first = false; + parameters_builder.append(parameter.name); + if (parameter.default_value) { + // FIXME: See note below + parameters_builder.append(" = TODO"); + } + } function_parameters = parameters_builder.build(); // FIXME: ASTNodes should be able to dump themselves to source strings - something like this: // auto& body = static_cast<ScriptFunction*>(this_object)->body(); diff --git a/Libraries/LibJS/Runtime/ScriptFunction.cpp b/Libraries/LibJS/Runtime/ScriptFunction.cpp index 042ad99872..f138f3b71d 100644 --- a/Libraries/LibJS/Runtime/ScriptFunction.cpp +++ b/Libraries/LibJS/Runtime/ScriptFunction.cpp @@ -46,12 +46,12 @@ static ScriptFunction* script_function_from(Interpreter& interpreter) return static_cast<ScriptFunction*>(this_object); } -ScriptFunction* ScriptFunction::create(GlobalObject& global_object, const FlyString& name, const Statement& body, Vector<FlyString> parameters, LexicalEnvironment* parent_environment) +ScriptFunction* ScriptFunction::create(GlobalObject& global_object, const FlyString& name, const Statement& body, Vector<FunctionNode::Parameter> parameters, LexicalEnvironment* parent_environment) { return global_object.heap().allocate<ScriptFunction>(name, body, move(parameters), parent_environment, *global_object.function_prototype()); } -ScriptFunction::ScriptFunction(const FlyString& name, const Statement& body, Vector<FlyString> parameters, LexicalEnvironment* parent_environment, Object& prototype) +ScriptFunction::ScriptFunction(const FlyString& name, const Statement& body, Vector<FunctionNode::Parameter> parameters, LexicalEnvironment* parent_environment, Object& prototype) : Function(prototype) , m_name(name) , m_body(body) @@ -77,7 +77,7 @@ LexicalEnvironment* ScriptFunction::create_environment() { HashMap<FlyString, Variable> variables; for (auto& parameter : m_parameters) { - variables.set(parameter, { js_undefined(), DeclarationKind::Var }); + variables.set(parameter.name, { js_undefined(), DeclarationKind::Var }); } if (body().is_scope_node()) { @@ -97,12 +97,15 @@ Value ScriptFunction::call(Interpreter& interpreter) auto& argument_values = interpreter.call_frame().arguments; ArgumentVector arguments; for (size_t i = 0; i < m_parameters.size(); ++i) { - auto name = parameters()[i]; + auto parameter = parameters()[i]; auto value = js_undefined(); - if (i < argument_values.size()) + if (i < argument_values.size() && !argument_values[i].is_undefined()) { value = argument_values[i]; - arguments.append({ name, value }); - interpreter.current_environment()->set(name, { value, DeclarationKind::Var }); + } else if (parameter.default_value) { + value = parameter.default_value->execute(interpreter); + } + arguments.append({ parameter.name, value }); + interpreter.current_environment()->set(parameter.name, { value, DeclarationKind::Var }); } return interpreter.run(m_body, arguments, ScopeType::Function); } diff --git a/Libraries/LibJS/Runtime/ScriptFunction.h b/Libraries/LibJS/Runtime/ScriptFunction.h index 23ccb3c323..24338972d5 100644 --- a/Libraries/LibJS/Runtime/ScriptFunction.h +++ b/Libraries/LibJS/Runtime/ScriptFunction.h @@ -26,19 +26,20 @@ #pragma once +#include <LibJS/AST.h> #include <LibJS/Runtime/Function.h> namespace JS { class ScriptFunction final : public Function { public: - static ScriptFunction* create(GlobalObject&, const FlyString& name, const Statement& body, Vector<FlyString> parameters, LexicalEnvironment* parent_environment); + static ScriptFunction* create(GlobalObject&, const FlyString& name, const Statement& body, Vector<FunctionNode::Parameter> parameters, LexicalEnvironment* parent_environment); - ScriptFunction(const FlyString& name, const Statement& body, Vector<FlyString> parameters, LexicalEnvironment* parent_environment, Object& prototype); + ScriptFunction(const FlyString& name, const Statement& body, Vector<FunctionNode::Parameter> parameters, LexicalEnvironment* parent_environment, Object& prototype); virtual ~ScriptFunction(); const Statement& body() const { return m_body; } - const Vector<FlyString>& parameters() const { return m_parameters; }; + const Vector<FunctionNode::Parameter>& parameters() const { return m_parameters; }; virtual Value call(Interpreter&) override; virtual Value construct(Interpreter&) override; @@ -56,7 +57,7 @@ private: FlyString m_name; NonnullRefPtr<Statement> m_body; - const Vector<FlyString> m_parameters; + const Vector<FunctionNode::Parameter> m_parameters; LexicalEnvironment* m_parent_environment { nullptr }; }; diff --git a/Libraries/LibJS/Tests/function-default-parameters.js b/Libraries/LibJS/Tests/function-default-parameters.js new file mode 100644 index 0000000000..ab537928b0 --- /dev/null +++ b/Libraries/LibJS/Tests/function-default-parameters.js @@ -0,0 +1,112 @@ +load("test-common.js"); + +try { + function func1(a, b = 1) { + return a + b; + } + + const arrowFunc1 = (a, b = 1) => a + b; + + assert(func1(4, 5) === 9); + assert(func1(4) === 5); + assert(func1(4, undefined) === 5); + assert(Number.isNaN(func1())); + + assert(arrowFunc1(4, 5) === 9); + assert(arrowFunc1(4) === 5); + assert(arrowFunc1(4, undefined) === 5); + assert(Number.isNaN(arrowFunc1())); + + function func2(a = 6) { + return typeof a; + } + + const arrowFunc2 = (a = 6) => typeof a; + + assert(func2() === "number"); + assert(func2(5) === "number"); + assert(func2(undefined) === "number"); + assert(func2(false) === "boolean"); + assert(func2(null) === "object"); + assert(func2({}) === "object"); + + assert(arrowFunc2() === "number"); + assert(arrowFunc2(5) === "number"); + assert(arrowFunc2(undefined) === "number"); + assert(arrowFunc2(false) === "boolean"); + assert(arrowFunc2(null) === "object"); + assert(arrowFunc2({}) === "object"); + + function func3(a = 5, b) { + return a + b; + } + + const arrowFunc3 = (a = 5, b) => a + b; + + assert(func3(4, 5) === 9); + assert(func3(undefined, 4) === 9); + assert(Number.isNaN(func3())); + + assert(arrowFunc3(4, 5) === 9); + assert(arrowFunc3(undefined, 4) === 9); + assert(Number.isNaN(arrowFunc3())); + + function func4(a, b = a) { + return a + b; + } + + const arrowFunc4 = (a, b = a) => a + b; + + assert(func4(4, 5) === 9); + assert(func4(4) === 8); + assert(func4("hf") === "hfhf"); + assert(func4(true) === 2); + assert(Number.isNaN(func4())); + + assert(arrowFunc4(4, 5) === 9); + assert(arrowFunc4(4) === 8); + assert(arrowFunc4("hf") === "hfhf"); + assert(arrowFunc4(true) === 2); + assert(Number.isNaN(arrowFunc4())); + + function func5(a = function() { return 5; }) { + return a(); + } + + const arrowFunc5 = (a = function() { return 5; }) => a(); + + assert(func5() === 5); + assert(func5(function() { return 10; }) === 10); + assert(func5(() => 10) === 10); + assert(arrowFunc5() === 5); + assert(arrowFunc5(function() { return 10; }) === 10); + assert(arrowFunc5(() => 10) === 10); + + function func6(a = () => 5) { + return a(); + } + + const arrowFunc6 = (a = () => 5) => a(); + + assert(func6() === 5); + assert(func6(function() { return 10; }) === 10); + assert(func6(() => 10) === 10); + assert(arrowFunc6() === 5); + assert(arrowFunc6(function() { return 10; }) === 10); + assert(arrowFunc6(() => 10) === 10); + + function func7(a = { foo: "bar" }) { + return a.foo; + } + + const arrowFunc7 = (a = { foo: "bar" }) => a.foo + + assert(func7() === "bar"); + assert(func7({ foo: "baz" }) === "baz"); + assert(arrowFunc7() === "bar"); + assert(arrowFunc7({ foo: "baz" }) === "baz"); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} |