ladybird/Libraries/LibJS/Runtime/Environment.h
Andreas Kling 5674f8bbe0 LibJS: Limit eval() deoptimization to the containing function scope
Previously, when direct eval() was called, we would mark the entire
environment chain as "permanently screwed by eval", disabling variable
access caching all the way up to the global scope.

This was overly conservative. According to the ECMAScript specification,
a sloppy direct eval() can only inject var declarations into its
containing function's variable environment - it cannot inject variables
into parent function scopes.

This patch makes two changes:

1. Stop propagating the "screwed by eval" flag at function boundaries.
   When set_permanently_screwed_by_eval() hits a FunctionEnvironment or
   GlobalEnvironment, it no longer continues to outer environments.

2. Check each environment during cache lookup traversal. If any
   environment in the path is marked as screwed, we bail to the slow
   path. This catches the case where we're inside a function with eval
   and have a cached coordinate pointing to an outer scope.

The second change is necessary because eval can create local variables
that shadow outer bindings. When looking up a variable from inside a
function that called eval, we can't trust cached coordinates that point
to outer scopes, since eval may have created a closer binding.

This improves performance for code with nested functions where an inner
function uses eval but parent functions perform many variable accesses.
The parent functions can now use cached environment coordinates.

All 29 new tests verify behavior matches V8.
2026-01-27 10:58:39 +01:00

88 lines
3.4 KiB
C++

/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
#include <LibJS/Export.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Object.h>
namespace JS {
struct Variable {
Value value;
DeclarationKind declaration_kind;
};
#define JS_ENVIRONMENT(class_, base_class) GC_CELL(class_, base_class)
class JS_API Environment : public Cell {
GC_CELL(Environment, Cell);
public:
enum class InitializeBindingHint {
Normal,
SyncDispose,
AsyncDispose,
};
virtual bool has_this_binding() const { return false; }
virtual ThrowCompletionOr<Value> get_this_binding(VM&) const { return Value {}; }
virtual Object* with_base_object() const { return nullptr; }
virtual ThrowCompletionOr<bool> has_binding([[maybe_unused]] Utf16FlyString const& name, [[maybe_unused]] Optional<size_t>* out_index = nullptr) const = 0;
virtual ThrowCompletionOr<void> create_mutable_binding(VM&, [[maybe_unused]] Utf16FlyString const& name, [[maybe_unused]] bool can_be_deleted) = 0;
virtual ThrowCompletionOr<void> create_immutable_binding(VM&, [[maybe_unused]] Utf16FlyString const& name, [[maybe_unused]] bool strict) = 0;
virtual ThrowCompletionOr<void> initialize_binding(VM&, [[maybe_unused]] Utf16FlyString const& name, Value, InitializeBindingHint) = 0;
virtual ThrowCompletionOr<void> set_mutable_binding(VM&, [[maybe_unused]] Utf16FlyString const& name, Value, [[maybe_unused]] bool strict) = 0;
virtual ThrowCompletionOr<Value> get_binding_value(VM&, [[maybe_unused]] Utf16FlyString const& name, [[maybe_unused]] bool strict) = 0;
virtual ThrowCompletionOr<bool> delete_binding(VM&, [[maybe_unused]] Utf16FlyString const& name) = 0;
// [[OuterEnv]]
Environment* outer_environment() { return m_outer_environment; }
Environment const* outer_environment() const { return m_outer_environment; }
[[nodiscard]] bool is_declarative_environment() const { return m_declarative; }
virtual bool is_global_environment() const { return false; }
virtual bool is_function_environment() const { return false; }
template<typename T>
bool fast_is() const = delete;
// This flag is set on environments within a function when direct eval() is performed in that function.
// It propagates up to the function boundary (not beyond) and is used to disable variable access caching.
// Code in parent functions is not affected because eval can only inject vars into its containing
// function's variable environment, not into parent function scopes.
bool is_permanently_screwed_by_eval() const { return m_permanently_screwed_by_eval; }
void set_permanently_screwed_by_eval();
protected:
enum class IsDeclarative {
No,
Yes,
};
explicit Environment(Environment* parent, IsDeclarative = IsDeclarative::No);
virtual void visit_edges(Visitor&) override;
// NB: This belongs to FunctionEnvironment, but we keep it here to pack better.
ThisBindingStatus m_this_binding_status { ThisBindingStatus::Uninitialized }; // [[ThisBindingStatus]]
private:
virtual bool is_environment() const final { return true; }
bool m_permanently_screwed_by_eval { false };
bool m_declarative { false };
GC::Ptr<Environment> m_outer_environment;
};
template<>
inline bool Cell::fast_is<Environment>() const { return is_environment(); }
}