feat: for-of 异常中断按规范关闭迭代器,next() 抛错可捕获并保留原始值
This commit is contained in:
@@ -36,9 +36,40 @@ pub fn make_iterator_for_value<H: VmHost>(vm: &mut H, value: JsValue) -> Result<
|
|||||||
let next_fn = make_native_function(vm, "next", iterator_wrapper_next::<H> as *const (), 0);
|
let next_fn = make_native_function(vm, "next", iterator_wrapper_next::<H> as *const (), 0);
|
||||||
vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn);
|
vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn);
|
||||||
|
|
||||||
|
// Forward IteratorClose to the inner iterator so for-of abrupt completion can clean up.
|
||||||
|
let return_si = vm.kernel_core().perm_interner().intern("return").0;
|
||||||
|
let return_fn = make_native_function(vm, "return", iterator_wrapper_return::<H> as *const (), 0);
|
||||||
|
vm.set_or_create_prop_value(wrapper_obj, return_si, return_fn);
|
||||||
|
|
||||||
Ok(JsValue::from_js_object(wrapper))
|
Ok(JsValue::from_js_object(wrapper))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn iterator_wrapper_return<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
|
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||||
|
if !this_val.is_object() {
|
||||||
|
return NativeResult::Ok(JsValue::undefined());
|
||||||
|
}
|
||||||
|
let wrapper = unsafe { &*this_val.as_js_object_ptr() };
|
||||||
|
let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0;
|
||||||
|
let inner = match vm.ordinary_get(wrapper, inner_si, this_val) {
|
||||||
|
Ok(inner) if inner.is_object() => inner,
|
||||||
|
_ => return NativeResult::Ok(JsValue::undefined()),
|
||||||
|
};
|
||||||
|
let inner_obj = unsafe { &*inner.as_js_object_ptr() };
|
||||||
|
let return_si = vm.kernel_core().perm_interner().intern("return").0;
|
||||||
|
let return_fn = match vm.ordinary_get(inner_obj, return_si, inner) {
|
||||||
|
Ok(f) if is_callable(f) => f,
|
||||||
|
_ => return NativeResult::Ok(JsValue::undefined()),
|
||||||
|
};
|
||||||
|
match vm.call_function_sync(return_fn, inner, &[]) {
|
||||||
|
Ok(result) => NativeResult::Ok(result),
|
||||||
|
Err(err) => match vm.take_uncaught_value() {
|
||||||
|
Some(original) => NativeResult::Err(original),
|
||||||
|
None => NativeResult::Err(crate::error::create_type_error(vm, &err)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||||
if !this_val.is_object() {
|
if !this_val.is_object() {
|
||||||
@@ -66,7 +97,12 @@ pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
|||||||
};
|
};
|
||||||
return match vm.call_function_sync(next, inner, &[]) {
|
return match vm.call_function_sync(next, inner, &[]) {
|
||||||
Ok(result) => NativeResult::Ok(result),
|
Ok(result) => NativeResult::Ok(result),
|
||||||
Err(err) => NativeResult::Err(crate::error::create_type_error(vm, &err)),
|
// Forward the ORIGINAL thrown value (any type) instead of re-wrapping it as a
|
||||||
|
// TypeError, so a surrounding try/catch sees the real error.
|
||||||
|
Err(err) => match vm.take_uncaught_value() {
|
||||||
|
Some(original) => NativeResult::Err(original),
|
||||||
|
None => NativeResult::Err(crate::error::create_type_error(vm, &err)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ pub trait VmHost {
|
|||||||
|
|
||||||
// Call infrastructure
|
// Call infrastructure
|
||||||
fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result<JsValue, String>;
|
fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result<JsValue, String>;
|
||||||
|
/// Take the original thrown JsValue preserved across a String-flattening call boundary,
|
||||||
|
/// so an iterator wrapper can re-throw the original error instead of re-wrapping it.
|
||||||
|
fn take_uncaught_value(&mut self) -> Option<JsValue>;
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result<Option<*mut JsObject>, String>;
|
fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result<Option<*mut JsObject>, String>;
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ pub struct TryHandler {
|
|||||||
pub catch_pc: Option<usize>,
|
pub catch_pc: Option<usize>,
|
||||||
pub finally_pc: Option<usize>,
|
pub finally_pc: Option<usize>,
|
||||||
pub frame_depth: 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`.
|
/// Heap-allocated snapshot used by `call_bytecode_function_inline`.
|
||||||
@@ -164,6 +166,10 @@ pub struct Vm {
|
|||||||
pub(crate) save_stack: Vec<JsValue>,
|
pub(crate) save_stack: Vec<JsValue>,
|
||||||
pub(crate) try_stack: Vec<TryHandler>,
|
pub(crate) try_stack: Vec<TryHandler>,
|
||||||
pub(crate) exception_value: Option<JsValue>,
|
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_exception: Option<JsValue>,
|
||||||
pub(crate) pending_error_kind: Option<&'static str>,
|
pub(crate) pending_error_kind: Option<&'static str>,
|
||||||
pub(crate) root_reg_limit: u8,
|
pub(crate) root_reg_limit: u8,
|
||||||
@@ -635,7 +641,10 @@ impl Vm {
|
|||||||
self.regs = saved_regs;
|
self.regs = saved_regs;
|
||||||
return match result {
|
return match result {
|
||||||
NativeResult::Ok(val) => Ok(val),
|
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),
|
NativeResult::TailCall { callee, this, args } => self.call_function_sync(callee, this, &args),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1267,6 +1276,7 @@ mod tests {
|
|||||||
catch_pc: Some(1),
|
catch_pc: Some(1),
|
||||||
finally_pc: None,
|
finally_pc: None,
|
||||||
frame_depth: 0,
|
frame_depth: 0,
|
||||||
|
for_of_depth: 0,
|
||||||
});
|
});
|
||||||
vm.exception_value = Some(JsValue::int(2));
|
vm.exception_value = Some(JsValue::int(2));
|
||||||
vm.pending_exception = Some(JsValue::int(3));
|
vm.pending_exception = Some(JsValue::int(3));
|
||||||
@@ -1520,6 +1530,9 @@ impl oxide_runtime_api::VmHost for Vm {
|
|||||||
fn epoch(&self) -> &Epoch {
|
fn epoch(&self) -> &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 {
|
fn property_key_si(&mut self, val: JsValue) -> u32 {
|
||||||
self.property_key_si(val)
|
self.property_key_si(val)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ impl Vm {
|
|||||||
catch_pc,
|
catch_pc,
|
||||||
finally_pc: None,
|
finally_pc: None,
|
||||||
frame_depth: self.frames.len(),
|
frame_depth: self.frames.len(),
|
||||||
|
for_of_depth: self.iters.for_of_iters.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +194,7 @@ impl Vm {
|
|||||||
catch_pc: None,
|
catch_pc: None,
|
||||||
finally_pc: Some(finally_pc),
|
finally_pc: Some(finally_pc),
|
||||||
frame_depth: self.frames.len(),
|
frame_depth: self.frames.len(),
|
||||||
|
for_of_depth: self.iters.for_of_iters.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -402,17 +402,26 @@ impl Vm {
|
|||||||
return Err("FOR_OF_DONE iterator is not an object".into());
|
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 iter_obj = unsafe { &*iterator.as_js_object_ptr() };
|
||||||
let next_si = self.kernel_core.perm_interner().intern("next").0;
|
let next_si = self.kernel_core.perm_interner().intern("next").0;
|
||||||
let next_fn = self.ordinary_get(iter_obj, next_si, iterator)?;
|
let next_fn = match self.ordinary_get(iter_obj, next_si, iterator) {
|
||||||
let result = self.call_function_sync(next_fn, 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() {
|
if !result.is_object() {
|
||||||
return self.raise_type_error("iterator result is not an object");
|
return self.raise_type_error("iterator result is not an object");
|
||||||
}
|
}
|
||||||
|
|
||||||
let result_obj = unsafe { &*result.as_js_object_ptr() };
|
let result_obj = unsafe { &*result.as_js_object_ptr() };
|
||||||
let done_si = self.kernel_core.perm_interner().intern("done").0;
|
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);
|
let done = to_boolean(done_val);
|
||||||
self.iters.set_last_for_of_result(result);
|
self.iters.set_last_for_of_result(result);
|
||||||
self.regs[rd] = JsValue::bool(!done);
|
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> {
|
pub(crate) fn dispatch_for_of_next(&mut self, rd: usize) -> Result<(), String> {
|
||||||
vm_trace!("FOR_OF_NEXT rd={}", rd);
|
vm_trace!("FOR_OF_NEXT rd={}", rd);
|
||||||
|
self.last_uncaught_value = None;
|
||||||
let result = self.iters.last_for_of_result();
|
let result = self.iters.last_for_of_result();
|
||||||
if !result.is_object() {
|
if !result.is_object() {
|
||||||
self.regs[rd] = JsValue::undefined();
|
self.regs[rd] = JsValue::undefined();
|
||||||
@@ -428,30 +438,86 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
let result_obj = unsafe { &*result.as_js_object_ptr() };
|
let result_obj = unsafe { &*result.as_js_object_ptr() };
|
||||||
let value_si = self.kernel_core.perm_interner().intern("value").0;
|
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(())
|
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> {
|
pub(crate) fn dispatch_for_of_close(&mut self) -> Result<(), String> {
|
||||||
vm_trace!("FOR_OF_CLOSE");
|
vm_trace!("FOR_OF_CLOSE");
|
||||||
let Some(iterator) = self.iters.pop_for_of() else {
|
let Some(iterator) = self.iters.pop_for_of() else {
|
||||||
return Ok(());
|
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() {
|
if !iterator.is_object() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let iter_obj = unsafe { &*iterator.as_js_object_ptr() };
|
let iter_obj = unsafe { &*iterator.as_js_object_ptr() };
|
||||||
let return_si = self.kernel_core.perm_interner().intern("return").0;
|
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() {
|
if return_fn.is_object() {
|
||||||
let return_obj = unsafe { &*return_fn.as_js_object_ptr() };
|
let return_obj = unsafe { &*return_fn.as_js_object_ptr() };
|
||||||
if return_obj.is_function() {
|
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(())
|
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> {
|
pub(crate) fn dispatch_rest_object(&mut self, rd: usize, a: usize) -> Result<(), String> {
|
||||||
vm_trace!("REST_OBJECT rd={}", rd);
|
vm_trace!("REST_OBJECT rd={}", rd);
|
||||||
let src = self.regs[a];
|
let src = self.regs[a];
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ impl Vm {
|
|||||||
self.restore_frame(frame);
|
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 {
|
if let Some(finally_pc) = handler.finally_pc {
|
||||||
vm_trace!("unwind: entering finally at pc={}", finally_pc);
|
vm_trace!("unwind: entering finally at pc={}", finally_pc);
|
||||||
if self.pending_exception.is_none() {
|
if self.pending_exception.is_none() {
|
||||||
@@ -193,10 +195,14 @@ impl Vm {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.close_for_of_above(0);
|
||||||
while let Some(frame) = self.frames.pop() {
|
while let Some(frame) = self.frames.pop() {
|
||||||
self.restore_frame(frame);
|
self.restore_frame(frame);
|
||||||
}
|
}
|
||||||
let exc = self.exception_value.take().unwrap_or(JsValue::undefined());
|
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 kind_str = self.pending_error_kind.take().unwrap_or("Error");
|
||||||
let exc_text = self.error_text(exc);
|
let exc_text = self.error_text(exc);
|
||||||
let msg = if exc_text.starts_with(kind_str) {
|
let msg = if exc_text.starts_with(kind_str) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ impl Vm {
|
|||||||
save_stack: Vec::new(),
|
save_stack: Vec::new(),
|
||||||
try_stack: Vec::new(),
|
try_stack: Vec::new(),
|
||||||
exception_value: None,
|
exception_value: None,
|
||||||
|
last_uncaught_value: None,
|
||||||
pending_exception: None,
|
pending_exception: None,
|
||||||
pending_error_kind: None,
|
pending_error_kind: None,
|
||||||
root_reg_limit: 0,
|
root_reg_limit: 0,
|
||||||
@@ -95,6 +96,7 @@ impl Vm {
|
|||||||
save_stack: Vec::new(),
|
save_stack: Vec::new(),
|
||||||
try_stack: Vec::new(),
|
try_stack: Vec::new(),
|
||||||
exception_value: None,
|
exception_value: None,
|
||||||
|
last_uncaught_value: None,
|
||||||
pending_exception: None,
|
pending_exception: None,
|
||||||
pending_error_kind: None,
|
pending_error_kind: None,
|
||||||
root_reg_limit: 0,
|
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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user