diff --git a/crates/expr/src/prelude.js b/crates/expr/src/prelude.js index fe95d097..aa202539 100644 --- a/crates/expr/src/prelude.js +++ b/crates/expr/src/prelude.js @@ -34,6 +34,15 @@ function __begin(t, v, c, l, path, uid, idx, fd, ti, tt, sv) { } function __finish(r) { + // Rethrow from this frame so the runtime's stack unwinds (see `Runtime::script`). + try { + return __result(r); + } catch (e) { + throw e; + } +} + +function __result(r) { r = __v(r); if (r !== null && typeof r === 'object' && r.__isLayer) return r.index; if (r !== null && typeof r === 'object' && r.__isTextStyle) return r.__out(); diff --git a/crates/expr/src/runtime.rs b/crates/expr/src/runtime.rs index fe45c268..aa699f42 100644 --- a/crates/expr/src/runtime.rs +++ b/crates/expr/src/runtime.rs @@ -236,8 +236,11 @@ impl Runtime { self.scripts.clear(); } // A block keeps `let`/`const` local to one evaluation; `{` shares line 1 with the code so - // error line numbers match the user's text. - let src = format!("{{{}\n}}", crate::rewrite::rewrite(text)); + // error line numbers match the user's text. Errors are rethrown from the script's own + // frame: boa leaves the stack of the inner frames behind when an exception leaves the + // script from a nested call (every host request does), and the thread's runtime would + // fail every expression with "reached the maximum stack size" after a few hundred. + let src = format!("try{{{}\n}}catch(__e){{throw __e}}", crate::rewrite::rewrite(text)); let s = Script::parse(Source::from_bytes(src.as_bytes()), None, &mut self.ctx).map_err(|e| format_error(&e.to_string(), line_count(text))); self.scripts.insert(text.to_string(), s.clone()); s diff --git a/crates/expr/src/tests.rs b/crates/expr/src/tests.rs index ac06485e..af7b7868 100644 --- a/crates/expr/src/tests.rs +++ b/crates/expr/src/tests.rs @@ -388,6 +388,22 @@ fn circular_references_terminate() { assert!(matches!(r, Ok(Value::Scalar(_))) || r.is_err(), "{r:?}"); } +#[test] +fn errors_and_property_reads_keep_the_runtime_usable() { + // Errors thrown inside helpers, and the aborts every property read makes until the host + // answers, must not use up the thread's runtime (it used to fail every expression after a + // few hundred with "reached the maximum stack size"). + let f = fx(); + for _ in 0..3000 { + assert!(f.ev(f.b, ROT, "thisComp.layer('Ghost').position", 0.0).is_err()); + } + for i in 0..1000 { + let t = i as f64 / 1000.0; + assert_close!(f.ev(f.b, ROT, "thisComp.layer('A').transform.rotation", t).unwrap().as_f64(), 90.0 * t); + } + assert_close!(f.num("time * 2", 1.0), 2.0); +} + #[test] fn effect_slider_reference() { let f = fx();