mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 20:01:43 +00:00
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:
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
Reference in New Issue
Block a user