Scripts: $.error keeps the last error thrown, and $.sleep throws in callbacks on the web

An exception that a finally block passes on, or one that leaves a
built-in's callback, no longer becomes $.error again over an error thrown
after it; throwing a caught error again still makes it the last one.

$.sleep with more than 0 inside a getter, a conversion or a built-in's
callback now throws whether or not the platform has a clock, so the web
build throws there as the other builds do.
This commit is contained in:
dotCooCoo
2026-11-11 04:48:13 -08:00
parent af22ac8f50
commit c746841839
5 changed files with 53 additions and 13 deletions
+17 -10
View File
@@ -591,6 +591,10 @@ impl Host {
/// The time since the run started; none without a clock.
fn elapsed(&self) -> Option<std::time::Duration> {
#[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<Option<Vec<std::path::PathBuf>>> = const { RefCell::new(None) };
/// Tests: the time since a run started, in place of its clock's.
pub(crate) static TEST_ELAPSED: Cell<Option<std::time::Duration>> = const { Cell::new(None) };
/// Tests: runs have no clock, as in a wasm build.
pub(crate) static TEST_NO_CLOCK: Cell<bool> = const { Cell::new(false) };
}
+14 -1
View File
@@ -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");
}
+9
View File
@@ -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()";
+12 -2
View File
@@ -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<crate::builtins::extensions::Localization>,
/// 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();
+1
View File
@@ -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)) => {