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
16 changes: 9 additions & 7 deletions core/engine/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &CallFrame> {
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.
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 25 additions & 6 deletions core/engine/src/native_function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -23,6 +24,7 @@ use crate::{
},
},
realm::Realm,
vm::{CallFrame, CallFrameFlags, CodeBlock},
};

mod continuation;
Expand Down Expand Up @@ -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() {
Expand All @@ -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();

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();

Expand Down
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"
);
}
4 changes: 4 additions & 0 deletions core/engine/src/vm/call_frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
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