diff --git a/core/engine/src/context/mod.rs b/core/engine/src/context/mod.rs index b9791334283..c8e62c08e33 100644 --- a/core/engine/src/context/mod.rs +++ b/core/engine/src/context/mod.rs @@ -517,9 +517,16 @@ impl Context { /// The stack trace is returned ordered with the most recent frames first. #[inline] pub fn stack_trace(&self) -> impl Iterator { + use crate::vm::CallFrameFlags; // The first frame is always a dummy frame (see `Vm` implementation for more details), - // so skip the dummy frame and return the reversed list so that the most recent frames are first. - self.vm.frames.iter().skip(1).rev() + // so skip the dummy frame, filter out lightweight native frames, and return the reversed + // list so that the most recent frames are first. + self.vm + .frames + .iter() + .skip(1) + .filter(|f| !f.flags.contains(CallFrameFlags::NATIVE_FRAME)) + .rev() } /// Replaces the currently active realm with `realm`, and returns the old realm. @@ -641,11 +648,6 @@ impl Context { self.module_loader.clone() } - /// Swaps the currently active realm with `realm`. - pub(crate) fn swap_realm(&mut self, realm: &mut Realm) { - std::mem::swap(&mut self.vm.frame_mut().realm, realm); - } - /// Increment and get the parser identifier. pub(crate) fn next_parser_identifier(&mut self) -> u32 { self.parser_identifier += 1; diff --git a/core/engine/src/native_function/mod.rs b/core/engine/src/native_function/mod.rs index da480f914dc..c6b0c7afb24 100644 --- a/core/engine/src/native_function/mod.rs +++ b/core/engine/src/native_function/mod.rs @@ -15,6 +15,7 @@ use crate::{ Context, JsNativeError, JsObject, JsResult, JsValue, builtins::{OrdinaryObject, function::ConstructorKind}, context::intrinsics::StandardConstructors, + environments::EnvironmentStack, object::{ FunctionObjectBuilder, JsData, JsFunction, JsPromise, internal_methods::{ @@ -23,6 +24,7 @@ use crate::{ }, }, realm::Realm, + vm::{CallFrame, CallFrameFlags, CodeBlock}, }; mod continuation; @@ -354,9 +356,18 @@ pub(crate) fn native_function_call( .shadow_stack .push_native(pc, name, native_source_info); - let mut realm = realm.unwrap_or_else(|| context.realm().clone()); + let realm = realm.unwrap_or_else(|| context.realm().clone()); - context.swap_realm(&mut realm); + // Push a lightweight frame for the native call so the frame stack + // correctly represents the spec's execution context stack (ยง10.3). + let native_frame = CallFrame::new( + Gc::new(CodeBlock::new(JsString::default(), 0, true)), + None, + EnvironmentStack::new(), + realm, + ) + .with_flags(CallFrameFlags::REGISTERS_ALREADY_PUSHED | CallFrameFlags::NATIVE_FRAME); + context.vm.frames.push(native_frame); context.vm.native_active_function = Some(this_function_object); let result = if constructor.is_some() { @@ -367,7 +378,7 @@ pub(crate) fn native_function_call( .map_err(|err| err.inject_realm(context.realm().clone())); context.vm.native_active_function = None; - context.swap_realm(&mut realm); + context.vm.frames.pop(); context.vm.shadow_stack.pop(); @@ -409,9 +420,17 @@ fn native_function_construct( .shadow_stack .push_native(pc, name, native_source_info); - let mut realm = realm.unwrap_or_else(|| context.realm().clone()); + let realm = realm.unwrap_or_else(|| context.realm().clone()); - context.swap_realm(&mut realm); + // Push a lightweight frame for the native construct call. + let native_frame = CallFrame::new( + Gc::new(CodeBlock::new(JsString::default(), 0, true)), + None, + EnvironmentStack::new(), + realm, + ) + .with_flags(CallFrameFlags::REGISTERS_ALREADY_PUSHED | CallFrameFlags::NATIVE_FRAME); + context.vm.frames.push(native_frame); context.vm.native_active_function = Some(this_function_object); let new_target = context.vm.stack.pop(); @@ -449,7 +468,7 @@ fn native_function_construct( }); context.vm.native_active_function = None; - context.swap_realm(&mut realm); + context.vm.frames.pop(); context.vm.shadow_stack.pop(); diff --git a/core/engine/src/tests/async_generator.rs b/core/engine/src/tests/async_generator.rs index a263d555272..188ddcc325f 100644 --- a/core/engine/src/tests/async_generator.rs +++ b/core/engine/src/tests/async_generator.rs @@ -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; @@ -120,3 +120,77 @@ fn return_on_then_queue() { TestAction::assert_eq("count", JsValue::from(2)), ]); } + +#[test] +fn cross_realm_async_generator_yield() { + // 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" + ); +} diff --git a/core/engine/src/vm/call_frame/mod.rs b/core/engine/src/vm/call_frame/mod.rs index cd560f18594..c4ccc2885cb 100644 --- a/core/engine/src/vm/call_frame/mod.rs +++ b/core/engine/src/vm/call_frame/mod.rs @@ -33,6 +33,10 @@ bitflags::bitflags! { /// If the `this` value has been cached. const THIS_VALUE_CACHED = 0b0000_1000; + + /// This is a lightweight frame pushed for a native function call. + /// It only exists to hold the function's realm on the frame stack. + const NATIVE_FRAME = 0b0001_0000; } } diff --git a/core/engine/src/vm/opcode/generator/yield_stm.rs b/core/engine/src/vm/opcode/generator/yield_stm.rs index 8e91aa3fbdf..a3c25419aac 100644 --- a/core/engine/src/vm/opcode/generator/yield_stm.rs +++ b/core/engine/src/vm/opcode/generator/yield_stm.rs @@ -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); } @@ -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).