From 5bc33712268ab6d2d2c65bc78b89d55e679c135a Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Fri, 25 Nov 2022 23:14:08 +0000 Subject: [PATCH] LibJS: Perform received abrupt generator completions in the generator Previously, throw and return completions would not be executed inside the generator. This is incorrect, as throw and return need to perform unwinds which can potentially execute more code inside the generator, such as finally blocks. This is done by also passing the completion type alongside the passed in value. The continuation block will immediately extract and type and value and perform the appropriate operation for the given type. For normal completions, this is continuing as normal. For throw completions, it will perform `throw `. For return completions, it will perform `return `, which is a `Yield return` in this case due to being inside a generator. This also refactors GeneratorObject to properly send across the completion type and value to the generator inside of trying to operate on the completions itself. This is a prerequisite for yield*, as it performs special iterator operations when receiving a throw/return completion and does not complete the generator like the regular yield would. There's still more work to be done to make GeneratorObject::execute be closer to the spec. It's mostly a restructuring of the existing GeneratorObject::next_impl. --- .../Libraries/LibJS/Bytecode/ASTCodegen.cpp | 54 +++++ .../Libraries/LibJS/Bytecode/Generator.cpp | 2 + .../Runtime/AsyncFunctionDriverWrapper.cpp | 4 +- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 2 + .../LibJS/Runtime/GeneratorObject.cpp | 200 ++++++++++++++---- .../Libraries/LibJS/Runtime/GeneratorObject.h | 17 +- .../LibJS/Runtime/GeneratorPrototype.cpp | 20 +- 7 files changed, 249 insertions(+), 50 deletions(-) diff --git a/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp b/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp index 9c8534a08e..fb272f21a3 100644 --- a/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp +++ b/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp @@ -1591,6 +1591,25 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod { VERIFY(generator.is_in_generator_function()); + auto received_completion_register = generator.allocate_register(); + auto received_completion_type_register = generator.allocate_register(); + auto received_completion_value_register = generator.allocate_register(); + + auto type_identifier = generator.intern_identifier("type"); + auto value_identifier = generator.intern_identifier("value"); + + auto get_received_completion_type_and_value = [&]() { + // The accumulator is set to an object, for example: { "type": 1 (normal), value: 1337 } + generator.emit(received_completion_register); + + generator.emit(type_identifier); + generator.emit(received_completion_type_register); + + generator.emit(received_completion_register); + generator.emit(value_identifier); + generator.emit(received_completion_value_register); + }; + if (m_is_yield_from) { return Bytecode::CodeGenerationError { this, @@ -1606,6 +1625,41 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod auto& continuation_block = generator.make_block(); generator.emit(Bytecode::Label { continuation_block }); generator.switch_to_basic_block(continuation_block); + get_received_completion_type_and_value(); + + auto& normal_completion_continuation_block = generator.make_block(); + auto& throw_completion_continuation_block = generator.make_block(); + + generator.emit(Value(to_underlying(Completion::Type::Normal))); + generator.emit(received_completion_type_register); + generator.emit( + Bytecode::Label { normal_completion_continuation_block }, + Bytecode::Label { throw_completion_continuation_block }); + + auto& throw_value_block = generator.make_block(); + auto& return_value_block = generator.make_block(); + + generator.switch_to_basic_block(throw_completion_continuation_block); + generator.emit(Value(to_underlying(Completion::Type::Throw))); + generator.emit(received_completion_type_register); + + // If type is not equal to "throw" or "normal", assume it's "return". + generator.emit( + Bytecode::Label { throw_value_block }, + Bytecode::Label { return_value_block }); + + generator.switch_to_basic_block(throw_value_block); + generator.emit(received_completion_value_register); + generator.perform_needed_unwinds(); + generator.emit(); + + generator.switch_to_basic_block(return_value_block); + generator.emit(received_completion_value_register); + generator.perform_needed_unwinds(); + generator.emit(nullptr); + + generator.switch_to_basic_block(normal_completion_continuation_block); + generator.emit(received_completion_value_register); return {}; } diff --git a/Userland/Libraries/LibJS/Bytecode/Generator.cpp b/Userland/Libraries/LibJS/Bytecode/Generator.cpp index 76ca4ebd75..9504bef2ca 100644 --- a/Userland/Libraries/LibJS/Bytecode/Generator.cpp +++ b/Userland/Libraries/LibJS/Bytecode/Generator.cpp @@ -29,6 +29,8 @@ CodeGenerationErrorOr> Generator::generate(ASTNode con auto& start_block = generator.make_block(); generator.emit(Label { start_block }); generator.switch_to_basic_block(start_block); + // NOTE: This doesn't have to handle received throw/return completions, as GeneratorObject::resume_abrupt + // will not enter the generator from the SuspendedStart state and immediately completes the generator. } TRY(node.generate_bytecode(generator)); if (generator.is_in_generator_or_async_function()) { diff --git a/Userland/Libraries/LibJS/Runtime/AsyncFunctionDriverWrapper.cpp b/Userland/Libraries/LibJS/Runtime/AsyncFunctionDriverWrapper.cpp index b8bf7c5fe3..36c93b2c41 100644 --- a/Userland/Libraries/LibJS/Runtime/AsyncFunctionDriverWrapper.cpp +++ b/Userland/Libraries/LibJS/Runtime/AsyncFunctionDriverWrapper.cpp @@ -36,8 +36,8 @@ ThrowCompletionOr AsyncFunctionDriverWrapper::react_to_async_task_complet auto& realm = *vm.current_realm(); auto generator_result = is_successful - ? m_generator_object->next_impl(vm, value, {}) - : m_generator_object->next_impl(vm, {}, value); + ? m_generator_object->resume(vm, value, {}) + : m_generator_object->resume_abrupt(vm, throw_completion(value), {}); if (generator_result.is_throw_completion()) { VERIFY(generator_result.throw_completion().type() == Completion::Type::Throw); diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index a65e027f6d..b832c09bfa 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -36,6 +36,8 @@ M(DynamicImportNotAllowed, "Dynamic Imports are not allowed") \ M(FinalizationRegistrySameTargetAndValue, "Target and held value must not be the same") \ M(GetCapabilitiesExecutorCalledMultipleTimes, "GetCapabilitiesExecutor was called multiple times") \ + M(GeneratorAlreadyExecuting, "Generator is already executing") \ + M(GeneratorBrandMismatch, "Generator brand '{}' does not match generator brand '{}')") \ M(GlobalEnvironmentAlreadyHasBinding, "Global environment already has binding '{}'") \ M(IndexOutOfRange, "Index {} is out of range of array length {}") \ M(InOperatorWithObject, "'in' operator must be used on an object") \ diff --git a/Userland/Libraries/LibJS/Runtime/GeneratorObject.cpp b/Userland/Libraries/LibJS/Runtime/GeneratorObject.cpp index 5eee673cfe..6e60ff8efd 100644 --- a/Userland/Libraries/LibJS/Runtime/GeneratorObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GeneratorObject.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace JS { @@ -51,67 +52,79 @@ void GeneratorObject::visit_edges(Cell::Visitor& visitor) visitor.visit(m_previous_value); } -ThrowCompletionOr GeneratorObject::next_impl(VM& vm, Optional next_argument, Optional value_to_throw) +// 27.5.3.2 GeneratorValidate ( generator, generatorBrand ), https://tc39.es/ecma262/#sec-generatorvalidate +ThrowCompletionOr GeneratorObject::validate(VM& vm, Optional const& generator_brand) { - auto& realm = *vm.current_realm(); - auto bytecode_interpreter = Bytecode::Interpreter::current(); - VERIFY(bytecode_interpreter); + // 1. Perform ? RequireInternalSlot(generator, [[GeneratorState]]). + // 2. Perform ? RequireInternalSlot(generator, [[GeneratorBrand]]). + // NOTE: Already done by the caller of resume or resume_abrupt, as they wouldn't have a GeneratorObject otherwise. - auto generated_value = [](Value value) -> ThrowCompletionOr { + // 3. If generator.[[GeneratorBrand]] is not the same value as generatorBrand, throw a TypeError exception. + if (m_generator_brand != generator_brand) + return vm.throw_completion(ErrorType::GeneratorBrandMismatch, m_generator_brand.value_or(""), generator_brand.value_or("")); + + // 4. Assert: generator also has a [[GeneratorContext]] internal slot. + // NOTE: Done by already being a GeneratorObject. + + // 5. Let state be generator.[[GeneratorState]]. + auto state = m_generator_state; + + // 6. If state is executing, throw a TypeError exception. + if (state == GeneratorState::Executing) + return vm.throw_completion(ErrorType::GeneratorAlreadyExecuting); + + // 7. Return state. + return state; +} + +ThrowCompletionOr GeneratorObject::execute(VM& vm, Completion const& completion) +{ + // Loosely based on step 4 of https://tc39.es/ecma262/#sec-generatorstart mixed with https://tc39.es/ecma262/#sec-generatoryield at the end. + + VERIFY(completion.value().has_value()); + + auto generated_value = [](Value value) -> Value { if (value.is_object()) - return TRY(value.as_object().get("result")); + return value.as_object().get_without_side_effects("result"); return value.is_empty() ? js_undefined() : value; }; - auto generated_continuation = [&](Value value) -> ThrowCompletionOr { + auto generated_continuation = [&](Value value) -> Bytecode::BasicBlock const* { if (value.is_object()) { - auto number_value = TRY(value.as_object().get("continuation")); - return reinterpret_cast(static_cast(TRY(number_value.to_double(vm)))); + auto number_value = value.as_object().get_without_side_effects("continuation"); + return reinterpret_cast(static_cast(number_value.as_double())); } return nullptr; }; - auto previous_generated_value = TRY(generated_value(m_previous_value)); + auto& realm = *vm.current_realm(); + auto* completion_object = Object::create(realm, nullptr); + completion_object->define_direct_property(vm.names.type, Value(to_underlying(completion.type())), default_attributes); + completion_object->define_direct_property(vm.names.value, completion.value().value(), default_attributes); - auto result = Object::create(realm, realm.intrinsics().object_prototype()); - result->define_direct_property("value", previous_generated_value, default_attributes); - - if (m_done) { - result->define_direct_property("done", Value(true), default_attributes); - return result; + auto* bytecode_interpreter = Bytecode::Interpreter::current(); + OwnPtr temp_bc_interpreter; + if (!bytecode_interpreter) { + temp_bc_interpreter = make(realm); + bytecode_interpreter = temp_bc_interpreter.ptr(); } + VERIFY(bytecode_interpreter); - // Extract the continuation - auto next_block = TRY(generated_continuation(m_previous_value)); + auto const* next_block = generated_continuation(m_previous_value); - if (!next_block) { - // The generator has terminated, now we can simply return done=true. - m_done = true; - result->define_direct_property("done", Value(true), default_attributes); - return result; - } + // We should never enter `execute` again after the generator is complete. + VERIFY(next_block); - // Make sure it's an actual block VERIFY(!m_generating_function->bytecode_executable()->basic_blocks.find_if([next_block](auto& block) { return block == next_block; }).is_end()); - // Temporarily switch to the captured execution context - TRY(vm.push_execution_context(m_execution_context, {})); - - // Pretend that 'yield' returned the passed value, or threw - if (value_to_throw.has_value()) { - bytecode_interpreter->accumulator() = js_undefined(); - return throw_completion(value_to_throw.release_value()); - } - Bytecode::RegisterWindow* frame = nullptr; if (m_frame.has_value()) frame = &m_frame.value(); - auto next_value = next_argument.value_or(js_undefined()); if (frame) - frame->registers[0] = next_value; + frame->registers[0] = completion_object; else - bytecode_interpreter->accumulator() = next_value; + bytecode_interpreter->accumulator() = completion_object; auto next_result = bytecode_interpreter->run_and_return_frame(*m_generating_function->bytecode_executable(), next_block, frame); @@ -120,12 +133,117 @@ ThrowCompletionOr GeneratorObject::next_impl(VM& vm, Optional next if (!m_frame.has_value()) m_frame = move(*next_result.frame); - m_previous_value = TRY(move(next_result.value)); - m_done = TRY(generated_continuation(m_previous_value)) == nullptr; + auto result_value = move(next_result.value); + if (result_value.is_throw_completion()) { + // Uncaught exceptions disable the generator. + m_generator_state = GeneratorState::Completed; + return result_value; + } + m_previous_value = result_value.release_value(); + bool done = generated_continuation(m_previous_value) == nullptr; - result->define_direct_property("value", TRY(generated_value(m_previous_value)), default_attributes); - result->define_direct_property("done", Value(m_done), default_attributes); + m_generator_state = done ? GeneratorState::Completed : GeneratorState::SuspendedYield; + return create_iterator_result_object(vm, generated_value(m_previous_value), done); +} +// 27.5.3.3 GeneratorResume ( generator, value, generatorBrand ), https://tc39.es/ecma262/#sec-generatorresume +ThrowCompletionOr GeneratorObject::resume(VM& vm, Value value, Optional generator_brand) +{ + // 1. Let state be ? GeneratorValidate(generator, generatorBrand). + auto state = TRY(validate(vm, generator_brand)); + + // 2. If state is completed, return CreateIterResultObject(undefined, true). + if (state == GeneratorState::Completed) + return create_iterator_result_object(vm, js_undefined(), true); + + // 3. Assert: state is either suspendedStart or suspendedYield. + VERIFY(state == GeneratorState::SuspendedStart || state == GeneratorState::SuspendedYield); + + // 4. Let genContext be generator.[[GeneratorContext]]. + auto& generator_context = m_execution_context; + + // 5. Let methodContext be the running execution context. + auto const& method_context = vm.running_execution_context(); + + // FIXME: 6. Suspend methodContext. + + // 8. Push genContext onto the execution context stack; genContext is now the running execution context. + // NOTE: This is done out of order as to not permanently disable the generator if push_execution_context throws, + // as `resume` will immediately throw when [[GeneratorState]] is "executing", never allowing the state to change. + TRY(vm.push_execution_context(generator_context, {})); + + // 7. Set generator.[[GeneratorState]] to executing. + m_generator_state = GeneratorState::Executing; + + // 9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it. Let result be the value returned by the resumed computation. + auto result = execute(vm, normal_completion(value)); + + // 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. + VERIFY(&vm.running_execution_context() == &method_context); + + // 11. Return ? result. + return result; +} + +// 27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ), https://tc39.es/ecma262/#sec-generatorresumeabrupt +ThrowCompletionOr GeneratorObject::resume_abrupt(JS::VM& vm, JS::Completion abrupt_completion, Optional generator_brand) +{ + // Not part of the spec, but the spec assumes abruptCompletion.[[Value]] is not empty. + VERIFY(abrupt_completion.value().has_value()); + + // 1. Let state be ? GeneratorValidate(generator, generatorBrand). + auto state = TRY(validate(vm, generator_brand)); + + // 2. If state is suspendedStart, then + if (state == GeneratorState::SuspendedStart) { + // a. Set generator.[[GeneratorState]] to completed. + m_generator_state = GeneratorState::Completed; + + // b. Once a generator enters the completed state it never leaves it and its associated execution context is never resumed. Any execution state associated with generator can be discarded at this point. + // We don't currently discard anything. + + // c. Set state to completed. + state = GeneratorState::Completed; + } + + // 3. If state is completed, then + if (state == GeneratorState::Completed) { + // a. If abruptCompletion.[[Type]] is return, then + if (abrupt_completion.type() == Completion::Type::Return) { + // i. Return CreateIterResultObject(abruptCompletion.[[Value]], true). + return create_iterator_result_object(vm, abrupt_completion.value().value(), true); + } + + // b. Return ? abruptCompletion. + return abrupt_completion; + } + + // 4. Assert: state is suspendedYield. + VERIFY(state == GeneratorState::SuspendedYield); + + // 5. Let genContext be generator.[[GeneratorContext]]. + auto& generator_context = m_execution_context; + + // 6. Let methodContext be the running execution context. + auto const& method_context = vm.running_execution_context(); + + // FIXME: 7. Suspend methodContext. + + // 9. Push genContext onto the execution context stack; genContext is now the running execution context. + // NOTE: This is done out of order as to not permanently disable the generator if push_execution_context throws, + // as `resume_abrupt` will immediately throw when [[GeneratorState]] is "executing", never allowing the state to change. + TRY(vm.push_execution_context(generator_context, {})); + + // 8. Set generator.[[GeneratorState]] to executing. + m_generator_state = GeneratorState::Executing; + + // 10. Resume the suspended evaluation of genContext using abruptCompletion as the result of the operation that suspended it. Let result be the Completion Record returned by the resumed computation. + auto result = execute(vm, abrupt_completion); + + // 11. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. + VERIFY(&vm.running_execution_context() == &method_context); + + // 12. Return ? result. return result; } diff --git a/Userland/Libraries/LibJS/Runtime/GeneratorObject.h b/Userland/Libraries/LibJS/Runtime/GeneratorObject.h index 3beb777ce1..9dd09762ba 100644 --- a/Userland/Libraries/LibJS/Runtime/GeneratorObject.h +++ b/Userland/Libraries/LibJS/Runtime/GeneratorObject.h @@ -21,17 +21,28 @@ public: virtual ~GeneratorObject() override = default; void visit_edges(Cell::Visitor&) override; - ThrowCompletionOr next_impl(VM&, Optional next_argument, Optional value_to_throw); - void set_done() { m_done = true; } + ThrowCompletionOr resume(VM&, Value value, Optional generator_brand); + ThrowCompletionOr resume_abrupt(VM&, JS::Completion abrupt_completion, Optional generator_brand); private: GeneratorObject(Realm&, Object& prototype, ExecutionContext); + enum class GeneratorState { + SuspendedStart, + SuspendedYield, + Executing, + Completed, + }; + + ThrowCompletionOr validate(VM&, Optional const& generator_brand); + ThrowCompletionOr execute(VM&, JS::Completion const& completion); + ExecutionContext m_execution_context; ECMAScriptFunctionObject* m_generating_function { nullptr }; Value m_previous_value; Optional m_frame; - bool m_done { false }; + GeneratorState m_generator_state { GeneratorState::SuspendedStart }; + Optional m_generator_brand; }; } diff --git a/Userland/Libraries/LibJS/Runtime/GeneratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/GeneratorPrototype.cpp index d6040de060..52ed345200 100644 --- a/Userland/Libraries/LibJS/Runtime/GeneratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/GeneratorPrototype.cpp @@ -30,23 +30,35 @@ void GeneratorPrototype::initialize(Realm& realm) // 27.5.1.2 Generator.prototype.next ( value ), https://tc39.es/ecma262/#sec-generator.prototype.next JS_DEFINE_NATIVE_FUNCTION(GeneratorPrototype::next) { + // 1. Return ? GeneratorResume(this value, value, empty). auto* generator_object = TRY(typed_this_object(vm)); - return generator_object->next_impl(vm, vm.argument(0), {}); + return generator_object->resume(vm, vm.argument(0), {}); } // 27.5.1.3 Generator.prototype.next ( value ), https://tc39.es/ecma262/#sec-generator.prototype.return JS_DEFINE_NATIVE_FUNCTION(GeneratorPrototype::return_) { + // 1. Let g be the this value. auto* generator_object = TRY(typed_this_object(vm)); - generator_object->set_done(); - return generator_object->next_impl(vm, {}, {}); + + // 2. Let C be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. + auto completion = Completion(Completion::Type::Return, vm.argument(0), {}); + + // 3. Return ? GeneratorResumeAbrupt(g, C, empty). + return generator_object->resume_abrupt(vm, completion, {}); } // 27.5.1.4 Generator.prototype.next ( value ), https://tc39.es/ecma262/#sec-generator.prototype.throw JS_DEFINE_NATIVE_FUNCTION(GeneratorPrototype::throw_) { + // 1. Let g be the this value. auto* generator_object = TRY(typed_this_object(vm)); - return generator_object->next_impl(vm, {}, vm.argument(0)); + + // 2. Let C be ThrowCompletion(exception). + auto completion = throw_completion(vm.argument(0)); + + // 3. Return ? GeneratorResumeAbrupt(g, C, empty). + return generator_object->resume_abrupt(vm, completion, {}); } }