LibJS: Eliminate GeneratorResult GC cell allocation on yield/await

Store yield_continuation and yield_is_await directly in
ExecutionContext instead of allocating a GeneratorResult GC cell.
This removes a heap allocation per yield/await and fixes a latent
bug where continuation addresses stored as doubles could lose
precision.
This commit is contained in:
Johan Dahlin 2026-03-16 00:02:11 +01:00 committed by Andreas Kling
parent 9a34fb59aa
commit 1179e40d3f
14 changed files with 90 additions and 176 deletions

View file

@ -30,7 +30,6 @@
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
#include <LibJS/Runtime/Environment.h>
#include <LibJS/Runtime/FunctionEnvironment.h>
#include <LibJS/Runtime/GeneratorResult.h>
#include <LibJS/Runtime/GlobalEnvironment.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Iterator.h>
@ -93,10 +92,13 @@ Interpreter::~Interpreter() = default;
ALWAYS_INLINE Value Interpreter::do_yield(Value value, Optional<Label> continuation)
{
// FIXME: If we get a pointer, which is not accurately representable as a double
// will cause this to explode
auto continuation_value = continuation.has_value() ? Value(continuation->address()) : js_null();
return vm().heap().allocate<GeneratorResult>(value, continuation_value, false).ptr();
auto& context = running_execution_context();
if (continuation.has_value())
context.yield_continuation = continuation->address();
else
context.yield_continuation = ExecutionContext::no_yield_continuation;
context.yield_is_await = false;
return value;
}
// 16.1.6 ScriptEvaluation ( scriptRecord ), https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation
@ -161,7 +163,7 @@ ThrowCompletionOr<Value> Interpreter::run(Script& script_record, GC::Ptr<Environ
// 13. If result.[[Type]] is normal, then
if (executable && result.type() == Completion::Type::Normal) {
// a. Set result to Completion(Evaluation of script).
result = run_executable(*script_context, *executable, {}, {});
result = run_executable(*script_context, *executable, 0, {});
// b. If result is a normal completion and result.[[Value]] is empty, then
if (result.type() == Completion::Type::Normal && result.value().is_special_empty_value()) {
@ -821,7 +823,7 @@ DeclarativeEnvironment& Interpreter::global_declarative_environment()
return realm().global_declarative_environment();
}
ThrowCompletionOr<Value> Interpreter::run_executable(ExecutionContext& context, Executable& executable, Optional<size_t> entry_point)
ThrowCompletionOr<Value> Interpreter::run_executable(ExecutionContext& context, Executable& executable, u32 entry_point)
{
dbgln_if(JS_BYTECODE_DEBUG, "Bytecode::Interpreter will run unit {}", &executable);
@ -847,7 +849,7 @@ ThrowCompletionOr<Value> Interpreter::run_executable(ExecutionContext& context,
executable.constants.data(),
count * sizeof(Value));
run_bytecode(entry_point.value_or(0));
run_bytecode(entry_point);
dbgln_if(JS_BYTECODE_DEBUG, "Bytecode::Interpreter did run unit {}", context.executable);
@ -3089,11 +3091,10 @@ void Yield::execute_impl(Bytecode::Interpreter& interpreter) const
void Await::execute_impl(Bytecode::Interpreter& interpreter) const
{
auto yielded_value = interpreter.get(m_argument).is_special_empty_value() ? js_undefined() : interpreter.get(m_argument);
// FIXME: If we get a pointer, which is not accurately representable as a double
// will cause this to explode
auto continuation_value = Value(m_continuation_label.address());
auto result = interpreter.vm().heap().allocate<GeneratorResult>(yielded_value, continuation_value, true);
interpreter.do_return(result);
auto& context = interpreter.running_execution_context();
context.yield_continuation = m_continuation_label.address();
context.yield_is_await = true;
interpreter.do_return(yielded_value);
}
ThrowCompletionOr<void> GetByValue::execute_impl(Bytecode::Interpreter& interpreter) const

View file

@ -34,9 +34,9 @@ public:
ThrowCompletionOr<Value> run(Script&, GC::Ptr<Environment> lexical_environment_override = nullptr);
ThrowCompletionOr<Value> run(SourceTextModule&);
ThrowCompletionOr<Value> run_executable(ExecutionContext&, Executable&, Optional<size_t> entry_point);
ThrowCompletionOr<Value> run_executable(ExecutionContext&, Executable&, u32 entry_point = 0);
ThrowCompletionOr<Value> run_executable(ExecutionContext& context, Executable& executable, Optional<size_t> entry_point, Value initial_accumulator_value)
ThrowCompletionOr<Value> run_executable(ExecutionContext& context, Executable& executable, u32 entry_point, Value initial_accumulator_value)
{
context.registers_and_constants_and_locals_and_arguments_span()[0] = initial_accumulator_value;
return run_executable(context, executable, entry_point);

View file

@ -94,7 +94,6 @@ set(SOURCES
Runtime/GeneratorFunctionPrototype.cpp
Runtime/GeneratorObject.cpp
Runtime/GeneratorPrototype.cpp
Runtime/GeneratorResult.cpp
Runtime/GlobalEnvironment.cpp
Runtime/GlobalObject.cpp
Runtime/IndexedProperties.cpp

View file

@ -19,7 +19,6 @@ class JS_API Cell : public GC::Cell {
public:
MUST_UPCALL virtual void initialize(Realm&);
virtual bool is_generator_result() const { return false; }
virtual bool is_environment() const { return false; }
ALWAYS_INLINE VM& vm() const;

View file

@ -10,7 +10,6 @@
#include <LibJS/Runtime/AsyncGeneratorRequest.h>
#include <LibJS/Runtime/CompletionCell.h>
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
#include <LibJS/Runtime/GeneratorResult.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/NativeJavaScriptBackedFunction.h>
#include <LibJS/Runtime/PromiseConstructor.h>
@ -19,7 +18,7 @@ namespace JS {
GC_DEFINE_ALLOCATOR(AsyncGenerator);
GC::Ref<AsyncGenerator> AsyncGenerator::create(Realm& realm, Value initial_value, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>> generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
GC::Ref<AsyncGenerator> AsyncGenerator::create(Realm& realm, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>> generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
{
auto& vm = realm.vm();
// This is "g1.prototype" in figure-2 (https://tc39.es/ecma262/img/figure-2.png)
@ -39,14 +38,14 @@ GC::Ref<AsyncGenerator> AsyncGenerator::create(Realm& realm, Value initial_value
return function->bytecode_executable();
});
return realm.create<AsyncGenerator>(realm, generating_function_prototype_object, move(execution_context), generating_executable, initial_value);
return realm.create<AsyncGenerator>(realm, generating_function_prototype_object, move(execution_context), generating_executable);
}
AsyncGenerator::AsyncGenerator(Realm& realm, Object* prototype, NonnullOwnPtr<ExecutionContext> context, GC::Ref<Bytecode::Executable> bytecode_executable, Value initial_value)
AsyncGenerator::AsyncGenerator(Realm& realm, Object* prototype, NonnullOwnPtr<ExecutionContext> context, GC::Ref<Bytecode::Executable> bytecode_executable)
: Object(realm, prototype)
, m_async_generator_context(move(context))
, m_generating_executable(bytecode_executable)
, m_previous_value(initial_value)
, m_yield_continuation(m_async_generator_context->yield_continuation)
{
}
@ -60,7 +59,6 @@ void AsyncGenerator::visit_edges(Cell::Visitor& visitor)
visitor.visit(request.capability);
}
visitor.visit(m_generating_executable);
visitor.visit(m_previous_value);
visitor.visit(m_current_promise);
m_async_generator_context->visit_edges(visitor);
}
@ -167,56 +165,49 @@ ThrowCompletionOr<void> AsyncGenerator::await(Value value)
void AsyncGenerator::execute(VM& vm, Completion completion)
{
while (true) {
// Loosely based on step 4 of https://tc39.es/ecma262/#sec-asyncgeneratorstart
auto generated_value = [](Value value) -> Value {
if (value.is_cell() && value.as_cell().is_generator_result())
return static_cast<GeneratorResult const&>(value.as_cell()).result();
return value.is_special_empty_value() ? js_undefined() : value;
};
auto generated_continuation = [&](Value value) -> Optional<size_t> {
if (value.is_cell() && value.as_cell().is_generator_result()) {
auto number_value = static_cast<GeneratorResult const&>(value.as_cell()).continuation();
if (number_value.is_null())
return {};
return static_cast<size_t>(number_value.as_double());
}
return {};
};
auto generated_is_await = [](Value value) -> bool {
if (value.is_cell() && value.as_cell().is_generator_result())
return static_cast<GeneratorResult const&>(value.as_cell()).is_await();
return false;
};
auto completion_cell = heap().allocate<CompletionCell>(completion);
auto& bytecode_interpreter = vm.bytecode_interpreter();
auto const continuation_address = generated_continuation(m_previous_value);
// We should never enter `execute` again after the generator is complete.
VERIFY(continuation_address.has_value());
VERIFY(m_yield_continuation != ExecutionContext::no_yield_continuation);
auto result_value = bytecode_interpreter.run_executable(vm.running_execution_context(), m_generating_executable, continuation_address, completion_cell);
// Clear yield state so that a normal return (no yield) is detected as done.
m_async_generator_context->yield_continuation = ExecutionContext::no_yield_continuation;
if (!result_value.is_throw_completion()) {
m_previous_value = result_value.release_value();
auto value = generated_value(m_previous_value);
bool is_await = generated_is_await(m_previous_value);
auto result_value = bytecode_interpreter.run_executable(vm.running_execution_context(), m_generating_executable, m_yield_continuation, completion_cell);
if (is_await) {
auto await_result = this->await(value);
if (await_result.is_throw_completion()) {
completion = await_result.release_error();
continue;
}
return;
}
if (result_value.is_throw_completion()) {
m_yield_continuation = ExecutionContext::no_yield_continuation;
// 27.6.3.2 AsyncGeneratorStart ( generator, generatorBody ), https://tc39.es/ecma262/#sec-asyncgeneratorstart
// 4.e. Assert: If we return here, the async generator either threw an exception or performed either an implicit or explicit return.
// 4.f. Remove acGenContext from the execution context stack and restore the execution context
// that is at the top of the execution context stack as the running execution context.
vm.pop_execution_context();
m_async_generator_state = State::Completed;
complete_step(result_value.release_error(), true);
drain_queue();
return;
}
bool done = result_value.is_throw_completion() || !generated_continuation(m_previous_value).has_value();
auto value = result_value.release_value();
if (value.is_special_empty_value())
value = js_undefined();
m_yield_continuation = m_async_generator_context->yield_continuation;
bool is_await = m_async_generator_context->yield_is_await;
if (is_await) {
auto await_result = this->await(value);
if (await_result.is_throw_completion()) {
completion = await_result.release_error();
continue;
}
return;
}
bool done = m_yield_continuation == ExecutionContext::no_yield_continuation;
if (!done) {
// 27.6.3.8 AsyncGeneratorYield ( value ), https://tc39.es/ecma262/#sec-asyncgeneratoryield
// 1. Let genContext be the running execution context.
@ -226,7 +217,6 @@ void AsyncGenerator::execute(VM& vm, Completion completion)
// NOTE: genContext is `m_async_generator_context`, generator is `this`.
// 5. Let completion be NormalCompletion(value).
auto value = generated_value(m_previous_value);
auto yield_completion = normal_completion(value);
// 6. Assert: The execution context stack has at least two elements.
@ -286,15 +276,8 @@ void AsyncGenerator::execute(VM& vm, Completion completion)
// 4.h. If result.[[Type]] is normal, set result to NormalCompletion(undefined).
// 4.i. If result.[[Type]] is return, set result to NormalCompletion(result.[[Value]]).
Completion result;
if (!result_value.is_throw_completion()) {
result = normal_completion(generated_value(m_previous_value));
} else {
result = result_value.release_error();
}
// 4.j. Perform AsyncGeneratorCompleteStep(acGenerator, result, true).
complete_step(result, true);
complete_step(normal_completion(value), true);
// 4.k. Perform AsyncGeneratorDrainQueue(acGenerator).
drain_queue();

View file

@ -28,7 +28,7 @@ public:
Completed,
};
static GC::Ref<AsyncGenerator> create(Realm&, Value, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>>, NonnullOwnPtr<ExecutionContext>);
static GC::Ref<AsyncGenerator> create(Realm&, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>>, NonnullOwnPtr<ExecutionContext>);
virtual ~AsyncGenerator() override;
@ -44,7 +44,7 @@ public:
Optional<String> const& generator_brand() const { return m_generator_brand; }
private:
AsyncGenerator(Realm&, Object* prototype, NonnullOwnPtr<ExecutionContext>, GC::Ref<Bytecode::Executable>, Value);
AsyncGenerator(Realm&, Object* prototype, NonnullOwnPtr<ExecutionContext>, GC::Ref<Bytecode::Executable>);
virtual void visit_edges(Cell::Visitor&) override;
@ -59,7 +59,7 @@ private:
Optional<String> m_generator_brand; // [[GeneratorBrand]]
GC::Ref<Bytecode::Executable> m_generating_executable;
Value m_previous_value;
u32 m_yield_continuation { ExecutionContext::no_yield_continuation };
GC::Ptr<Promise> m_current_promise;
};

View file

@ -523,9 +523,9 @@ ThrowCompletionOr<Value> ECMAScriptFunctionObject::ordinary_call_evaluate_body(V
return result;
if (kind() == FunctionKind::AsyncGenerator)
return AsyncGenerator::create(*context.realm, result, GC::Ref { *this }, context.copy());
return AsyncGenerator::create(*context.realm, GC::Ref { *this }, context.copy());
auto generator_object = GeneratorObject::create(*context.realm, result, GC::Ref { *this }, context.copy());
auto generator_object = GeneratorObject::create(*context.realm, GC::Ref { *this }, context.copy());
// NOTE: Async functions are entirely transformed to generator functions, and wrapped in a custom driver that returns a promise.
if (kind() == FunctionKind::Async)

View file

@ -112,6 +112,9 @@ NonnullOwnPtr<ExecutionContext> ExecutionContext::copy() const
copy->variable_environment = variable_environment;
copy->private_environment = private_environment;
copy->program_counter = program_counter;
copy->yield_continuation = yield_continuation;
copy->yield_is_await = yield_is_await;
copy->caller_is_construct = caller_is_construct;
copy->this_value = this_value;
copy->executable = executable;
copy->passed_argument_count = passed_argument_count;

View file

@ -62,6 +62,15 @@ public:
// FIXME: Move this out of LibJS (e.g. by using the CustomData concept), as it's used exclusively by LibWeb.
u32 skip_when_determining_incumbent_counter { 0 };
// Non-standard: Used by generators/async generators to communicate yield/await
// state back to the caller without allocating a GC cell.
// UINT32_MAX means "no continuation" (generator is done).
static constexpr u32 no_yield_continuation = UINT32_MAX;
u32 yield_continuation { no_yield_continuation };
bool yield_is_await { false };
bool caller_is_construct { false };
Optional<Value> this_value;
GC::Ptr<Bytecode::Executable> executable;
@ -110,7 +119,6 @@ public:
u32 passed_argument_count { 0 };
u32 caller_return_pc { 0 };
u32 caller_dst_raw { 0 };
bool caller_is_construct { false };
private:
friend class Bytecode::Interpreter;

View file

@ -9,7 +9,6 @@
#include <LibJS/Runtime/CompletionCell.h>
#include <LibJS/Runtime/GeneratorObject.h>
#include <LibJS/Runtime/GeneratorPrototype.h>
#include <LibJS/Runtime/GeneratorResult.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/NativeJavaScriptBackedFunction.h>
@ -18,7 +17,7 @@ namespace JS {
GC_DEFINE_ALLOCATOR(GeneratorObject);
GC::Ref<GeneratorObject> GeneratorObject::create(Realm& realm, Value initial_value, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>> generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
GC::Ref<GeneratorObject> GeneratorObject::create(Realm& realm, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>> generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
{
auto& vm = realm.vm();
// This is "g1.prototype" in figure-2 (https://tc39.es/ecma262/img/figure-2.png)
@ -56,7 +55,7 @@ GC::Ref<GeneratorObject> GeneratorObject::create(Realm& realm, Value initial_val
auto object = realm.create<GeneratorObject>(realm, generating_function_prototype_object, move(execution_context));
object->m_generating_executable = generating_executable;
object->m_previous_value = initial_value;
object->m_yield_continuation = object->m_execution_context->yield_continuation;
return object;
}
@ -71,7 +70,6 @@ void GeneratorObject::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_generating_executable);
visitor.visit(m_previous_value);
m_execution_context->visit_edges(visitor);
}
@ -104,32 +102,17 @@ ThrowCompletionOr<GeneratorObject::IterationResult> GeneratorObject::execute(VM&
{
// Loosely based on step 4 of https://tc39.es/ecma262/#sec-generatorstart mixed with https://tc39.es/ecma262/#sec-generatoryield at the end.
auto generated_value = [](Value value) -> Value {
if (value.is_cell() && value.as_cell().is_generator_result())
return static_cast<GeneratorResult const&>(value.as_cell()).result();
return value.is_special_empty_value() ? js_undefined() : value;
};
auto generated_continuation = [&](Value value) -> Optional<size_t> {
if (value.is_cell() && value.as_cell().is_generator_result()) {
auto number_value = static_cast<GeneratorResult const&>(value.as_cell()).continuation();
if (number_value.is_null())
return {};
return static_cast<u64>(number_value.as_double());
}
return {};
};
auto completion_cell = heap().allocate<CompletionCell>(completion);
auto& bytecode_interpreter = vm.bytecode_interpreter();
auto const next_block = generated_continuation(m_previous_value);
// We should never enter `execute` again after the generator is complete.
VERIFY(next_block.has_value());
VERIFY(m_yield_continuation != ExecutionContext::no_yield_continuation);
auto result_value = bytecode_interpreter.run_executable(vm.running_execution_context(), *m_generating_executable, next_block, completion_cell);
// Clear yield state so that a normal return (no yield) is detected as done.
m_execution_context->yield_continuation = ExecutionContext::no_yield_continuation;
auto result_value = bytecode_interpreter.run_executable(vm.running_execution_context(), *m_generating_executable, m_yield_continuation, completion_cell);
vm.pop_execution_context();
@ -138,12 +121,17 @@ ThrowCompletionOr<GeneratorObject::IterationResult> GeneratorObject::execute(VM&
m_generator_state = GeneratorState::Completed;
return result_value.throw_completion();
}
m_previous_value = result_value.release_value();
bool done = !generated_continuation(m_previous_value).has_value();
auto value = result_value.release_value();
if (value.is_special_empty_value())
value = js_undefined();
m_yield_continuation = m_execution_context->yield_continuation;
bool done = m_yield_continuation == ExecutionContext::no_yield_continuation;
m_generator_state = done ? GeneratorState::Completed : GeneratorState::SuspendedYield;
return IterationResult(generated_value(m_previous_value), done);
return IterationResult(value, done);
}
// 27.5.3.3 GeneratorResume ( generator, value, generatorBrand ), https://tc39.es/ecma262/#sec-generatorresume

View file

@ -17,7 +17,7 @@ class GeneratorObject : public Object {
GC_DECLARE_ALLOCATOR(GeneratorObject);
public:
static GC::Ref<GeneratorObject> create(Realm&, Value, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>>, NonnullOwnPtr<ExecutionContext>);
static GC::Ref<GeneratorObject> create(Realm&, Variant<GC::Ref<ECMAScriptFunctionObject>, GC::Ref<NativeJavaScriptBackedFunction>>, NonnullOwnPtr<ExecutionContext>);
virtual ~GeneratorObject() override = default;
void visit_edges(Cell::Visitor&) override;
@ -54,7 +54,7 @@ protected:
private:
NonnullOwnPtr<ExecutionContext> m_execution_context;
GC::Ptr<Bytecode::Executable> m_generating_executable;
Value m_previous_value;
u32 m_yield_continuation { ExecutionContext::no_yield_continuation };
GeneratorState m_generator_state { GeneratorState::SuspendedStart };
Optional<StringView> m_generator_brand;
};

View file

@ -1,22 +0,0 @@
/*
* Copyright (c) 2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/GeneratorResult.h>
namespace JS {
GC_DEFINE_ALLOCATOR(GeneratorResult);
GeneratorResult::~GeneratorResult() = default;
void GeneratorResult::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_result);
visitor.visit(m_continuation);
}
}

View file

@ -1,45 +0,0 @@
/*
* Copyright (c) 2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/CellAllocator.h>
#include <LibJS/Heap/Cell.h>
#include <LibJS/Runtime/Value.h>
namespace JS {
class GeneratorResult final : public Cell {
GC_CELL(GeneratorResult, Cell);
GC_DECLARE_ALLOCATOR(GeneratorResult);
public:
GeneratorResult(Value result, Value continuation, bool is_await)
: m_is_await(is_await)
, m_result(result)
, m_continuation(continuation)
{
}
virtual ~GeneratorResult() override;
[[nodiscard]] Value result() const { return m_result; }
[[nodiscard]] Value continuation() const { return m_continuation; }
[[nodiscard]] bool is_await() const { return m_is_await; }
private:
virtual bool is_generator_result() const override { return true; }
virtual void visit_edges(Cell::Visitor& visitor) override;
bool m_is_await { false };
Value m_result;
Value m_continuation;
};
template<>
inline bool Cell::fast_is<GeneratorResult>() const { return is_generator_result(); }
}

View file

@ -80,9 +80,9 @@ ThrowCompletionOr<Value> NativeJavaScriptBackedFunction::call()
auto& realm = *vm.current_realm();
if (kind == FunctionKind::AsyncGenerator)
return AsyncGenerator::create(realm, result, GC::Ref { *this }, vm.running_execution_context().copy());
return AsyncGenerator::create(realm, GC::Ref { *this }, vm.running_execution_context().copy());
auto generator_object = GeneratorObject::create(realm, result, GC::Ref { *this }, vm.running_execution_context().copy());
auto generator_object = GeneratorObject::create(realm, GC::Ref { *this }, vm.running_execution_context().copy());
// NOTE: Async functions are entirely transformed to generator functions, and wrapped in a custom driver that returns a promise.
if (kind == FunctionKind::Async)