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, {}); } }