feat: for-of 异常中断按规范关闭迭代器,next() 抛错可捕获并保留原始值

This commit is contained in:
爱特每羊队
2026-06-27 18:28:07 +08:00
parent 09a9d3dfd2
commit 11d69ea95c
8 changed files with 263 additions and 9 deletions
+14 -1
View File
@@ -113,6 +113,8 @@ pub struct TryHandler {
pub catch_pc: Option<usize>,
pub finally_pc: Option<usize>,
pub frame_depth: usize,
/// for_of_iters length at try entry — bounds which iterators IteratorClose on unwind.
pub for_of_depth: usize,
}
/// Heap-allocated snapshot used by `call_bytecode_function_inline`.
@@ -164,6 +166,10 @@ pub struct Vm {
pub(crate) save_stack: Vec<JsValue>,
pub(crate) try_stack: Vec<TryHandler>,
pub(crate) exception_value: Option<JsValue>,
/// Side-channel carrying the JsValue thrown by a sync call whose error was flattened
/// to a String by `call_function_sync`/`unwind`, so for-of can re-throw the original
/// value. Plain field (NOT in InlineSyncState) so it survives the inline-call restore.
pub(crate) last_uncaught_value: Option<JsValue>,
pub(crate) pending_exception: Option<JsValue>,
pub(crate) pending_error_kind: Option<&'static str>,
pub(crate) root_reg_limit: u8,
@@ -635,7 +641,10 @@ impl Vm {
self.regs = saved_regs;
return match result {
NativeResult::Ok(val) => Ok(val),
NativeResult::Err(err) => Err(self.error_text(err)),
NativeResult::Err(err) => {
self.last_uncaught_value = Some(err);
Err(self.error_text(err))
}
NativeResult::TailCall { callee, this, args } => self.call_function_sync(callee, this, &args),
};
}
@@ -1267,6 +1276,7 @@ mod tests {
catch_pc: Some(1),
finally_pc: None,
frame_depth: 0,
for_of_depth: 0,
});
vm.exception_value = Some(JsValue::int(2));
vm.pending_exception = Some(JsValue::int(3));
@@ -1520,6 +1530,9 @@ impl oxide_runtime_api::VmHost for Vm {
fn epoch(&self) -> &Epoch {
self.epoch()
}
fn take_uncaught_value(&mut self) -> Option<JsValue> {
self.last_uncaught_value.take()
}
fn property_key_si(&mut self, val: JsValue) -> u32 {
self.property_key_si(val)
}
+2
View File
@@ -177,6 +177,7 @@ impl Vm {
catch_pc,
finally_pc: None,
frame_depth: self.frames.len(),
for_of_depth: self.iters.for_of_iters.len(),
});
}
@@ -193,6 +194,7 @@ impl Vm {
catch_pc: None,
finally_pc: Some(finally_pc),
frame_depth: self.frames.len(),
for_of_depth: self.iters.for_of_iters.len(),
});
}
+73 -7
View File
@@ -402,17 +402,26 @@ impl Vm {
return Err("FOR_OF_DONE iterator is not an object".into());
}
self.last_uncaught_value = None;
let iter_obj = unsafe { &*iterator.as_js_object_ptr() };
let next_si = self.kernel_core.perm_interner().intern("next").0;
let next_fn = self.ordinary_get(iter_obj, next_si, iterator)?;
let result = self.call_function_sync(next_fn, iterator, &[])?;
let next_fn = match self.ordinary_get(iter_obj, next_si, iterator) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
let result = match self.call_function_sync(next_fn, iterator, &[]) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
if !result.is_object() {
return self.raise_type_error("iterator result is not an object");
}
let result_obj = unsafe { &*result.as_js_object_ptr() };
let done_si = self.kernel_core.perm_interner().intern("done").0;
let done_val = self.ordinary_get(result_obj, done_si, result)?;
let done_val = match self.ordinary_get(result_obj, done_si, result) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
let done = to_boolean(done_val);
self.iters.set_last_for_of_result(result);
self.regs[rd] = JsValue::bool(!done);
@@ -421,6 +430,7 @@ impl Vm {
pub(crate) fn dispatch_for_of_next(&mut self, rd: usize) -> Result<(), String> {
vm_trace!("FOR_OF_NEXT rd={}", rd);
self.last_uncaught_value = None;
let result = self.iters.last_for_of_result();
if !result.is_object() {
self.regs[rd] = JsValue::undefined();
@@ -428,30 +438,86 @@ impl Vm {
}
let result_obj = unsafe { &*result.as_js_object_ptr() };
let value_si = self.kernel_core.perm_interner().intern("value").0;
self.regs[rd] = self.ordinary_get(result_obj, value_si, result)?;
self.regs[rd] = match self.ordinary_get(result_obj, value_si, result) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
Ok(())
}
/// next()/value access threw: route through unwind so a surrounding try/catch can catch
/// it, re-throwing the ORIGINAL value when available. Per ECMA-262 the iterator is NOT
/// closed via return() on a next()-throw — pop it so the unwinding IteratorClose pass skips it.
fn throw_for_of_error(&mut self, msg: String) -> Result<(), String> {
self.iters.pop_for_of();
let exc = match self.last_uncaught_value.take() {
Some(v) => v,
None => oxide_builtins::error::create_error(self, &msg),
};
self.exception_value = Some(exc);
self.pending_error_kind = Some(self.thrown_error_kind(exc));
self.unwind()
}
pub(crate) fn dispatch_for_of_close(&mut self) -> Result<(), String> {
vm_trace!("FOR_OF_CLOSE");
let Some(iterator) = self.iters.pop_for_of() else {
return Ok(());
};
// Normal / break / return exit: no prior abrupt completion, so return()'s own throw propagates.
self.close_for_of_iterator(iterator, false)
}
/// Call `iterator.return()` (IteratorClose). When `suppress_return_error` is true the call
/// is being made because an enclosing abrupt completion is unwinding: return()'s own result
/// is discarded and the in-flight exception is preserved across the call. When false (normal
/// for-of exit) return()'s error propagates.
pub(crate) fn close_for_of_iterator(
&mut self, iterator: JsValue, suppress_return_error: bool,
) -> Result<(), String> {
if !iterator.is_object() {
return Ok(());
}
let iter_obj = unsafe { &*iterator.as_js_object_ptr() };
let return_si = self.kernel_core.perm_interner().intern("return").0;
let return_fn = self.ordinary_get(iter_obj, return_si, iterator)?;
let return_fn = match self.ordinary_get(iter_obj, return_si, iterator) {
Ok(v) => v,
Err(e) => {
if suppress_return_error {
return Ok(());
}
return Err(e);
}
};
if return_fn.is_object() {
let return_obj = unsafe { &*return_fn.as_js_object_ptr() };
if return_obj.is_function() {
let _ = self.call_function_sync(return_fn, iterator, &[])?;
if suppress_return_error {
let saved_exc = self.exception_value;
let saved_kind = self.pending_error_kind;
let _ = self.call_function_sync(return_fn, iterator, &[]);
self.exception_value = saved_exc;
self.pending_error_kind = saved_kind;
} else {
let _ = self.call_function_sync(return_fn, iterator, &[])?;
}
}
}
Ok(())
}
/// IteratorClose every active for-of iterator above `depth`, used by `unwind()` to close
/// loops abandoned by a throw. Pops before closing so a re-entrant call can't re-close,
/// and so the next()-throw path (which already popped its own iterator) is skipped.
pub(crate) fn close_for_of_above(&mut self, depth: usize) {
while self.iters.for_of_iters.len() > depth {
let Some(iterator) = self.iters.pop_for_of() else {
break;
};
let _ = self.close_for_of_iterator(iterator, true);
}
}
pub(crate) fn dispatch_rest_object(&mut self, rd: usize, a: usize) -> Result<(), String> {
vm_trace!("REST_OBJECT rd={}", rd);
let src = self.regs[a];
+6
View File
@@ -176,6 +176,8 @@ impl Vm {
self.restore_frame(frame);
}
}
// IteratorClose any for-of loops abandoned inside this handler's scope.
self.close_for_of_above(handler.for_of_depth);
if let Some(finally_pc) = handler.finally_pc {
vm_trace!("unwind: entering finally at pc={}", finally_pc);
if self.pending_exception.is_none() {
@@ -193,10 +195,14 @@ impl Vm {
return Ok(());
}
}
self.close_for_of_above(0);
while let Some(frame) = self.frames.pop() {
self.restore_frame(frame);
}
let exc = self.exception_value.take().unwrap_or(JsValue::undefined());
// Preserve the escaping JsValue so a for-of next()-throw can re-throw the original
// value instead of the flattened string (consumed by dispatch_for_of_done/next).
self.last_uncaught_value = Some(exc);
let kind_str = self.pending_error_kind.take().unwrap_or("Error");
let exc_text = self.error_text(exc);
let msg = if exc_text.starts_with(kind_str) {
+2
View File
@@ -38,6 +38,7 @@ impl Vm {
save_stack: Vec::new(),
try_stack: Vec::new(),
exception_value: None,
last_uncaught_value: None,
pending_exception: None,
pending_error_kind: None,
root_reg_limit: 0,
@@ -95,6 +96,7 @@ impl Vm {
save_stack: Vec::new(),
try_stack: Vec::new(),
exception_value: None,
last_uncaught_value: None,
pending_exception: None,
pending_error_kind: None,
root_reg_limit: 0,
@@ -0,0 +1,126 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Spec (ECMA-262 §13.7.5): next()-throw propagates the ORIGINAL value WITHOUT calling
// return(); IteratorClose (return()) runs only on BODY abrupt completion (throw/break/return).
// return() is observed via globalThis (a function CAN read/write globalThis properties;
// it cannot write a plain outer `var` in this engine).
#[test]
fn for_of_next_throw_is_catchable_with_original_type() {
assert_eq!(
eval(
"try{for(var v of {next:function(){throw new TypeError('boom');}}){}}\
catch(e){e.name==='TypeError' && e.message==='boom'}"
),
"true",
"next() throw catchable with original error type+message"
);
}
#[test]
fn for_of_next_throw_preserves_bare_thrown_value() {
assert_eq!(
eval("try{for(var v of {next:function(){throw 7;}}){}}catch(e){e===7}"),
"true",
"a bare thrown value is preserved, not re-wrapped"
);
}
#[test]
fn for_of_next_throw_does_not_call_return() {
assert_eq!(
eval(
"globalThis.r=false;\
var it={next:function(){throw new Error('x');},return:function(){globalThis.r=true;return {};}};\
try{for(var v of it){}}catch(e){}globalThis.r===false"
),
"true",
"return() must NOT be called when next() throws"
);
}
#[test]
fn for_of_next_throw_without_return_still_propagates() {
assert_eq!(
eval("try{for(var v of {next:function(){throw new TypeError('e');}}){}}catch(e){e instanceof TypeError}"),
"true"
);
}
#[test]
fn for_of_body_throw_calls_return() {
assert_eq!(
eval(
"globalThis.r=false;\
var it={next:function(){return {value:1,done:false};},return:function(){globalThis.r=true;return {};}};\
try{for(var v of it){throw new Error('boom');}}catch(e){}globalThis.r===true"
),
"true",
"body throw calls return() (IteratorClose)"
);
}
#[test]
fn for_of_body_throw_propagates_body_error() {
assert_eq!(
eval(
"var it={next:function(){return {value:1,done:false};},return:function(){return {};}};\
try{for(var v of it){throw new Error('boom');}}catch(e){e.message==='boom'}"
),
"true"
);
}
#[test]
fn for_of_body_throw_return_also_throws_body_error_wins() {
assert_eq!(
eval(
"var it={next:function(){return {value:1,done:false};},return:function(){throw new Error('B');}};\
try{for(var v of it){throw new Error('A');}}catch(e){e.message==='A'}"
),
"true",
"when return() also throws, the body's original error wins"
);
}
#[test]
fn for_of_break_calls_return() {
assert_eq!(
eval(
"globalThis.r=false;\
var it={next:function(){return {value:1,done:false};},return:function(){globalThis.r=true;return {};}};\
for(var v of it){break;}globalThis.r===true"
),
"true",
"break calls return() (via FOR_OF_CLOSE)"
);
}
#[test]
fn for_of_array_regression_still_iterates() {
assert_eq!(eval("var r=0;for(var v of [1,2,3]){r=r+v;}r===6"), "true");
}
#[test]
fn for_of_string_regression_still_iterates() {
assert_eq!(eval("var r=0;for(var c of 'abc'){r=r+1;}r===3"), "true");
}