diff --git a/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp b/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp index c2d42484645..01508440ae1 100644 --- a/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp +++ b/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp @@ -1587,6 +1587,78 @@ Bytecode::CodeGenerationErrorOr ReturnStatement::generate_bytecode(Bytecod return {}; } +static void get_received_completion_type_and_value(Bytecode::Generator& generator, Bytecode::Register received_completion_register, Bytecode::Register received_completion_type_register, Bytecode::Register received_completion_value_register, Bytecode::IdentifierTableIndex type_identifier, Bytecode::IdentifierTableIndex value_identifier) +{ + // The accumulator is set to an object, for example: { "type": 1 (normal), value: 1337 } + generator.emit(received_completion_register); + + generator.emit_get_by_id(type_identifier); + generator.emit(received_completion_type_register); + + generator.emit(received_completion_register); + generator.emit_get_by_id(value_identifier); + generator.emit(received_completion_value_register); +} + +static void generate_await(Bytecode::Generator& generator, Bytecode::Register received_completion_register, Bytecode::Register received_completion_type_register, Bytecode::Register received_completion_value_register, Bytecode::IdentifierTableIndex type_identifier, Bytecode::IdentifierTableIndex value_identifier); + +enum class AwaitBeforeYield { + No, + Yes, +}; + +static void generate_yield(Bytecode::Generator& generator, Bytecode::Label continuation_label, Bytecode::Register received_completion_register, Bytecode::Register received_completion_type_register, Bytecode::Register received_completion_value_register, Bytecode::IdentifierTableIndex type_identifier, Bytecode::IdentifierTableIndex value_identifier, AwaitBeforeYield await_before_yield) +{ + if (!generator.is_in_async_generator_function()) { + generator.emit(Bytecode::Label { continuation_label }); + return; + } + + if (await_before_yield == AwaitBeforeYield::Yes) + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); + + auto& unwrap_yield_resumption_block = generator.make_block(); + generator.emit(Bytecode::Label { unwrap_yield_resumption_block }); + generator.switch_to_basic_block(unwrap_yield_resumption_block); + get_received_completion_type_and_value(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); + + // 27.6.3.7 AsyncGeneratorUnwrapYieldResumption ( resumptionValue ), https://tc39.es/ecma262/#sec-asyncgeneratorunwrapyieldresumption + // 1. If resumptionValue.[[Type]] is not return, return ? resumptionValue. + auto& load_completion_and_jump_to_continuation_label_block = generator.make_block(); + auto& resumption_value_type_is_return_block = generator.make_block(); + generator.emit(Value(to_underlying(Completion::Type::Return))); + generator.emit(received_completion_type_register); + generator.emit( + Bytecode::Label { load_completion_and_jump_to_continuation_label_block }, + Bytecode::Label { resumption_value_type_is_return_block }); + + generator.switch_to_basic_block(resumption_value_type_is_return_block); + + // 2. Let awaited be Completion(Await(resumptionValue.[[Value]])). + generator.emit(received_completion_value_register); + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); + + // 3. If awaited.[[Type]] is throw, return ? awaited. + auto& awaited_type_is_normal_block = generator.make_block(); + generator.emit(Value(to_underlying(Completion::Type::Throw))); + generator.emit(received_completion_type_register); + generator.emit( + Bytecode::Label { load_completion_and_jump_to_continuation_label_block }, + Bytecode::Label { awaited_type_is_normal_block }); + + // 4. Assert: awaited.[[Type]] is normal. + generator.switch_to_basic_block(awaited_type_is_normal_block); + + // 5. Return Completion Record { [[Type]]: return, [[Value]]: awaited.[[Value]], [[Target]]: empty }. + generator.emit(Value(to_underlying(Completion::Type::Return))); + generator.emit(received_completion_register, type_identifier); + generator.emit(Bytecode::Label { load_completion_and_jump_to_continuation_label_block }); + + generator.switch_to_basic_block(load_completion_and_jump_to_continuation_label_block); + generator.emit(received_completion_register); + generator.emit(continuation_label); +} + Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecode::Generator& generator) const { VERIFY(generator.is_in_generator_function()); @@ -1598,21 +1670,10 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod 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_get_by_id(type_identifier); - generator.emit(received_completion_type_register); - - generator.emit(received_completion_register); - generator.emit_get_by_id(value_identifier); - generator.emit(received_completion_value_register); - }; - if (m_is_yield_from) { // 15.5.5 Runtime Semantics: Evaluation, https://tc39.es/ecma262/#sec-generator-function-definitions-runtime-semantics-evaluation - // FIXME: 1. Let generatorKind be GetGeneratorKind(). + // 1. Let generatorKind be GetGeneratorKind(). + // NOTE: is_in_async_generator_function differentiates the generator kind. // 2. Let exprRef be ? Evaluation of AssignmentExpression. // 3. Let value be ? GetValue(exprRef). @@ -1620,9 +1681,9 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod TRY(m_argument->generate_bytecode(generator)); // 4. Let iteratorRecord be ? GetIterator(value, generatorKind). - // FIXME: Consider generatorKind. auto iterator_record_register = generator.allocate_register(); - generator.emit(); + auto iterator_hint = generator.is_in_async_generator_function() ? IteratorHint::Async : IteratorHint::Sync; + generator.emit(iterator_hint); generator.emit(iterator_record_register); // 5. Let iterator be iteratorRecord.[[Iterator]]. @@ -1670,7 +1731,9 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.emit_with_extra_register_slots(2, AK::Array { received_completion_value_register, received_completion_value_register }); generator.emit(Bytecode::Op::CallType::Call, next_method_register, iterator_register); - // FIXME: ii. If generatorKind is async, set innerResult to ? Await(innerResult). + // ii. If generatorKind is async, set innerResult to ? Await(innerResult). + if (generator.is_in_async_generator_function()) + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); // iii. If innerResult is not an Object, throw a TypeError exception. generator.emit(); @@ -1697,15 +1760,15 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.switch_to_basic_block(type_is_normal_not_done_block); - // FIXME: vi. If generatorKind is async, set received to Completion(AsyncGeneratorYield(? IteratorValue(innerResult))). + // vi. If generatorKind is async, set received to Completion(AsyncGeneratorYield(? IteratorValue(innerResult))). // vii. Else, set received to Completion(GeneratorYield(innerResult)). - // FIXME: Else, generator.emit(inner_result_register); // FIXME: Yield currently only accepts a Value, not an object conforming to the IteratorResult interface, so we have to do an observable lookup of `value` here. + // This only matters for non-async generators. generator.emit(); - generator.emit(Bytecode::Label { continuation_block }); + generate_yield(generator, Bytecode::Label { continuation_block }, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier, AwaitBeforeYield::No); // b. Else if received.[[Type]] is throw, then generator.switch_to_basic_block(is_type_throw_block); @@ -1742,7 +1805,9 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.emit_with_extra_register_slots(2, AK::Array { received_completion_value_register, received_completion_value_register }); generator.emit(Bytecode::Op::CallType::Call, throw_method_register, iterator_register); - // FIXME: 2. If generatorKind is async, set innerResult to ? Await(innerResult). + // 2. If generatorKind is async, set innerResult to ? Await(innerResult). + if (generator.is_in_async_generator_function()) + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); // 3. NOTE: Exceptions from the inner iterator throw method are propagated. Normal completions from an inner throw method are processed similarly to an inner next. // 4. If innerResult is not an Object, throw a TypeError exception. @@ -1768,26 +1833,31 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.switch_to_basic_block(type_is_throw_not_done_block); - // FIXME: 7. If generatorKind is async, set received to Completion(AsyncGeneratorYield(? IteratorValue(innerResult))). + // 7. If generatorKind is async, set received to Completion(AsyncGeneratorYield(? IteratorValue(innerResult))). // 8. Else, set received to Completion(GeneratorYield(innerResult)). - // FIXME: Else, generator.emit(inner_result_register); // FIXME: Yield currently only accepts a Value, not an object conforming to the IteratorResult interface, so we have to do an observable lookup of `value` here. + // This only matters for non-async generators. generator.emit(); - generator.emit(Bytecode::Label { continuation_block }); + generate_yield(generator, Bytecode::Label { continuation_block }, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier, AwaitBeforeYield::No); generator.switch_to_basic_block(throw_method_is_undefined_block); // 1. NOTE: If iterator does not have a throw method, this throw is going to terminate the yield* loop. But first we need to give iterator a chance to clean up. // 2. Let closeCompletion be Completion Record { [[Type]]: normal, [[Value]]: empty, [[Target]]: empty }. - // FIXME: 3. If generatorKind is async, perform ? AsyncIteratorClose(iteratorRecord, closeCompletion). - // 4. Else, perform ? IteratorClose(iteratorRecord, closeCompletion). - // FIXME: Else, + // 3. If generatorKind is async, perform ? AsyncIteratorClose(iteratorRecord, closeCompletion). generator.emit(iterator_record_register); - generator.emit(Completion::Type::Normal, Optional {}); + if (generator.is_in_async_generator_function()) { + // FIXME: This performs `await` outside of the generator! + generator.emit(Completion::Type::Normal, Optional {}); + } + // 4. Else, perform ? IteratorClose(iteratorRecord, closeCompletion). + else { + generator.emit(Completion::Type::Normal, Optional {}); + } // 5. NOTE: The next step throws a TypeError to indicate that there was a yield* protocol violation: iterator does not have a throw method. // 6. Throw a TypeError exception. @@ -1817,10 +1887,13 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.switch_to_basic_block(return_is_undefined_block); - // FIXME: 1. If generatorKind is async, set received.[[Value]] to ? Await(received.[[Value]]). + // 1. If generatorKind is async, set received.[[Value]] to ? Await(received.[[Value]]). + generator.emit(received_completion_value_register); + if (generator.is_in_async_generator_function()) + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); + // 2. Return ? received. // NOTE: This will always be a return completion. - generator.emit(received_completion_value_register); generator.perform_needed_unwinds(); generator.emit(nullptr); @@ -1830,7 +1903,9 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.emit_with_extra_register_slots(2, AK::Array { received_completion_value_register, received_completion_value_register }); generator.emit(Bytecode::Op::CallType::Call, return_method_register, iterator_register); - // FIXME: v. If generatorKind is async, set innerReturnResult to ? Await(innerReturnResult). + // v. If generatorKind is async, set innerReturnResult to ? Await(innerReturnResult). + if (generator.is_in_async_generator_function()) + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); // vi. If innerReturnResult is not an Object, throw a TypeError exception. generator.emit(); @@ -1860,18 +1935,18 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.switch_to_basic_block(type_is_return_not_done_block); - // FIXME: ix. If generatorKind is async, set received to Completion(AsyncGeneratorYield(? IteratorValue(innerReturnResult))). + // ix. If generatorKind is async, set received to Completion(AsyncGeneratorYield(? IteratorValue(innerReturnResult))). // x. Else, set received to Completion(GeneratorYield(innerReturnResult)). - // FIXME: Else, generator.emit(inner_return_result_register); // FIXME: Yield currently only accepts a Value, not an object conforming to the IteratorResult interface, so we have to do an observable lookup of `value` here. + // This only matters for non-async generators. generator.emit(); - generator.emit(Bytecode::Label { continuation_block }); + generate_yield(generator, Bytecode::Label { continuation_block }, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier, AwaitBeforeYield::No); generator.switch_to_basic_block(continuation_block); - get_received_completion_type_and_value(); + get_received_completion_type_and_value(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); generator.emit(Bytecode::Label { loop_block }); generator.switch_to_basic_block(loop_end_block); @@ -1884,9 +1959,9 @@ Bytecode::CodeGenerationErrorOr YieldExpression::generate_bytecode(Bytecod generator.emit(js_undefined()); auto& continuation_block = generator.make_block(); - generator.emit(Bytecode::Label { continuation_block }); + generate_yield(generator, Bytecode::Label { continuation_block }, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier, AwaitBeforeYield::Yes); generator.switch_to_basic_block(continuation_block); - get_received_completion_type_and_value(); + get_received_completion_type_and_value(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); auto& normal_completion_continuation_block = generator.make_block(); auto& throw_completion_continuation_block = generator.make_block(); @@ -2419,32 +2494,15 @@ Bytecode::CodeGenerationErrorOr ThisExpression::generate_bytecode(Bytecode return {}; } -static void generate_await(Bytecode::Generator& generator) +static void generate_await(Bytecode::Generator& generator, Bytecode::Register received_completion_register, Bytecode::Register received_completion_type_register, Bytecode::Register received_completion_value_register, Bytecode::IdentifierTableIndex type_identifier, Bytecode::IdentifierTableIndex value_identifier) { VERIFY(generator.is_in_async_function()); - // Transform `await expr` to `yield expr`, see AsyncFunctionDriverWrapper - // For that we just need to copy most of the code from YieldExpression - 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& continuation_block = generator.make_block(); - generator.emit(Bytecode::Label { continuation_block }); + generator.emit(Bytecode::Label { continuation_block }); generator.switch_to_basic_block(continuation_block); - // The accumulator is set to an object, for example: { "type": 1 (normal), value: 1337 } - generator.emit(received_completion_register); - - generator.emit_get_by_id(type_identifier); - generator.emit(received_completion_type_register); - - generator.emit(received_completion_register); - generator.emit_get_by_id(value_identifier); - generator.emit(received_completion_value_register); + get_received_completion_type_and_value(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); auto& normal_completion_continuation_block = generator.make_block(); auto& throw_value_block = generator.make_block(); @@ -2455,7 +2513,7 @@ static void generate_await(Bytecode::Generator& generator) Bytecode::Label { normal_completion_continuation_block }, Bytecode::Label { throw_value_block }); - // Simplification: The only abrupt completion we receive from AsyncFunctionDriverWrapper is Type::Throw + // Simplification: The only abrupt completion we receive from AsyncFunctionDriverWrapper or AsyncGenerator is Type::Throw // So we do not need to account for the Type::Return path generator.switch_to_basic_block(throw_value_block); generator.emit(received_completion_value_register); @@ -2469,7 +2527,15 @@ static void generate_await(Bytecode::Generator& generator) Bytecode::CodeGenerationErrorOr AwaitExpression::generate_bytecode(Bytecode::Generator& generator) const { TRY(m_argument->generate_bytecode(generator)); - generate_await(generator); + + 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"); + + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); return {}; } @@ -2651,8 +2717,16 @@ static Bytecode::CodeGenerationErrorOr for_in_of_body_evaluation(Bytecode: generator.emit(); // b. If iteratorKind is async, set nextResult to ? Await(nextResult). - if (iterator_kind == IteratorHint::Async) - generate_await(generator); + if (iterator_kind == IteratorHint::Async) { + 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"); + + generate_await(generator, received_completion_register, received_completion_type_register, received_completion_value_register, type_identifier, value_identifier); + } // c. If Type(nextResult) is not Object, throw a TypeError exception. generator.emit(); diff --git a/Userland/Libraries/LibJS/Runtime/AsyncGenerator.cpp b/Userland/Libraries/LibJS/Runtime/AsyncGenerator.cpp index 20c023afda7..6c0edd2385c 100644 --- a/Userland/Libraries/LibJS/Runtime/AsyncGenerator.cpp +++ b/Userland/Libraries/LibJS/Runtime/AsyncGenerator.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Linus Groh + * Copyright (c) 2023, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,12 +8,28 @@ #include #include #include +#include #include +#include namespace JS { -AsyncGenerator::AsyncGenerator(Object& prototype) +ThrowCompletionOr> AsyncGenerator::create(Realm& realm, Value initial_value, ECMAScriptFunctionObject* generating_function, ExecutionContext execution_context, Bytecode::RegisterWindow frame) +{ + auto& vm = realm.vm(); + // This is "g1.prototype" in figure-2 (https://tc39.es/ecma262/img/figure-2.png) + auto generating_function_prototype = TRY(generating_function->get(vm.names.prototype)); + auto generating_function_prototype_object = TRY(generating_function_prototype.to_object(vm)); + auto object = MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, generating_function_prototype_object, move(execution_context))); + object->m_generating_function = generating_function; + object->m_frame = move(frame); + object->m_previous_value = initial_value; + return object; +} + +AsyncGenerator::AsyncGenerator(Realm&, Object& prototype, ExecutionContext context) : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_async_generator_context(move(context)) { } @@ -24,6 +41,496 @@ void AsyncGenerator::visit_edges(Cell::Visitor& visitor) visitor.visit(*request.completion.value()); visitor.visit(request.capability); } + m_async_generator_context.visit_edges(visitor); + visitor.visit(m_generating_function); + visitor.visit(m_previous_value); + if (m_frame.has_value()) + m_frame->visit_edges(visitor); + visitor.visit(m_current_promise); +} + +// 27.6.3.4 AsyncGeneratorEnqueue ( generator, completion, promiseCapability ), https://tc39.es/ecma262/#sec-asyncgeneratorenqueue +void AsyncGenerator::async_generator_enqueue(Completion completion, NonnullGCPtr promise_capability) +{ + // 1. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }. + auto request = AsyncGeneratorRequest { .completion = move(completion), .capability = promise_capability }; + + // 2. Append request to generator.[[AsyncGeneratorQueue]]. + m_async_generator_queue.append(move(request)); + + // 3. Return unused. +} + +void AsyncGenerator::set_async_generator_state(Badge, AsyncGenerator::State value) +{ + m_async_generator_state = value; +} + +// 27.7.5.3 Await ( value ), https://tc39.es/ecma262/#await +ThrowCompletionOr AsyncGenerator::await(Value value) +{ + auto& vm = this->vm(); + auto& realm = *vm.current_realm(); + + // 1. Let asyncContext be the running execution context. + auto& async_context = vm.running_execution_context(); + + // 2. Let promise be ? PromiseResolve(%Promise%, value). + auto* promise_object = TRY(promise_resolve(vm, realm.intrinsics().promise_constructor(), value)); + + // 3. Let fulfilledClosure be a new Abstract Closure with parameters (v) that captures asyncContext and performs the + // following steps when called: + auto fulfilled_closure = [this, &async_context](VM& vm) -> ThrowCompletionOr { + auto value = vm.argument(0); + + // a. Let prevContext be the running execution context. + auto& prev_context = vm.running_execution_context(); + + // FIXME: b. Suspend prevContext. + + // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. + TRY(vm.push_execution_context(async_context, {})); + + // d. Resume the suspended evaluation of asyncContext using NormalCompletion(v) as the result of the operation that + // suspended it. + execute(vm, normal_completion(value)); + + // e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and + // prevContext is the currently running execution context. + VERIFY(&vm.running_execution_context() == &prev_context); + + // f. Return undefined. + return js_undefined(); + }; + + // 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). + auto on_fulfilled = NativeFunction::create(realm, move(fulfilled_closure), 1, ""); + + // 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the + // following steps when called: + auto rejected_closure = [this, &async_context](VM& vm) -> ThrowCompletionOr { + auto reason = vm.argument(0); + + // a. Let prevContext be the running execution context. + auto& prev_context = vm.running_execution_context(); + + // FIXME: b. Suspend prevContext. + + // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. + TRY(vm.push_execution_context(async_context, {})); + + // d. Resume the suspended evaluation of asyncContext using ThrowCompletion(reason) as the result of the operation that + // suspended it. + execute(vm, throw_completion(reason)); + + // e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and + // prevContext is the currently running execution context. + VERIFY(&vm.running_execution_context() == &prev_context); + + // f. Return undefined. + return js_undefined(); + }; + + // 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »). + auto on_rejected = NativeFunction::create(realm, move(rejected_closure), 1, ""); + + // 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected). + m_current_promise = verify_cast(promise_object); + m_current_promise->perform_then(on_fulfilled, on_rejected, {}); + + // 8. Remove asyncContext 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(); + + // NOTE: None of these are necessary. 10-12 are handled by step d of the above lambdas. + // 9. Let callerContext be the running execution context. + // 10. Resume callerContext passing empty. If asyncContext is ever resumed again, let completion be the Completion Record with which it is resumed. + // 11. Assert: If control reaches here, then asyncContext is the running execution context again. + // 12. Return completion. + return {}; +} + +void AsyncGenerator::execute(VM& vm, Completion completion) +{ + while (true) { + // Loosely based on step 4 of https://tc39.es/ecma262/#sec-asyncgeneratorstart + VERIFY(completion.value().has_value()); + + auto generated_value = [](Value value) -> Value { + if (value.is_object()) + return value.as_object().get_without_side_effects("result"); + return value.is_empty() ? js_undefined() : value; + }; + + auto generated_continuation = [&](Value value) -> Bytecode::BasicBlock const* { + if (value.is_object()) { + auto number_value = value.as_object().get_without_side_effects("continuation"); + return reinterpret_cast(static_cast(number_value.as_double())); + } + return nullptr; + }; + + auto generated_is_await = [](Value value) -> bool { + if (value.is_object()) + return value.as_object().get_without_side_effects("isAwait").as_bool(); + return false; + }; + + 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& 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); + + VERIFY(!m_generating_function->bytecode_executable()->basic_blocks.find_if([next_block](auto& block) { return block == next_block; }).is_end()); + + Bytecode::RegisterWindow* frame = nullptr; + if (m_frame.has_value()) + frame = &m_frame.value(); + + if (frame) + frame->registers[0] = completion_object; + else + bytecode_interpreter.accumulator() = completion_object; + + auto next_result = bytecode_interpreter.run_and_return_frame(realm, *m_generating_function->bytecode_executable(), next_block, frame); + + if (!m_frame.has_value()) + m_frame = move(*next_result.frame); + + auto result_value = move(next_result.value); + 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); + + if (is_await) { + auto await_result = this->await(value); + if (await_result.is_throw_completion()) { + completion = await_result.release_error(); + continue; + } + return; + } + } + + bool done = result_value.is_throw_completion() || generated_continuation(m_previous_value) == nullptr; + if (!done) { + // 27.6.3.8 AsyncGeneratorYield ( value ), https://tc39.es/ecma262/#sec-asyncgeneratoryield + // 1. Let genContext be the running execution context. + // 2. Assert: genContext is the execution context of a generator. + // 3. Let generator be the value of the Generator component of genContext. + // 4. Assert: GetGeneratorKind() is async. + // 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. + VERIFY(vm.execution_context_stack().size() >= 2); + + // 7. Let previousContext be the second to top element of the execution context stack. + auto& previous_context = vm.execution_context_stack().at(vm.execution_context_stack().size() - 2); + + // 8. Let previousRealm be previousContext's Realm. + auto previous_realm = previous_context->realm; + + // 9. Perform AsyncGeneratorCompleteStep(generator, completion, false, previousRealm). + complete_step(yield_completion, false, previous_realm.ptr()); + + // 10. Let queue be generator.[[AsyncGeneratorQueue]]. + auto& queue = m_async_generator_queue; + + // 11. If queue is not empty, then + if (!queue.is_empty()) { + // a. NOTE: Execution continues without suspending the generator. + // b. Let toYield be the first element of queue. + auto& to_yield = queue.first(); + + // c. Let resumptionValue be Completion(toYield.[[Completion]]). + completion = Completion(to_yield.completion); + + // d. Return ? AsyncGeneratorUnwrapYieldResumption(resumptionValue). + // NOTE: AsyncGeneratorUnwrapYieldResumption is performed inside the continuation block inside the generator, + // so we just need to enter the generator again. + continue; + } + // 12. Else, + else { + // a. Set generator.[[AsyncGeneratorState]] to suspendedYield. + m_async_generator_state = State::SuspendedYield; + + // b. Remove genContext 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(); + + // c. Let callerContext be the running execution context. + // d. Resume callerContext passing undefined. If genContext is ever resumed again, let resumptionValue be the Completion Record with which it is resumed. + // e. Assert: If control reaches here, then genContext is the running execution context again. + // f. Return ? AsyncGeneratorUnwrapYieldResumption(resumptionValue). + // NOTE: e-f are performed whenever someone calls `execute` again. + return; + } + } + + // 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(); + + // 4.g. Set acGenerator.[[AsyncGeneratorState]] to completed. + m_async_generator_state = State::Completed; + + // 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); + + // 4.k. Perform AsyncGeneratorDrainQueue(acGenerator). + drain_queue(); + + // 4.l. Return undefined. + return; + } +} + +// 27.6.3.6 AsyncGeneratorResume ( generator, completion ), https://tc39.es/ecma262/#sec-asyncgeneratorresume +ThrowCompletionOr AsyncGenerator::resume(VM& vm, Completion completion) +{ + // 1. Assert: generator.[[AsyncGeneratorState]] is either suspendedStart or suspendedYield. + VERIFY(m_async_generator_state == State::SuspendedStart || m_async_generator_state == State::SuspendedYield); + + // 2. Let genContext be generator.[[AsyncGeneratorContext]]. + auto& generator_context = m_async_generator_context; + + // 3. Let callerContext be the running execution context. + auto const& caller_context = vm.running_execution_context(); + + // FIXME: 4. Suspend callerContext. + + // 5. Set generator.[[AsyncGeneratorState]] to executing. + m_async_generator_state = State::Executing; + + // 6. Push genContext onto the execution context stack; genContext is now the running execution context. + TRY(vm.push_execution_context(generator_context, {})); + + // 7. Resume the suspended evaluation of genContext using completion as the result of the operation that suspended + // it. Let result be the Completion Record returned by the resumed computation. + // 8. Assert: result is never an abrupt completion. + execute(vm, completion); + + // 9. Assert: When we return here, genContext has already been removed from the execution context stack and + // callerContext is the currently running execution context. + VERIFY(&vm.running_execution_context() == &caller_context); + + // 10. Return unused. + return {}; +} + +// 27.6.3.9 AsyncGeneratorAwaitReturn ( generator ), https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn +ThrowCompletionOr AsyncGenerator::await_return() +{ + auto& vm = this->vm(); + auto& realm = *vm.current_realm(); + + // 1. Let queue be generator.[[AsyncGeneratorQueue]]. + auto& queue = m_async_generator_queue; + + // 2. Assert: queue is not empty. + VERIFY(!queue.is_empty()); + + // 3. Let next be the first element of queue. + auto& next = m_async_generator_queue.first(); + + // 4. Let completion be Completion(next.[[Completion]]). + auto completion = Completion(next.completion); + + // 5. Assert: completion.[[Type]] is return. + VERIFY(completion.type() == Completion::Type::Return); + + // 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). + auto* promise = TRY(promise_resolve(vm, realm.intrinsics().promise_constructor(), completion.value().value())); + + // 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs + // the following steps when called: + auto fulfilled_closure = [this](VM& vm) -> ThrowCompletionOr { + // a. Set generator.[[AsyncGeneratorState]] to completed. + m_async_generator_state = State::Completed; + + // b. Let result be NormalCompletion(value). + auto result = normal_completion(vm.argument(0)); + + // c. Perform AsyncGeneratorCompleteStep(generator, result, true). + complete_step(result, true); + + // d. Perform AsyncGeneratorDrainQueue(generator). + drain_queue(); + + // e. Return undefined. + return js_undefined(); + }; + + // 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). + auto on_fulfilled = NativeFunction::create(realm, move(fulfilled_closure), 1, ""); + + // 9. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures generator and performs + // the following steps when called: + auto rejected_closure = [this](VM& vm) -> ThrowCompletionOr { + // a. Set generator.[[AsyncGeneratorState]] to completed. + m_async_generator_state = State::Completed; + + // b. Let result be ThrowCompletion(reason). + auto result = throw_completion(vm.argument(0)); + + // c. Perform AsyncGeneratorCompleteStep(generator, result, true). + complete_step(result, true); + + // d. Perform AsyncGeneratorDrainQueue(generator). + drain_queue(); + + // e. Return undefined. + return js_undefined(); + }; + + // 10. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »). + auto on_rejected = NativeFunction::create(realm, move(rejected_closure), 1, ""); + + // 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected). + // NOTE: await_return should only be called when the generator is in SuspendedStart or Completed state, + // so an await shouldn't be running currently, so it should be safe to overwrite m_current_promise. + m_current_promise = verify_cast(promise); + m_current_promise->perform_then(on_fulfilled, on_rejected, {}); + + // 12. Return unused. + return {}; +} + +// 27.6.3.5 AsyncGeneratorCompleteStep ( generator, completion, done [ , realm ] ), https://tc39.es/ecma262/#sec-asyncgeneratorcompletestep +void AsyncGenerator::complete_step(Completion completion, bool done, Realm* realm) +{ + auto& vm = this->vm(); + + // 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. + VERIFY(!m_async_generator_queue.is_empty()); + + // 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. + // 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. + auto next = m_async_generator_queue.take_first(); + + // 4. Let promiseCapability be next.[[Capability]]. + auto promise_capability = next.capability; + + // 5. Let value be completion.[[Value]]. + auto value = completion.value().value(); + + // 6. If completion.[[Type]] is throw, then + if (completion.type() == Completion::Type::Throw) { + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « value »). + MUST(call(vm, *promise_capability->reject(), js_undefined(), value)); + } + // 7. Else, + else { + // a. Assert: completion.[[Type]] is normal. + VERIFY(completion.type() == Completion::Type::Normal); + + GCPtr iterator_result; + + // b. If realm is present, then + if (realm) { + // i. Let oldRealm be the running execution context's Realm. + auto old_realm = vm.running_execution_context().realm; + + // ii. Set the running execution context's Realm to realm. + vm.running_execution_context().realm = realm; + + // iii. Let iteratorResult be CreateIterResultObject(value, done). + iterator_result = create_iterator_result_object(vm, value, done); + + // iv. Set the running execution context's Realm to oldRealm. + vm.running_execution_context().realm = old_realm; + } + // c. Else, + else { + // i. Let iteratorResult be CreateIterResultObject(value, done). + iterator_result = create_iterator_result_object(vm, value, done); + } + + VERIFY(iterator_result); + + // d. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). + MUST(call(vm, *promise_capability->resolve(), js_undefined(), iterator_result)); + } + + // 8. Return unused. +} + +// 27.6.3.10 AsyncGeneratorDrainQueue ( generator ), https://tc39.es/ecma262/#sec-asyncgeneratordrainqueue +void AsyncGenerator::drain_queue() +{ + // 1. Assert: generator.[[AsyncGeneratorState]] is completed. + VERIFY(m_async_generator_state == State::Completed); + + // 2. Let queue be generator.[[AsyncGeneratorQueue]]. + auto& queue = m_async_generator_queue; + + // 3. If queue is empty, return unused. + if (queue.is_empty()) + return; + + // 4. Let done be false. + bool done = false; + + // 5. Repeat, while done is false, + while (!done) { + // a. Let next be the first element of queue. + auto& next = m_async_generator_queue.first(); + + // b. Let completion be Completion(next.[[Completion]]). + auto completion = Completion(next.completion); + + // c. If completion.[[Type]] is return, then + if (completion.type() == Completion::Type::Return) { + // i. Set generator.[[AsyncGeneratorState]] to awaiting-return. + m_async_generator_state = State::AwaitingReturn; + + // ii. Perform ! AsyncGeneratorAwaitReturn(generator). + MUST(await_return()); + + // iii. Set done to true. + done = true; + } + // d. Else, + else { + // i. If completion.[[Type]] is normal, then + if (completion.type() == Completion::Type::Normal) { + // 1. Set completion to NormalCompletion(undefined). + completion = normal_completion(js_undefined()); + } + + // ii. Perform AsyncGeneratorCompleteStep(generator, completion, true). + complete_step(completion, true); + + // iii. If queue is empty, set done to true. + if (queue.is_empty()) + done = true; + } + } + + // 6. Return unused. } } diff --git a/Userland/Libraries/LibJS/Runtime/AsyncGenerator.h b/Userland/Libraries/LibJS/Runtime/AsyncGenerator.h index f1736158497..7e3544e187f 100644 --- a/Userland/Libraries/LibJS/Runtime/AsyncGenerator.h +++ b/Userland/Libraries/LibJS/Runtime/AsyncGenerator.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Linus Groh + * Copyright (c) 2023, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,6 +8,7 @@ #pragma once #include +#include #include #include @@ -25,21 +27,40 @@ public: Completed, }; + static ThrowCompletionOr> create(Realm&, Value, ECMAScriptFunctionObject*, ExecutionContext, Bytecode::RegisterWindow); + virtual ~AsyncGenerator() override = default; + void async_generator_enqueue(Completion, NonnullGCPtr); + ThrowCompletionOr resume(VM&, Completion completion); + ThrowCompletionOr await_return(); + void complete_step(Completion, bool done, Realm* realm = nullptr); + void drain_queue(); + + State async_generator_state() const { return m_async_generator_state; } + void set_async_generator_state(Badge, State value); + + Optional const& generator_brand() const { return m_generator_brand; } + private: - explicit AsyncGenerator(Object& prototype); + AsyncGenerator(Realm&, Object& prototype, ExecutionContext); virtual void visit_edges(Cell::Visitor&) override; + void execute(VM&, Completion completion); + ThrowCompletionOr await(Value); + // At the time of constructing an AsyncGenerator, we still need to point to an // execution context on the stack, but later need to 'adopt' it. - using ExecutionContextVariant = Variant; + State m_async_generator_state { State::SuspendedStart }; // [[AsyncGeneratorState]] + ExecutionContext m_async_generator_context; // [[AsyncGeneratorContext]] + Vector m_async_generator_queue; // [[AsyncGeneratorQueue]] + Optional m_generator_brand; // [[GeneratorBrand]] - Optional m_async_generator_state; // [[AsyncGeneratorState]] - ExecutionContextVariant m_async_generator_context; // [[AsyncGeneratorContext]] - Vector m_async_generator_queue; // [[AsyncGeneratorQueue]] - Optional m_generator_brand; // [[GeneratorBrand]] + GCPtr m_generating_function; + Value m_previous_value; + Optional m_frame; + GCPtr m_current_promise; }; } diff --git a/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.cpp index 562ee43de77..be33b0b17b6 100644 --- a/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.cpp @@ -1,10 +1,14 @@ /* * Copyright (c) 2022-2023, Linus Groh + * Copyright (c) 2023, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include +#include +#include namespace JS { @@ -18,6 +22,10 @@ ThrowCompletionOr AsyncGeneratorPrototype::initialize(Realm& realm) { auto& vm = this->vm(); MUST_OR_THROW_OOM(Base::initialize(realm)); + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.next, next, 1, attr); + define_native_function(realm, vm.names.return_, return_, 1, attr); + define_native_function(realm, vm.names.throw_, throw_, 1, attr); // 27.6.1.5 AsyncGenerator.prototype [ @@toStringTag ], https://tc39.es/ecma262/#sec-asyncgenerator-prototype-tostringtag define_direct_property(vm.well_known_symbol_to_string_tag(), MUST_OR_THROW_OOM(PrimitiveString::create(vm, "AsyncGenerator"sv)), Attribute::Configurable); @@ -25,4 +33,180 @@ ThrowCompletionOr AsyncGeneratorPrototype::initialize(Realm& realm) return {}; } +// 27.6.3.3 AsyncGeneratorValidate ( generator, generatorBrand ), https://tc39.es/ecma262/#sec-asyncgeneratorvalidate +static ThrowCompletionOr> async_generator_validate(VM& vm, Value generator, Optional generator_brand) +{ + // 1. Perform ? RequireInternalSlot(generator, [[AsyncGeneratorContext]]). + // 2. Perform ? RequireInternalSlot(generator, [[AsyncGeneratorState]]). + // 3. Perform ? RequireInternalSlot(generator, [[AsyncGeneratorQueue]]). + if (!generator.is_object() || !is(generator.as_object())) + return vm.throw_completion(ErrorType::NotAnObjectOfType, "AsyncGenerator"); + + auto& async_generator = static_cast(generator.as_object()); + + // 4. If generator.[[GeneratorBrand]] is not generatorBrand, throw a TypeError exception. + if (async_generator.generator_brand() != generator_brand) + return vm.throw_completion(ErrorType::GeneratorBrandMismatch, async_generator.generator_brand().value_or("emp"_short_string), generator_brand.value_or("emp"_short_string)); + + // 5. Return unused. + return async_generator; +} + +// 27.6.1.2 AsyncGenerator.prototype.next ( value ), https://tc39.es/ecma262/#sec-asyncgenerator-prototype-next +JS_DEFINE_NATIVE_FUNCTION(AsyncGeneratorPrototype::next) +{ + auto& realm = *vm.current_realm(); + + // 1. Let generator be the this value. + auto generator_this_value = vm.this_value(); + + // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). + auto promise_capability = MUST(new_promise_capability(vm, realm.intrinsics().promise_constructor())); + + // 3. Let result be Completion(AsyncGeneratorValidate(generator, empty)). + // 4. IfAbruptRejectPromise(result, promiseCapability). + auto generator = TRY_OR_REJECT(vm, promise_capability, async_generator_validate(vm, generator_this_value, OptionalNone {})); + + // 5. Let state be generator.[[AsyncGeneratorState]]. + auto state = generator->async_generator_state(); + + // 6. If state is completed, then + if (state == AsyncGenerator::State::Completed) { + // a. Let iteratorResult be CreateIterResultObject(undefined, true). + auto iterator_result = create_iterator_result_object(vm, js_undefined(), true); + + // b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). + MUST(call(vm, *promise_capability->resolve(), js_undefined(), iterator_result)); + + // c. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); + } + + // 7. Let completion be NormalCompletion(value). + auto completion = normal_completion(vm.argument(0)); + + // 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). + generator->async_generator_enqueue(completion, promise_capability); + + // 9. If state is either suspendedStart or suspendedYield, then + if (state == AsyncGenerator::State::SuspendedStart || state == AsyncGenerator::State::SuspendedYield) { + // a. Perform AsyncGeneratorResume(generator, completion). + TRY_OR_REJECT(vm, promise_capability, generator->resume(vm, completion)); + } + // 10. Else, + else { + // a. Assert: state is either executing or awaiting-return. + VERIFY(state == AsyncGenerator::State::Executing || state == AsyncGenerator::State::AwaitingReturn); + } + + // 11. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); +} + +// 27.6.1.3 AsyncGenerator.prototype.return ( value ), https://tc39.es/ecma262/#sec-asyncgenerator-prototype-return +JS_DEFINE_NATIVE_FUNCTION(AsyncGeneratorPrototype::return_) +{ + auto& realm = *vm.current_realm(); + + // 1. Let generator be the this value. + auto generator_this_value = vm.this_value(); + + // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). + auto promise_capability = MUST(new_promise_capability(vm, realm.intrinsics().promise_constructor())); + + // 3. Let result be Completion(AsyncGeneratorValidate(generator, empty)). + // 4. IfAbruptRejectPromise(result, promiseCapability). + auto generator = TRY_OR_REJECT(vm, promise_capability, async_generator_validate(vm, generator_this_value, OptionalNone {})); + + // 5. Let completion be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. + auto completion = Completion(Completion::Type::Return, vm.argument(0), {}); + + // 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). + generator->async_generator_enqueue(completion, promise_capability); + + // 7. Let state be generator.[[AsyncGeneratorState]]. + auto state = generator->async_generator_state(); + + // 8. If state is either suspendedStart or completed, then + if (state == AsyncGenerator::State::SuspendedStart || state == AsyncGenerator::State::Completed) { + // a. Set generator.[[AsyncGeneratorState]] to awaiting-return. + generator->set_async_generator_state({}, AsyncGenerator::State::AwaitingReturn); + + // b. Perform ! AsyncGeneratorAwaitReturn(generator). + MUST(generator->await_return()); + } + // 9. Else if state is suspendedYield, then + else if (state == AsyncGenerator::State::SuspendedYield) { + // a. Perform AsyncGeneratorResume(generator, completion). + TRY_OR_REJECT(vm, promise_capability, generator->resume(vm, completion)); + } + // 10. Else, + else { + // a. Assert: state is either executing or awaiting-return. + VERIFY(state == AsyncGenerator::State::Executing || state == AsyncGenerator::State::AwaitingReturn); + } + + // 11. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); +} + +// 27.6.1.4 AsyncGenerator.prototype.throw ( exception ), https://tc39.es/ecma262/#sec-asyncgenerator-prototype-throw +JS_DEFINE_NATIVE_FUNCTION(AsyncGeneratorPrototype::throw_) +{ + auto& realm = *vm.current_realm(); + + auto exception = vm.argument(0); + + // 1. Let generator be the this value. + auto generator_this_value = vm.this_value(); + + // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). + auto promise_capability = MUST(new_promise_capability(vm, realm.intrinsics().promise_constructor())); + + // 3. Let result be Completion(AsyncGeneratorValidate(generator, empty)). + // 4. IfAbruptRejectPromise(result, promiseCapability). + auto generator = TRY_OR_REJECT(vm, promise_capability, async_generator_validate(vm, generator_this_value, OptionalNone {})); + + // 5. Let state be generator.[[AsyncGeneratorState]]. + auto state = generator->async_generator_state(); + + // 6. If state is suspendedStart, then + if (state == AsyncGenerator::State::SuspendedStart) { + // a. Set generator.[[AsyncGeneratorState]] to completed. + generator->set_async_generator_state({}, AsyncGenerator::State::Completed); + + // b. Set state to completed. + state = AsyncGenerator::State::Completed; + } + + // 7. If state is completed, then + if (state == AsyncGenerator::State::Completed) { + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « exception »). + MUST(call(vm, *promise_capability->reject(), js_undefined(), exception)); + + // b. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); + } + + // 8. Let completion be ThrowCompletion(exception). + auto completion = throw_completion(exception); + + // 9. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). + generator->async_generator_enqueue(completion, promise_capability); + + // 10. If state is suspendedYield, then + if (state == AsyncGenerator::State::SuspendedYield) { + // a. Perform AsyncGeneratorResume(generator, completion). + TRY_OR_REJECT(vm, promise_capability, generator->resume(vm, completion)); + } + // 11. Else, + else { + // a. Assert: state is either executing or awaiting-return. + VERIFY(state == AsyncGenerator::State::Executing || state == AsyncGenerator::State::AwaitingReturn); + } + + // 12. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.h b/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.h index 4bdd6dea20e..bef457bb077 100644 --- a/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/AsyncGeneratorPrototype.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Linus Groh + * Copyright (c) 2023, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -20,6 +21,10 @@ public: private: explicit AsyncGeneratorPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(next); + JS_DECLARE_NATIVE_FUNCTION(return_); + JS_DECLARE_NATIVE_FUNCTION(throw_); }; } diff --git a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index c0699024651..4139d892f36 100644 --- a/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -855,9 +856,6 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body() auto& vm = this->vm(); auto& realm = *vm.current_realm(); - if (m_kind == FunctionKind::AsyncGenerator) - return vm.throw_completion(ErrorType::NotImplemented, "Async Generator function execution"); - auto* bytecode_interpreter = vm.bytecode_interpreter_if_exists(); // The bytecode interpreter can execute generator functions while the AST interpreter cannot. @@ -867,7 +865,7 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body() // However, this does cause an awkward situation with features not supported in bytecode, where features that work outside of generators with AST // suddenly stop working inside of generators. // This is a stop gap until bytecode mode becomes the default. - if (m_kind == FunctionKind::Generator && !bytecode_interpreter) { + if ((m_kind == FunctionKind::Generator || m_kind == FunctionKind::AsyncGenerator) && !bytecode_interpreter) { bytecode_interpreter = &vm.bytecode_interpreter(); } @@ -920,6 +918,11 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body() if (m_kind == FunctionKind::Normal) return { Completion::Type::Return, result.value_or(js_undefined()), {} }; + if (m_kind == FunctionKind::AsyncGenerator) { + auto async_generator_object = TRY(AsyncGenerator::create(realm, result, this, vm.running_execution_context().copy(), move(*result_and_frame.frame))); + return { Completion::Type::Return, async_generator_object, {} }; + } + auto generator_object = TRY(GeneratorObject::create(realm, result, this, vm.running_execution_context().copy(), move(*result_and_frame.frame))); // NOTE: Async functions are entirely transformed to generator functions, and wrapped in a custom driver that returns a promise @@ -932,6 +935,8 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body() } else { if (m_kind == FunctionKind::Generator) return vm.throw_completion(ErrorType::NotImplemented, "Generator function execution in AST interpreter"); + if (m_kind == FunctionKind::AsyncGenerator) + return vm.throw_completion(ErrorType::NotImplemented, "Async generator function execution in AST interpreter"); OwnPtr local_interpreter; Interpreter* ast_interpreter = vm.interpreter_if_exists(); diff --git a/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.next.js b/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.next.js new file mode 100644 index 00000000000..c21ba3b07d2 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.next.js @@ -0,0 +1,145 @@ +describe("correct behaviour", () => { + async function* generatorFunction() { + yield 1; + await Promise.resolve(2); + const b = yield 3; + await Promise.resolve(b); + yield b + 1; + yield Promise.resolve(b + 2); + yield* [Promise.resolve(b + 3), Promise.resolve(b + 4), Promise.resolve(b + 5)]; + return b + 6; + } + + test("length is 1", () => { + expect(generatorFunction.prototype.next).toHaveLength(1); + }); + + const generator = generatorFunction(); + + function runGenerator(valueToPass, unwrapIteratorResult = true) { + let result = null; + test(`generator runs valueToPass=${valueToPass}`, () => { + const promise = generator.next(valueToPass); + promise + .then(value => { + result = value; + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + expect(result).toBeInstanceOf(Object); + expect(Object.getPrototypeOf(result)).toBe(Object.prototype); + expect(Object.keys(result)).toEqual(["value", "done"]); + }); + return unwrapIteratorResult ? result.value : result; + } + + test("can yield", () => { + const firstRunResult = runGenerator("bad1"); + expect(firstRunResult).toBe(1); + }); + + test("await does not yield", () => { + const secondRunResult = runGenerator("bad2"); + expect(secondRunResult).toBe(3); + }); + + test("can pass values via yield", () => { + const thirdRunResult = runGenerator(4); + expect(thirdRunResult).toBe(5); + }); + + test("yield implicitly awaits", () => { + const fourthRunResult = runGenerator("bad3"); + expect(fourthRunResult).toBe(6); + + const fifthRunResult = runGenerator("bad4"); + expect(fifthRunResult).toBe(7); + + const sixthRunResult = runGenerator("bad5"); + expect(sixthRunResult).toBe(8); + + const seventhRunResult = runGenerator("bad6"); + expect(seventhRunResult).toBe(9); + }); + + test("can return a value", () => { + const eighthRunResult = runGenerator("bad7", false); + expect(eighthRunResult.value).toBe(10); + expect(eighthRunResult.done).toBeTrue(); + }); + + test("gets undefined in completed state", () => { + const ninethRunResult = runGenerator("bad8", false); + expect(ninethRunResult.value).toBeUndefined(); + expect(ninethRunResult.done).toBeTrue(); + }); + + async function* implicitReturnFunction() { + 0xbbadbeef; + } + + const implicitReturnGenerator = implicitReturnFunction(); + + test("gets undefined from implicit return", () => { + implicitReturnGenerator + .next("bad9") + .then(iteratorResult => { + expect(iteratorResult.value).toBeUndefined(); + expect(iteratorResult.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Implicit await generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); + + async function* unhandledExceptionFunction() { + throw 1337; + } + + const unhandledExceptionGenerator = unhandledExceptionFunction(); + + test("promise is rejected on unhandled exceptions", () => { + unhandledExceptionGenerator + .next("bad10") + .then(() => { + expect().fail( + "Unhandled exception generator did NOT throw an unhandled exception." + ); + }) + .catch(e => { + expect(e).toBe(1337); + }); + runQueuedPromiseJobs(); + }); + + test("generator is complete after unhandled exception", () => { + unhandledExceptionGenerator + .next("bad11") + .then(iteratorResult => { + expect(iteratorResult.value).toBeUndefined(); + expect(iteratorResult.done).toBeTrue(); + }) + .catch(e => { + expect().fail( + "Unhandled exception generator threw an unhandled exception in Completed state." + ); + }); + runQueuedPromiseJobs(); + }); +}); + +describe("errors", () => { + test("this value must be an AsyncGenerator object", () => { + async function* generator() {} + let rejection = null; + generator.prototype.next.call("foo").catch(error => { + rejection = error; + }); + runQueuedPromiseJobs(); + expect(rejection).toBeInstanceOf(TypeError); + expect(rejection.message).toBe("Not an object of type AsyncGenerator"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.return.js b/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.return.js new file mode 100644 index 00000000000..dabba466452 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.return.js @@ -0,0 +1,146 @@ +describe("correct behavior", () => { + async function* emptyGeneratorFunction() {} + + test("length is 1", () => { + expect(emptyGeneratorFunction.prototype.return).toHaveLength(1); + }); + + const emptyGenerator = emptyGeneratorFunction(); + + test("return from SuspendedStart", () => { + emptyGenerator + .return(1337) + .then(result => { + expect(result.value).toBe(1337); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); + + test("return from Completed", () => { + emptyGenerator + .return(123) + .then(result => { + expect(result.value).toBe(123); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); + + async function* generatorTwo() { + yield 1337; + yield 123; + } + + const generatorTwoIterator = generatorTwo(); + + test("return from SuspendedYield", () => { + generatorTwoIterator + .next("bad1") + .then(result => { + expect(result.value).toBe(1337); + expect(result.done).toBeFalse(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + generatorTwoIterator + .return(999) + .then(result => { + expect(result.value).toBe(999); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + generatorTwoIterator + .next("bad2") + .then(result => { + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); + + async function* injectedCompletionGenerator() { + try { + yield 1; + } finally { + yield 2; + } + } + + const injectedCompletionGeneratorObject = injectedCompletionGenerator(); + + test("return completion is injected into generator", () => { + injectedCompletionGeneratorObject + .next("bad1") + .then(result => { + expect(result.value).toBe(1); + expect(result.done).toBeFalse(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + injectedCompletionGeneratorObject + .return(3) + .then(result => { + expect(result.value).toBe(2); + expect(result.done).toBeFalse(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + injectedCompletionGeneratorObject + .next("bad3") + .then(result => { + expect(result.value).toBe(3); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + injectedCompletionGeneratorObject + .next("bad4") + .then(result => { + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); +}); + +describe("errors", () => { + test("this value must be an AsyncGenerator object", () => { + async function* generator() {} + let rejection = null; + generator.prototype.return.call("foo").catch(error => { + rejection = error; + }); + runQueuedPromiseJobs(); + expect(rejection).toBeInstanceOf(TypeError); + expect(rejection.message).toBe("Not an object of type AsyncGenerator"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.throw.js b/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.throw.js new file mode 100644 index 00000000000..bc48eac7f63 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/AsyncGenerator/AsyncGenerator.prototype.throw.js @@ -0,0 +1,132 @@ +describe("correct behavior", () => { + async function* emptyGeneratorFunction() {} + + test("length is 1", () => { + expect(emptyGeneratorFunction.prototype.throw).toHaveLength(1); + }); + + const emptyGenerator = emptyGeneratorFunction(); + + test("throw from SuspendedStart", () => { + emptyGenerator + .throw(1337) + .then(() => { + expect().fail("Generator did NOT throw an unhandled exception."); + }) + .catch(e => { + expect(e).toBe(1337); + }); + runQueuedPromiseJobs(); + }); + + test("throw from Completed", () => { + emptyGenerator + .throw(123) + .then(() => { + expect().fail("Generator did NOT throw an unhandled exception."); + }) + .catch(e => { + expect(e).toBe(123); + }); + runQueuedPromiseJobs(); + }); + + async function* generatorTwo() { + yield 1337; + yield 123; + } + + const generatorTwoIterator = generatorTwo(); + + test("throw from SuspendedYield", () => { + generatorTwoIterator + .next("bad1") + .then(result => { + expect(result.value).toBe(1337); + expect(result.done).toBeFalse(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + generatorTwoIterator + .throw(999) + .then(() => { + expect().fail("Generator did NOT throw an unhandled exception."); + }) + .catch(e => { + expect(e).toBe(999); + }); + runQueuedPromiseJobs(); + + generatorTwoIterator + .next("bad2") + .then(result => { + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); + + async function* injectedCompletionGenerator() { + try { + yield 1; + } catch (e) { + yield e; + } + } + + const injectedCompletionGeneratorObject = injectedCompletionGenerator(); + + test("throw completion is injected into generator", () => { + injectedCompletionGeneratorObject + .next("bad1") + .then(result => { + expect(result.value).toBe(1); + expect(result.done).toBeFalse(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + injectedCompletionGeneratorObject + .throw(9999) + .then(result => { + expect(result.value).toBe(9999); + expect(result.done).toBeFalse(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + + injectedCompletionGeneratorObject + .next("bad2") + .then(result => { + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }) + .catch(e => { + expect().fail(`Generator threw an unhandled exception: ${e}`); + }); + runQueuedPromiseJobs(); + }); +}); + +describe("errors", () => { + test("this value must be an AsyncGenerator object", () => { + async function* generator() {} + let rejection = null; + generator.prototype.throw.call("foo").catch(error => { + rejection = error; + }); + runQueuedPromiseJobs(); + expect(rejection).toBeInstanceOf(TypeError); + expect(rejection.message).toBe("Not an object of type AsyncGenerator"); + }); +});