Skip to content

Commit 28155e6

Browse files
author
Elior Nguyen
committed
fix: pass previousRealm to AsyncGeneratorCompleteStep in AsyncGeneratorYield
1 parent 965f38c commit 28155e6

File tree

4 files changed

+111
-9
lines changed

4 files changed

+111
-9
lines changed

core/engine/src/builtins/generator/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ impl GeneratorContext {
100100
resume_kind: GeneratorResumeKind,
101101
context: &mut Context,
102102
) -> CompletionRecord {
103+
// Capture the caller's realm before swapping stacks.
104+
// This is used by AsyncGeneratorYield (spec step 8: "Let previousRealm be
105+
// previousContext's Realm") to pass to AsyncGeneratorCompleteStep.
106+
let caller_realm = Some(context.realm().clone());
107+
103108
std::mem::swap(&mut context.vm.stack, &mut self.stack);
104109
let frame = self.call_frame.take().expect("should have a call frame");
105110
let fp = frame.fp;
@@ -109,6 +114,7 @@ impl GeneratorContext {
109114
let frame = context.vm.frame_mut();
110115
frame.fp = fp;
111116
frame.rp = rp;
117+
frame.caller_realm = caller_realm;
112118
frame.set_exit_early(true);
113119

114120
if let Some(value) = value {

core/engine/src/tests/async_generator.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
Context, JsValue, TestAction, builtins::promise::PromiseState, object::JsPromise,
2+
Context, JsValue, Source, TestAction, builtins::promise::PromiseState, object::JsPromise,
33
run_test_actions,
44
};
55
use boa_macros::js_str;
@@ -120,3 +120,82 @@ fn return_on_then_queue() {
120120
TestAction::assert_eq("count", JsValue::from(2)),
121121
]);
122122
}
123+
124+
#[test]
125+
fn cross_realm_yield_creates_iterator_result_in_caller_realm() {
126+
// Verifies that an async generator created in one realm can be correctly
127+
// consumed from another realm. This exercises the AsyncGeneratorYield
128+
// spec steps 6-8 (previousRealm handling).
129+
let mut context = Context::default();
130+
131+
// Create a second realm for the generator.
132+
let generator_realm = context.create_realm().unwrap();
133+
134+
// Switch to the generator realm and create the async generator there.
135+
let old_realm = context.enter_realm(generator_realm);
136+
let generator = context
137+
.eval(Source::from_bytes(
138+
b"(async function* g() { yield 42; yield 99; })()",
139+
))
140+
.unwrap();
141+
142+
// Switch back to the caller's (original) realm.
143+
context.enter_realm(old_realm);
144+
145+
// Call .next() from the caller's realm.
146+
let next_fn = generator
147+
.as_object()
148+
.unwrap()
149+
.get(js_str!("next"), &mut context)
150+
.unwrap();
151+
152+
// First yield: should produce {value: 42, done: false}
153+
let result_promise = next_fn
154+
.as_callable()
155+
.unwrap()
156+
.call(&generator, &[], &mut context)
157+
.unwrap();
158+
159+
context.run_jobs().unwrap();
160+
161+
let promise = JsPromise::from_object(result_promise.as_object().unwrap().clone()).unwrap();
162+
let PromiseState::Fulfilled(iter_result) = promise.state() else {
163+
panic!("first yield: promise was not fulfilled");
164+
};
165+
166+
let result_obj = iter_result.as_object().unwrap();
167+
let value = result_obj.get(js_str!("value"), &mut context).unwrap();
168+
let done = result_obj
169+
.get(js_str!("done"), &mut context)
170+
.unwrap()
171+
.as_boolean()
172+
.unwrap();
173+
174+
assert_eq!(value, JsValue::from(42));
175+
assert!(!done);
176+
177+
// Second yield: should produce {value: 99, done: false}
178+
let result_promise2 = next_fn
179+
.as_callable()
180+
.unwrap()
181+
.call(&generator, &[], &mut context)
182+
.unwrap();
183+
184+
context.run_jobs().unwrap();
185+
186+
let promise2 = JsPromise::from_object(result_promise2.as_object().unwrap().clone()).unwrap();
187+
let PromiseState::Fulfilled(iter_result2) = promise2.state() else {
188+
panic!("second yield: promise was not fulfilled");
189+
};
190+
191+
let result_obj2 = iter_result2.as_object().unwrap();
192+
let value2 = result_obj2.get(js_str!("value"), &mut context).unwrap();
193+
let done2 = result_obj2
194+
.get(js_str!("done"), &mut context)
195+
.unwrap()
196+
.as_boolean()
197+
.unwrap();
198+
199+
assert_eq!(value2, JsValue::from(99));
200+
assert!(!done2);
201+
}

