diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index d82cccaf1..125464d9f 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -591,6 +591,10 @@ impl Host { /// The time since the run started; none without a clock. fn elapsed(&self) -> Option { + #[cfg(test)] + if TEST_NO_CLOCK.with(Cell::get) { + return None; + } #[cfg(test)] if let Some(d) = TEST_ELAPSED.with(Cell::get) { return Some(d); @@ -600,17 +604,18 @@ impl Host { /// `$.sleep`: ends the slice, and the run does not go on until `d` has passed. Without a clock /// it does not wait. A getter, a conversion, a built-in's callback or a file `$.evalFile` runs - /// cannot stop at the call, so a wait there is an error. + /// cannot stop at the call, so a wait there is an error, with or without a clock. fn sleep(&self, d: std::time::Duration) -> std::result::Result<(), HostError> { - if let Some(now) = self.elapsed() - && !d.is_zero() - { - if !self.can_wait.get() { - return Err(HostError::Error( - "$.sleep: a run cannot wait inside a callback of a built-in function, a getter, a conversion or a file $.evalFile runs; call it from the script's own code" - .into(), - )); - } + if d.is_zero() { + return Ok(()); + } + if !self.can_wait.get() { + return Err(HostError::Error( + "$.sleep: a run cannot wait inside a callback of a built-in function, a getter, a conversion or a file $.evalFile runs; call it from the script's own code" + .into(), + )); + } + if let Some(now) = self.elapsed() { self.wake_at.set(Some(now.saturating_add(d))); self.pause.set(true); } @@ -1752,4 +1757,6 @@ thread_local! { pub(crate) static TEST_JSINCLUDE: RefCell>> = const { RefCell::new(None) }; /// Tests: the time since a run started, in place of its clock's. pub(crate) static TEST_ELAPSED: Cell> = const { Cell::new(None) }; + /// Tests: runs have no clock, as in a wasm build. + pub(crate) static TEST_NO_CLOCK: Cell = const { Cell::new(false) }; } diff --git a/crates/engine/src/tests/script_dollar.rs b/crates/engine/src/tests/script_dollar.rs index 07dbb5381..d5b962450 100644 --- a/crates/engine/src/tests/script_dollar.rs +++ b/crates/engine/src/tests/script_dollar.rs @@ -5,7 +5,7 @@ use std::time::Duration; use serde_json::{Value, json}; -use crate::script::TEST_ELAPSED; +use crate::script::{TEST_ELAPSED, TEST_NO_CLOCK}; use crate::*; /// The values `source` gives `vc.log`, in a run of its text. @@ -232,3 +232,16 @@ fn sleep_throws_where_the_run_cannot_wait_instead_of_running_on() { let message = "Error: $.sleep: a run cannot wait inside a callback of a built-in function, a getter, a conversion or a file $.evalFile runs; call it from the script's own code"; assert_eq!(r["log"], json!([message, message, "done"])); } + +#[test] +fn sleep_throws_where_the_run_cannot_wait_without_a_clock_too() { + let mut s = Session::new(); + TEST_NO_CLOCK.set(true); + let source = "var o = {}; Object.defineProperty(o, 'x', { get: function () { $.sleep(250); return 1; } }); + try { o.x; vc.log('no error'); } catch (e) { vc.log(e.name); } + $.sleep(250); + vc.log('done');"; + let log = logged(&mut s, source); + TEST_NO_CLOCK.set(false); + assert_eq!(log, json!(["Error", "done"]), "the script's own code does not wait without a clock"); +} diff --git a/crates/js/src/extension_tests.rs b/crates/js/src/extension_tests.rs index eb94e678a..19a20ab5b 100644 --- a/crates/js/src/extension_tests.rs +++ b/crates/js/src/extension_tests.rs @@ -159,6 +159,15 @@ fn script_items_read_the_global_object_the_last_error_the_line_and_the_stack() { let source = "var before = h.error; try { null.x; } catch (e) {} var caught = h.error; try { throw 'text'; } catch (e) {} [before === null, caught instanceof TypeError, h.error === caught].join()"; assert_eq!(text(&mut r, source), "true,true,true"); + let source = "try { try { throw new RangeError('first'); } finally { try { throw new TypeError('second'); } catch (e) {} } } catch (e) {} + h.error.message"; + assert_eq!(text(&mut r, source), "second", "an exception a finally block passes on is not thrown again"); + let source = "try { [1].map(function () { try { throw new RangeError('first'); } finally { try { throw new TypeError('second'); } catch (e) {} } }); } catch (e) {} + h.error.message"; + assert_eq!(text(&mut r, source), "second", "nor is one that leaves a built-in's callback"); + let source = "var kept; try { throw new RangeError('first'); } catch (e) { kept = e; } try { throw new TypeError('second'); } catch (e) {} + try { throw kept; } catch (e) {} h.error.message"; + assert_eq!(text(&mut r, source), "first", "throwing a caught error again makes it the last one"); assert_eq!(r.run("h.error").unwrap(), JsValue::Null, "each run starts without one"); assert_eq!(r.run("\n\n h.line").unwrap(), JsValue::Number(3.0)); let source = "function outer() {\n return inner();\n}\nfunction inner() {\n return h.stack;\n}\nvar f = function () { return outer(); };\nf()"; diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index ff7100277..18ccb3f0d 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -166,6 +166,9 @@ pub(crate) struct Interp { pub(crate) last_thrown: Value, /// The last `Error` object thrown in this run, caught or not, or `null` (`$.error`). pub(crate) last_error: Value, + /// An exception passed on rather than thrown again: one a `finally` block restores, or one + /// leaving a nested run. [`Interp::unwind`] does not make it [`Interp::last_error`] again. + pub(crate) passing: Value, /// The locale `localize` and `Object.prototype.toString` read. pub(crate) localization: Rc, /// The state of `Math.random`'s generator. @@ -386,6 +389,7 @@ impl Interp { throw_site: None, last_thrown: Value::Undefined, last_error: Value::Null, + passing: Value::Undefined, localization: Rc::default(), random: random_seed(), join_stack: Vec::new(), @@ -562,6 +566,7 @@ impl Interp { roots.values.extend(self.temp_roots.iter()); roots.values.push(&self.last_thrown); roots.values.push(&self.last_error); + roots.values.push(&self.passing); for f in &self.frames { roots.values.push(&f.this); roots.values.push(&f.completion); @@ -647,7 +652,9 @@ impl Interp { /// boundary frame. Returns the exception when no handler takes it before the boundary frame. fn unwind(&mut self, v: Value) -> JsResult<()> { self.note_site(); - if let Value::Object(o) = v + let passed = matches!((&v, std::mem::replace(&mut self.passing, Value::Undefined)), (Value::Object(a), Value::Object(b)) if *a == b); + if !passed + && let Value::Object(o) = v && self.heap.object(o).is_some_and(|o| matches!(o.class, Class::Error)) { self.last_error = v.clone(); @@ -674,6 +681,7 @@ impl Interp { let Some(frame) = self.frames.pop() else { return Err(Abrupt::Throw(v)) }; self.stack.truncate(frame.base); if frame.boundary { + self.passing = v.clone(); return Err(Abrupt::Throw(v)); } } @@ -868,7 +876,9 @@ impl Interp { return self.pop().map(drop); } if kind == 1.0 { - return Err(Abrupt::Throw(self.pop()?)); + let v = self.pop()?; + self.passing = v.clone(); + return Err(Abrupt::Throw(v)); } let k = (kind - 2.0) as usize; let target = self.frame()?.code.finally_tables.get(table as usize).and_then(|t| t.get(k)).copied(); diff --git a/crates/js/src/realm.rs b/crates/js/src/realm.rs index 62abd7a79..8951bbf2d 100644 --- a/crates/js/src/realm.rs +++ b/crates/js/src/realm.rs @@ -258,6 +258,7 @@ impl Realm { interp.temp_roots.clear(); interp.join_stack.clear(); interp.waiting = false; + interp.passing = Value::Undefined; match result { Ok(v) => Ok(interp.to_js_value(&v)), Err(Abrupt::Throw(v)) => {