Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion core/engine/src/tests/async_generator.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
Context, JsValue, TestAction, builtins::promise::PromiseState, object::JsPromise,
Context, JsValue, Source, TestAction, builtins::promise::PromiseState, object::JsPromise,
run_test_actions,
};
use boa_macros::js_str;
Expand Down Expand Up @@ -120,3 +120,77 @@ fn return_on_then_queue() {
TestAction::assert_eq("count", JsValue::from(2)),
]);
}

#[test]
fn cross_realm_async_generator_yield() {
Copy link
Member

@jedel1043 jedel1043 Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also might want to test if the generator is really using the caller's realm to create the iter object. A simple way to do this would be to check that the prototype of the async generator's returned object is equal to the Object.prototype in old_realm.

// Exercises AsyncGeneratorYield spec steps 6-8 (previousRealm handling)
// by creating a generator in one realm and consuming it from another.
// Per spec, previousRealm is the realm of the second-to-top execution
// context (the `next()` / AwaitFulfilled handler), which has the same
// realm as the generator. The iter result prototype should match the
// generator realm's Object.prototype.
let mut context = Context::default();

let generator_realm = context.create_realm().unwrap();

let old_realm = context.enter_realm(generator_realm.clone());
let generator = context
.eval(Source::from_bytes(
b"(async function* g() { yield 42; yield 99; })()",
))
.unwrap();
context.enter_realm(old_realm);

// Grab Object.prototype from the generator's realm (previousRealm per spec).
let gen_realm_object_proto = generator_realm
.intrinsics()
.constructors()
.object()
.prototype();

let next_fn = generator
.as_object()
.unwrap()
.get(js_str!("next"), &mut context)
.unwrap();

let call_next = |ctx: &mut Context| -> JsValue {
let result = next_fn
.as_callable()
.unwrap()
.call(&generator, &[], ctx)
.unwrap();
ctx.run_jobs().unwrap();
result
};

// First yield: value 42
let first = call_next(&mut context);
assert_promise_iter_value(&first, &JsValue::from(42), false, &mut context);

// Verify the iter result was created in the generator's realm (previousRealm).
let first_promise = JsPromise::from_object(first.as_object().unwrap().clone()).unwrap();
let PromiseState::Fulfilled(first_result) = first_promise.state() else {
panic!("promise was not fulfilled");
};
assert_eq!(
first_result.as_object().unwrap().prototype(),
Some(gen_realm_object_proto.clone()),
"iter result prototype should be generator realm's Object.prototype"
);

// Second yield: value 99
let second = call_next(&mut context);
assert_promise_iter_value(&second, &JsValue::from(99), false, &mut context);

// Verify the iter result was created in the generator's realm (previousRealm).
let second_promise = JsPromise::from_object(second.as_object().unwrap().clone()).unwrap();
let PromiseState::Fulfilled(second_result) = second_promise.state() else {
panic!("promise was not fulfilled");
};
assert_eq!(
second_result.as_object().unwrap().prototype(),
Some(gen_realm_object_proto),
"iter result prototype should be generator realm's Object.prototype"
);
}
27 changes: 19 additions & 8 deletions core/engine/src/vm/opcode/generator/yield_stm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,23 @@ impl AsyncGeneratorYield {
let value = context.vm.get_register(value.into());
let completion = Ok(value.clone());

// TODO: 6. Assert: The execution context stack has at least two elements.
// TODO: 7. Let previousContext be the second to top element of the execution context stack.
// TODO: 8. Let previousRealm be previousContext's Realm.
// 6. Assert: The execution context stack has at least two elements.
// 7. Let previousContext be the second to top element of the execution context stack.
// 8. Let previousRealm be previousContext's Realm.
let previous_realm = context
.vm
.frames
.get(context.vm.frames.len() - 2)
.map(|frame| frame.realm.clone());

// 9. Perform AsyncGeneratorCompleteStep(generator, completion, false, previousRealm).
if let Err(err) =
AsyncGenerator::complete_step(&async_generator_object, completion, false, None, context)
{
if let Err(err) = AsyncGenerator::complete_step(
&async_generator_object,
completion,
false,
previous_realm,
context,
) {
return context.handle_error(err);
}

Expand Down Expand Up @@ -114,8 +124,9 @@ impl AsyncGeneratorYield {
// a. Set generator.[[AsyncGeneratorState]] to suspended-yield.
r#gen.data_mut().state = AsyncGeneratorState::SuspendedYield;

// TODO: 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.
// TODO: c. Let callerContext be the running execution context.
// 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.
// 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).
Expand Down
Loading