From ed80952cb6e2b32e2541198134ffc98d282a559d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 15 Apr 2020 21:58:22 +0200 Subject: [PATCH] LibJS: Introduce LexicalEnvironment This patch replaces the old variable lookup logic with a new one based on lexical environments. This brings us closer to the way JavaScript is actually specced, and also gives us some basic support for closures. The interpreter's call stack frames now have a pointer to the lexical environment for that frame. Each lexical environment can have a chain of parent environments. Before calling a Function, we first ask it to create_environment(). This gives us a new LexicalEnvironment for that function, which has the function's lexical parent's environment as its parent. This allows inner functions to access variables in their outer function: function foo() { <-- LexicalEnvironment A var x = 1; function() { <-- LexicalEnvironment B (parent: A) console.log(x); } } If we return the result of a function expression from a function, that new function object will keep a reference to its parent environment, which is how we get closures. :^) I'm pretty sure I didn't get everything right here, but it's a pretty good start. This is quite a bit slower than before, but also correcter! --- Libraries/LibJS/AST.cpp | 9 +-- Libraries/LibJS/Forward.h | 1 + Libraries/LibJS/Interpreter.cpp | 62 ++++++++++++------ Libraries/LibJS/Interpreter.h | 19 +++--- Libraries/LibJS/Makefile | 1 + Libraries/LibJS/Runtime/Function.h | 1 + .../LibJS/Runtime/LexicalEnvironment.cpp | 64 +++++++++++++++++++ Libraries/LibJS/Runtime/LexicalEnvironment.h | 64 +++++++++++++++++++ Libraries/LibJS/Runtime/NativeFunction.h | 1 + Libraries/LibJS/Runtime/ScriptFunction.cpp | 35 +++++++++- Libraries/LibJS/Runtime/ScriptFunction.h | 5 +- 11 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 Libraries/LibJS/Runtime/LexicalEnvironment.cpp create mode 100644 Libraries/LibJS/Runtime/LexicalEnvironment.h diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index f6a2910bb2..ada01e8917 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -48,14 +48,14 @@ Value ScopeNode::execute(Interpreter& interpreter) const Value FunctionDeclaration::execute(Interpreter& interpreter) const { - auto* function = interpreter.heap().allocate(name(), body(), parameters()); + auto* function = interpreter.heap().allocate(name(), body(), parameters(), interpreter.current_environment()); interpreter.set_variable(name(), function); return js_undefined(); } Value FunctionExpression::execute(Interpreter& interpreter) const { - return interpreter.heap().allocate(name(), body(), parameters()); + return interpreter.heap().allocate(name(), body(), parameters(), interpreter.current_environment()); } Value ExpressionStatement::execute(Interpreter& interpreter) const @@ -119,6 +119,7 @@ Value CallExpression::execute(Interpreter& interpreter) const auto& call_frame = interpreter.push_call_frame(); call_frame.function_name = function.name(); call_frame.arguments = move(arguments); + call_frame.environment = function.create_environment(); Object* new_object = nullptr; Value result; @@ -134,11 +135,11 @@ Value CallExpression::execute(Interpreter& interpreter) const result = function.call(interpreter); } + interpreter.pop_call_frame(); + if (interpreter.exception()) return {}; - interpreter.pop_call_frame(); - if (is_new_expression()) { if (result.is_object()) return result; diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 6de9b6ce1f..2330542171 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -61,6 +61,7 @@ class HandleImpl; class Heap; class HeapBlock; class Interpreter; +class LexicalEnvironment; class PrimitiveString; class ScopeNode; class Shape; diff --git a/Libraries/LibJS/Interpreter.cpp b/Libraries/LibJS/Interpreter.cpp index 22f8489d87..4dbd06bdf4 100644 --- a/Libraries/LibJS/Interpreter.cpp +++ b/Libraries/LibJS/Interpreter.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,16 @@ Interpreter::~Interpreter() Value Interpreter::run(const Statement& statement, ArgumentVector arguments, ScopeType scope_type) { + if (statement.is_program()) { + if (m_call_stack.is_empty()) { + CallFrame global_call_fram; + global_call_fram.this_value = m_global_object; + global_call_fram.function_name = "(global execution context)"; + global_call_fram.environment = heap().allocate(); + m_call_stack.append(move(global_call_fram)); + } + } + if (!statement.is_scope_node()) return statement.execute(*this); @@ -91,6 +102,11 @@ Value Interpreter::run(const Statement& statement, ArgumentVector arguments, Sco void Interpreter::enter_scope(const ScopeNode& scope_node, ArgumentVector arguments, ScopeType scope_type) { + if (scope_type == ScopeType::Function) { + m_scope_stack.append({ scope_type, scope_node, false }); + return; + } + HashMap scope_variables_with_declaration_kind; scope_variables_with_declaration_kind.ensure_capacity(16); @@ -107,13 +123,30 @@ void Interpreter::enter_scope(const ScopeNode& scope_node, ArgumentVector argume scope_variables_with_declaration_kind.set(argument.name, { argument.value, DeclarationKind::Var }); } - m_scope_stack.append({ scope_type, scope_node, move(scope_variables_with_declaration_kind) }); + bool pushed_lexical_environment = false; + + if (scope_type != ScopeType::Function) { + // only a block, but maybe it has block-scoped variables! + if (!scope_variables_with_declaration_kind.is_empty()) { + auto* block_lexical_environment = heap().allocate(move(scope_variables_with_declaration_kind), current_environment()); + m_call_stack.last().environment = block_lexical_environment; + pushed_lexical_environment = true; + } + } else if (scope_type == ScopeType::Function) { + for (auto& it : scope_variables_with_declaration_kind) { + current_environment()->set(it.key, it.value); + } + } + + m_scope_stack.append({ scope_type, scope_node, pushed_lexical_environment }); } void Interpreter::exit_scope(const ScopeNode& scope_node) { while (!m_scope_stack.is_empty()) { auto popped_scope = m_scope_stack.take_last(); + if (popped_scope.pushed_environment) + m_call_stack.last().environment = m_call_stack.last().environment->parent(); if (popped_scope.scope_node.ptr() == &scope_node) break; } @@ -125,17 +158,15 @@ void Interpreter::exit_scope(const ScopeNode& scope_node) void Interpreter::set_variable(const FlyString& name, Value value, bool first_assignment) { - for (ssize_t i = m_scope_stack.size() - 1; i >= 0; --i) { - auto& scope = m_scope_stack.at(i); - - auto possible_match = scope.variables.get(name); + for (auto* environment = current_environment(); environment; environment = environment->parent()) { + auto possible_match = environment->get(name); if (possible_match.has_value()) { if (!first_assignment && possible_match.value().declaration_kind == DeclarationKind::Const) { throw_exception("Assignment to constant variable"); return; } - scope.variables.set(move(name), { move(value), possible_match.value().declaration_kind }); + environment->set(name, { value, possible_match.value().declaration_kind }); return; } } @@ -145,13 +176,11 @@ void Interpreter::set_variable(const FlyString& name, Value value, bool first_as Optional Interpreter::get_variable(const FlyString& name) { - for (ssize_t i = m_scope_stack.size() - 1; i >= 0; --i) { - auto& scope = m_scope_stack.at(i); - auto value = scope.variables.get(name); - if (value.has_value()) - return value.value().value; + for (auto* environment = current_environment(); environment; environment = environment->parent()) { + auto possible_match = environment->get(name); + if (possible_match.has_value()) + return possible_match.value().value; } - return global_object().get(name); } @@ -169,13 +198,6 @@ void Interpreter::gather_roots(Badge, HashTable& roots) if (m_last_value.is_cell()) roots.set(m_last_value.as_cell()); - for (auto& scope : m_scope_stack) { - for (auto& it : scope.variables) { - if (it.value.value.is_cell()) - roots.set(it.value.value.as_cell()); - } - } - for (auto& call_frame : m_call_stack) { if (call_frame.this_value.is_cell()) roots.set(call_frame.this_value.as_cell()); @@ -183,6 +205,7 @@ void Interpreter::gather_roots(Badge, HashTable& roots) if (argument.is_cell()) roots.set(argument.as_cell()); } + roots.set(call_frame.environment); } } @@ -192,6 +215,7 @@ Value Interpreter::call(Function* function, Value this_value, const Vectorname(); call_frame.this_value = this_value; call_frame.arguments = arguments; + call_frame.environment = function->create_environment(); auto result = function->call(*this); pop_call_frame(); return result; diff --git a/Libraries/LibJS/Interpreter.h b/Libraries/LibJS/Interpreter.h index b25df3352a..4cd9a8fb45 100644 --- a/Libraries/LibJS/Interpreter.h +++ b/Libraries/LibJS/Interpreter.h @@ -33,6 +33,7 @@ #include #include #include +#include #include namespace JS { @@ -46,21 +47,17 @@ enum class ScopeType { Continuable, }; -struct Variable { - Value value; - DeclarationKind declaration_kind; -}; - struct ScopeFrame { ScopeType type; NonnullRefPtr scope_node; - HashMap variables; + bool pushed_environment { false }; }; struct CallFrame { FlyString function_name; Value this_value; Vector arguments; + LexicalEnvironment* environment { nullptr }; }; struct Argument { @@ -106,12 +103,18 @@ public: CallFrame& push_call_frame() { - m_call_stack.append({ {}, js_undefined(), {} }); + m_call_stack.append({ {}, js_undefined(), {}, nullptr }); return m_call_stack.last(); } void pop_call_frame() { m_call_stack.take_last(); } const CallFrame& call_frame() { return m_call_stack.last(); } - const Vector call_stack() { return m_call_stack; } + const Vector& call_stack() { return m_call_stack; } + + void push_environment(LexicalEnvironment*); + void pop_environment(); + + const LexicalEnvironment* current_environment() const { return m_call_stack.last().environment; } + LexicalEnvironment* current_environment() { return m_call_stack.last().environment; } size_t argument_count() const { diff --git a/Libraries/LibJS/Makefile b/Libraries/LibJS/Makefile index e7c7cc0f70..3e924b8dac 100644 --- a/Libraries/LibJS/Makefile +++ b/Libraries/LibJS/Makefile @@ -25,6 +25,7 @@ OBJS = \ Runtime/FunctionConstructor.o \ Runtime/FunctionPrototype.o \ Runtime/GlobalObject.o \ + Runtime/LexicalEnvironment.o \ Runtime/MathObject.o \ Runtime/NativeFunction.o \ Runtime/NativeProperty.o \ diff --git a/Libraries/LibJS/Runtime/Function.h b/Libraries/LibJS/Runtime/Function.h index 242f671b9e..3465e02135 100644 --- a/Libraries/LibJS/Runtime/Function.h +++ b/Libraries/LibJS/Runtime/Function.h @@ -38,6 +38,7 @@ public: virtual Value call(Interpreter&) = 0; virtual Value construct(Interpreter&) = 0; virtual const FlyString& name() const = 0; + virtual LexicalEnvironment* create_environment() = 0; protected: Function(); diff --git a/Libraries/LibJS/Runtime/LexicalEnvironment.cpp b/Libraries/LibJS/Runtime/LexicalEnvironment.cpp new file mode 100644 index 0000000000..ddf244bb68 --- /dev/null +++ b/Libraries/LibJS/Runtime/LexicalEnvironment.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +namespace JS { + +LexicalEnvironment::LexicalEnvironment() +{ +} + +LexicalEnvironment::LexicalEnvironment(HashMap variables, LexicalEnvironment* parent) + : m_parent(parent) + , m_variables(move(variables)) +{ +} + +LexicalEnvironment::~LexicalEnvironment() +{ +} + +void LexicalEnvironment::visit_children(Visitor& visitor) +{ + Cell::visit_children(visitor); + if (m_parent) + visitor.visit(m_parent); + for (auto& it : m_variables) + visitor.visit(it.value.value); +} + +Optional LexicalEnvironment::get(const FlyString& name) const +{ + return m_variables.get(name); +} + +void LexicalEnvironment::set(const FlyString& name, Variable variable) +{ + m_variables.set(name, variable); +} + +} diff --git a/Libraries/LibJS/Runtime/LexicalEnvironment.h b/Libraries/LibJS/Runtime/LexicalEnvironment.h new file mode 100644 index 0000000000..c2cf3d8651 --- /dev/null +++ b/Libraries/LibJS/Runtime/LexicalEnvironment.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS { + +struct Variable { + Value value; + DeclarationKind declaration_kind; +}; + +class LexicalEnvironment final : public Cell { +public: + LexicalEnvironment(); + LexicalEnvironment(HashMap variables, LexicalEnvironment* parent); + virtual ~LexicalEnvironment() override; + + LexicalEnvironment* parent() { return m_parent; } + + Optional get(const FlyString&) const; + void set(const FlyString&, Variable); + + void clear(); + + const HashMap& variables() const { return m_variables; } + +private: + virtual const char* class_name() const override { return "LexicalEnvironment"; } + virtual void visit_children(Visitor&) override; + + LexicalEnvironment* m_parent { nullptr }; + HashMap m_variables; +}; + +} diff --git a/Libraries/LibJS/Runtime/NativeFunction.h b/Libraries/LibJS/Runtime/NativeFunction.h index d79fb38b04..ee03bafb47 100644 --- a/Libraries/LibJS/Runtime/NativeFunction.h +++ b/Libraries/LibJS/Runtime/NativeFunction.h @@ -49,6 +49,7 @@ protected: private: virtual bool is_native_function() const override { return true; } virtual const char* class_name() const override { return "NativeFunction"; } + virtual LexicalEnvironment* create_environment() override final { return nullptr; } FlyString m_name; AK::Function m_native_function; diff --git a/Libraries/LibJS/Runtime/ScriptFunction.cpp b/Libraries/LibJS/Runtime/ScriptFunction.cpp index 3560ea77b4..ab241fe331 100644 --- a/Libraries/LibJS/Runtime/ScriptFunction.cpp +++ b/Libraries/LibJS/Runtime/ScriptFunction.cpp @@ -33,11 +33,17 @@ namespace JS { -ScriptFunction::ScriptFunction(const FlyString& name, const Statement& body, Vector parameters) +ScriptFunction::ScriptFunction(const FlyString& name, const Statement& body, Vector parameters, LexicalEnvironment* parent_environment) : m_name(name) , m_body(body) , m_parameters(move(parameters)) + , m_parent_environment(parent_environment) { + HashMap variables; + for (auto& parameter : parameters) { + variables.set(parameter, {}); + } + put("prototype", heap().allocate()); put_native_property("length", length_getter, length_setter); } @@ -46,6 +52,30 @@ ScriptFunction::~ScriptFunction() { } +void ScriptFunction::visit_children(Visitor& visitor) +{ + Function::visit_children(visitor); + visitor.visit(m_parent_environment); +} + +LexicalEnvironment* ScriptFunction::create_environment() +{ + HashMap variables; + for (auto& parameter : m_parameters) { + variables.set(parameter, { js_undefined(), DeclarationKind::Var }); + } + + if (body().is_scope_node()) { + for (auto& declaration : static_cast(body()).variables()) { + for (auto& declarator : declaration.declarations()) { + variables.set(declarator.id().string(), { js_undefined(), DeclarationKind::Var }); + } + } + } + + return heap().allocate(move(variables), m_parent_environment); +} + Value ScriptFunction::call(Interpreter& interpreter) { auto& argument_values = interpreter.call_frame().arguments; @@ -55,7 +85,8 @@ Value ScriptFunction::call(Interpreter& interpreter) auto value = js_undefined(); if (i < argument_values.size()) value = argument_values[i]; - arguments.append({ move(name), move(value) }); + arguments.append({ name, value }); + interpreter.current_environment()->set(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 18afbe131d..1e65faca41 100644 --- a/Libraries/LibJS/Runtime/ScriptFunction.h +++ b/Libraries/LibJS/Runtime/ScriptFunction.h @@ -32,7 +32,7 @@ namespace JS { class ScriptFunction final : public Function { public: - ScriptFunction(const FlyString& name, const Statement& body, Vector parameters = {}); + ScriptFunction(const FlyString& name, const Statement& body, Vector parameters, LexicalEnvironment* parent_environment); virtual ~ScriptFunction(); const Statement& body() const { return m_body; } @@ -46,6 +46,8 @@ public: private: virtual bool is_script_function() const final { return true; } virtual const char* class_name() const override { return "ScriptFunction"; } + virtual LexicalEnvironment* create_environment() override; + virtual void visit_children(Visitor&) override; static Value length_getter(Interpreter&); static void length_setter(Interpreter&, Value); @@ -53,6 +55,7 @@ private: FlyString m_name; NonnullRefPtr m_body; const Vector m_parameters; + LexicalEnvironment* m_parent_environment { nullptr }; }; }