core/engine/src/vm/call_frame/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ pub struct CallFrame {
7777
/// \[\[Realm\]\]
7878
pub(crate) realm: Realm,
7979

80+
/// The caller's realm, captured during generator resume.
81+
/// Used by `AsyncGeneratorYield` to pass `previousRealm` to `AsyncGeneratorCompleteStep`.
82+
pub(crate) caller_realm: Option<Realm>,
83+
8084
// SAFETY: Nothing in `CallFrameFlags` requires tracing, so this is safe.
8185
#[unsafe_ignore_trace]
8286
pub(crate) flags: CallFrameFlags,
@@ -154,6 +158,7 @@ impl CallFrame {
154158
active_runnable,
155159
environments,
156160
realm,
161+
caller_realm: None,
157162
flags: CallFrameFlags::empty(),
158163
}
159164
}

core/engine/src/vm/opcode/generator/yield_stm.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,21 @@ impl AsyncGeneratorYield {
6666
let value = context.vm.get_register(value.into());
6767
let completion = Ok(value.clone());
6868

69-
// TODO: 6. Assert: The execution context stack has at least two elements.
70-
// TODO: 7. Let previousContext be the second to top element of the execution context stack.
71-
// TODO: 8. Let previousRealm be previousContext's Realm.
69+
// 6. Assert: The execution context stack has at least two elements.
70+
// (Guaranteed by Boa's stack-swap architecture — a caller context always exists.)
71+
// 7. Let previousContext be the second to top element of the execution context stack.
72+
// 8. Let previousRealm be previousContext's Realm.
73+
// (Captured in GeneratorContext::resume() before the stack swap and stored in the CallFrame.)
74+
let previous_realm = context.vm.frame().caller_realm.clone();
75+
7276
// 9. Perform AsyncGeneratorCompleteStep(generator, completion, false, previousRealm).
73-
if let Err(err) =
74-
AsyncGenerator::complete_step(&async_generator_object, completion, false, None, context)
75-
{
77+
if let Err(err) = AsyncGenerator::complete_step(
78+
&async_generator_object,
79+
completion,
80+
false,
81+
previous_realm,
82+
context,
83+
) {
7684
return context.handle_error(err);
7785
}
7886

@@ -114,8 +122,12 @@ impl AsyncGeneratorYield {
114122
// a. Set generator.[[AsyncGeneratorState]] to suspended-yield.
115123
r#gen.data_mut().state = AsyncGeneratorState::SuspendedYield;
116124

117-
// 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.
118-
// TODO: c. Let callerContext be the running execution context.
125+
// b. Remove genContext from the execution context stack and restore the execution context
126+
// that is at the top of the execution context stack as the running execution context.
127+
// (Handled implicitly by Boa's stack-swap architecture: handle_yield() breaks out of
128+
// the run loop, and GeneratorContext::resume() swaps stacks back.)
129+
// c. Let callerContext be the running execution context.
130+
// (After the stack swap, the caller's context is automatically restored.)
119131
// d. Resume callerContext passing undefined. If genContext is ever resumed again, let resumptionValue be the Completion Record with which it is resumed.
120132
// e. Assert: If control reaches here, then genContext is the running execution context again.
121133
// f. Return ? AsyncGeneratorUnwrapYieldResumption(resumptionValue).

0 commit comments

Comments
 (0)