diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index d9a351c77..af17e6586 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -300,6 +300,12 @@ pub(crate) const MAX_APPLY_ARGS: usize = 1 << 16; /// The estimated bytes of a name's entries in the list and the set `for-in` builds. const KEY_ENTRY_BYTES: usize = 32; +#[cfg(test)] +thread_local! { + /// Tests: the [`Interp::finally_sites`] entries that leaving a block or a frame has looked at. + pub(crate) static SITE_CHECKS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + /// The estimated bytes of the index names `0` to `len - 1` while `for-in` lists them. fn index_keys_bytes(len: usize) -> usize { let mut total = 0usize; @@ -614,9 +620,16 @@ impl Interp { } /// Removes the [`Self::finally_sites`] entries whose stack slot is at or above `len`: those of - /// `finally` blocks that were left. + /// `finally` blocks that were left. The slots rise along the list, so only its end is looked at. fn drop_left_finally_sites(&mut self, len: usize) { - self.finally_sites.retain(|&(_, slot)| slot < len); + while let Some(&(_, slot)) = self.finally_sites.last() { + #[cfg(test)] + SITE_CHECKS.with(|c| c.set(c.get() + 1)); + if slot < len { + break; + } + self.finally_sites.pop(); + } } pub(crate) fn collect_garbage(&mut self) { diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index 9d673dc78..28157d6ee 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -396,6 +396,19 @@ fn a_function_that_returns_from_a_finally_block_does_not_keep_its_code() { assert!(thrown < plain + 4096, "{thrown} > {plain}"); } +#[test] +fn a_return_looks_only_at_the_finally_sites_of_its_own_frame() { + // 200 finally blocks run at once, each for its own exception, and the innermost calls a + // function 1,000 times. Each return looks at the last saved site only, not at all 200. + let src = "function g() { return 1; }\n\ + function f(n) { try { throw n; } finally { if (n) { f(n - 1); } else { for (var i = 0; i < 1000; i++) g(); } } }\n\ + try { f(199); } catch (e) { e; }"; + crate::interp::SITE_CHECKS.with(|c| c.set(0)); + assert_eq!(small(Limits::default()).run(src), Ok(JsValue::Number(0.0))); + let checks = crate::interp::SITE_CHECKS.with(std::cell::Cell::get); + assert!(checks < 10_000, "{checks} saved sites looked at"); +} + #[test] fn for_in_names_take_fuel_and_heap_under_later_limits() { // The first run, under the default limits, builds an object with many properties. Later runs