Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
15 changes: 14 additions & 1 deletion core/engine/src/builtins/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,23 @@ impl GeneratorContext {
resume_kind: GeneratorResumeKind,
context: &mut Context,
) -> CompletionRecord {
// Capture the caller's realm before the stack swap.
// `native_function_call` stores the pre-swap realm in `native_caller_realm`,
// which is the actual caller's realm (not the function's realm).
// AsyncGeneratorYield uses this as `previousRealm` (spec §27.6.3.8, step 8).
//
// Only set on the initial resume (from `next()`); subsequent resumes
// (e.g., await continuations from `run_jobs()`) must preserve the
// previously captured caller realm.
let caller_realm = context.vm.native_caller_realm.clone();

std::mem::swap(&mut context.vm.stack, &mut self.stack);
let Some(frame) = self.call_frame.take() else {
let Some(mut frame) = self.call_frame.take() else {
return CompletionRecord::Throw(PanicError::new("should have a call frame").into());
};
if frame.caller_realm.is_none() {
frame.caller_realm = caller_realm;
}
let fp = frame.fp;
let rp = frame.rp;
context.vm.push_frame(frame);
Expand Down
3 changes: 3 additions & 0 deletions core/engine/src/native_function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ pub(crate) fn native_function_call(

context.swap_realm(&mut realm);
context.vm.native_active_function = Some(this_function_object);
let previous_native_caller_realm = context.vm.native_caller_realm.take();
context.vm.native_caller_realm = Some(realm.clone());

let result = if constructor.is_some() {
function.call(&JsValue::undefined(), &args, context)
Expand All @@ -369,6 +371,7 @@ pub(crate) fn native_function_call(
.map_err(|err| err.inject_realm(context.realm().clone()));

context.vm.native_active_function = None;
context.vm.native_caller_realm = previous_native_caller_realm;
context.swap_realm(&mut realm);

context.vm.shadow_stack.pop();
Expand Down
70 changes: 69 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,71 @@ fn return_on_then_queue() {
TestAction::assert_eq("count", JsValue::from(2)),
]);
}

#[test]
fn cross_realm_async_generator_yield() {
Copy link
Copy Markdown
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.
// Also verifies that the iter result objects are created in the caller's
// realm by checking their prototype against old_realm's Object.prototype.
let mut context = Context::default();

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

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

// Grab Object.prototype from the caller's realm (old_realm).
let caller_object_proto = old_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 old_realm.
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(caller_object_proto.clone()),
"iter result prototype should be old_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 old_realm.
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(caller_object_proto),
"iter result prototype should be old_realm's Object.prototype"
);
}
5 changes: 5 additions & 0 deletions core/engine/src/vm/call_frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ pub struct CallFrame {
/// \[\[Realm\]\]
pub(crate) realm: Realm,

/// The caller's realm, captured before a generator stack swap.
/// Used by `AsyncGeneratorYield` to pass `previousRealm` to `complete_step`.
pub(crate) caller_realm: Option<Realm>,

// SAFETY: Nothing in `CallFrameFlags` requires tracing, so this is safe.
#[unsafe_ignore_trace]
pub(crate) flags: CallFrameFlags,
Expand Down Expand Up @@ -154,6 +158,7 @@ impl CallFrame {
active_runnable,
environments,
realm,
caller_realm: None,
flags: CallFrameFlags::empty(),
}
}
Expand Down
6 changes: 6 additions & 0 deletions core/engine/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ pub struct Vm {
/// because we don't push a frame for them.
pub(crate) native_active_function: Option<JsObject>,

/// The caller's realm before a native function's `swap_realm`.
/// Used by `GeneratorContext::resume()` to capture `previousRealm`
/// for `AsyncGeneratorYield`.
pub(crate) native_caller_realm: Option<Realm>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You added this again...

/// Number of nested host calls that re-enter the VM via `Context::run()`.
///
/// This is incremented by high-level host entry points such as
Expand Down Expand Up @@ -347,6 +352,7 @@ impl Vm {
pending_exception: None,
runtime_limits: RuntimeLimits::default(),
native_active_function: None,
native_caller_realm: None,
host_call_depth: 0,
shadow_stack: ShadowStack::default(),
#[cfg(feature = "trace")]
Expand Down
26 changes: 18 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,22 @@ 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.frame().caller_realm.clone();

// Clear caller_realm so the next `next()` call can set a fresh one.
context.vm.frame_mut().caller_realm = None;

// 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 +123,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