Expressions: keep the runtime usable after errors and property reads

boa leaves the value stack of inner call frames behind when an exception
leaves a script (or a function called from Rust) from a nested call. Every
property read throws the pending-host-request abort from inside a prelude
helper, and so does any helper error (`thisComp.layer("Ghost")`), so the
thread's runtime filled up after ~100 evaluations of a pick-whip
expression: from then on every expression on that thread failed with
"RuntimeLimitError: reached the maximum stack size" and silently fell back
to its keyframed value (e.g. a follower layer stops following after ~90
render_frame calls).

Wrap the script and `__finish` in try/catch and rethrow from their own
frame, which unwinds the stack; backtraces (error line numbers) survive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
storybored379
2026-10-03 09:31:41 -07:00
co-authored by Claude Opus 5.5
parent 4bfb689f19
commit 0ae7a1925a
3 changed files with 30 additions and 2 deletions
+5 -2
View File
@@ -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