From 244cee46877d7916bc9680ae5191bf0b3a0a1a60 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 4 Oct 2026 00:19:22 +0700 Subject: [PATCH] =?UTF-8?q?M8.1:=20expressions=20keep=20working=20on=20lon?= =?UTF-8?q?g-lived=20threads=20=E2=80=94=20boa=20leaves=20values=20on=20it?= =?UTF-8?q?s=20VM=20stack=20when=20an=20exception=20unwinds=20through=20ca?= =?UTF-8?q?ll=20frames=20(every=20pending=20host=20request=20does),=20so?= =?UTF-8?q?=20after=20tens=20to=20hundreds=20of=20evaluations=20that=20rea?= =?UTF-8?q?d=20layers=20or=20effects=20every=20expression=20on=20the=20thr?= =?UTF-8?q?ead=20failed=20with=20"maximum=20stack=20size";=20a=20run=20tha?= =?UTF-8?q?t=20hits=20the=20limit=20now=20gets=20a=20fresh=20context=20and?= =?UTF-8?q?=20is=20retried=20once=20(regression=20test:=206000=20host-read?= =?UTF-8?q?ing=20evaluations=20on=20one=20thread)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/expr/src/runtime.rs | 27 ++++++++++++++++++++++++--- crates/expr/src/tests.rs | 13 +++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/expr/src/runtime.rs b/crates/expr/src/runtime.rs index fe45c268..24416b89 100644 --- a/crates/expr/src/runtime.rs +++ b/crates/expr/src/runtime.rs @@ -16,6 +16,8 @@ const PRELUDE: &str = include_str!("prelude.js"); /// Message of the error thrown when a request isn't answered yet (never shown to users). const PENDING: &str = "\u{1}effectcraft: pending host request"; const MAX_SCRIPTS: usize = 4096; +/// The part of boa's stack-size limit error the runtime recovers from (see [`run`]). +const STACK_LIMIT: &str = "maximum stack size"; /// Memoized host answers and pending misses of one expression evaluation. #[derive(Default)] @@ -392,13 +394,32 @@ fn position_line(msg: &str) -> Option { } /// Run `text` once on this thread's runtime. +/// +/// boa leaves values on its VM stack when an exception unwinds through call frames, and every +/// pending host request does that (the request throws out of the object-model functions), so a +/// long-lived context eventually fails every run with "maximum stack size". When a run hits that +/// limit the context is replaced with a fresh one and the run retried once: each run starts from +/// `__begin` with memoized answers, so nothing carries over. A script that really overflows the +/// stack fails again on the fresh context and reports the error. pub fn run(text: &str, b: &Begin) -> Run { RUNTIME.with(|r| { let mut r = r.borrow_mut(); - match &mut **r.get_or_insert_with(|| ManuallyDrop::new(Runtime::new())) { - Ok(rt) => rt.run(text, b), - Err(e) => Run::Done(Err(e.clone())), + for fresh in [false, true] { + let out = match &mut **r.get_or_insert_with(|| ManuallyDrop::new(Runtime::new())) { + Ok(rt) => rt.run(text, b), + Err(e) => return Run::Done(Err(e.clone())), + }; + match out { + Run::Done(Err(e)) if !fresh && e.contains(STACK_LIMIT) => { + // Dropped here, mid-thread, where boa's heap is alive (unlike at thread exit). + if let Some(old) = r.take() { + drop(ManuallyDrop::into_inner(old)); + } + } + out => return out, + } } + Run::Done(Err(format!("Error: {STACK_LIMIT}"))) }) } diff --git a/crates/expr/src/tests.rs b/crates/expr/src/tests.rs index ac06485e..1f8ae7a2 100644 --- a/crates/expr/src/tests.rs +++ b/crates/expr/src/tests.rs @@ -1131,3 +1131,16 @@ fn project_and_time_globals() { assert_close!(f.num("thisLayer.marker.key(1).protectedRegion ? 1 : 0", 0.0), 1.0); assert_eq!(f.text("thisComp.layer(\"A\").marker.key(1).cuePointName + thisComp.layer(\"A\").marker.key(1).parameters.k", 0.0), "gov"); } + +#[test] +fn thousands_of_host_reads_on_one_thread_keep_working() { + // Every pending host request unwinds through call frames, which leaks boa stack slots; the + // runtime replaces a context that fills up instead of failing every later expression. + let f = fx(); + for i in 0..2000 { + let t = (i % 20) as f64 * 0.05; + assert_eq!(f.num("effect(\"Slider Control\")(\"Slider\")", t), 42.0, "evaluation {i}"); + assert!(close(f.num("thisComp.layer(\"A\").transform.rotation.valueAtTime(1)", t), 90.0), "evaluation {i}"); + assert_eq!(f.num("function a() { return effect(1)(1); } function b() { return a(); } b() + 1", t), 43.0, "evaluation {i}"); + } +}