From 1677170333ed3f889b339aaaa34ddb15387dd8e0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 03:40:02 -0800 Subject: [PATCH] Scripts: a marked error a finally block leaves by return, break or continue is collected A marked host error whose finally block was left by return, break or continue kept its entry, and its error object, until the run ended. A script that discarded many of them reached the heap limit. Each entry now records the stack slot that holds its exception, and entries whose slot no longer holds it are removed before the next exception is handled and before each collection. --- crates/js/src/host_tests.rs | 21 +++++++++++++++++++++ crates/js/src/interp.rs | 25 +++++++++++++++++-------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/crates/js/src/host_tests.rs b/crates/js/src/host_tests.rs index 963f91776d..48aaa96106 100644 --- a/crates/js/src/host_tests.rs +++ b/crates/js/src/host_tests.rs @@ -292,6 +292,27 @@ fn a_marked_host_error_keeps_its_mark_only_while_no_catch_takes_it() { assert_eq!(r.error_mark(), None, "a later run that ends normally has no mark"); } +/// Marked errors that `finally` blocks discard with `return`, `break` or `continue` are collected +/// with the rest of the garbage, so a run that discards many of them stays within its heap. +#[test] +fn marked_errors_a_finally_block_discards_are_collected() { + let mut r = Realm::new(); + r.set_limits(Limits { fuel: 200_000_000, heap_bytes: 1 << 20, ..Limits::default() }); + let f: HostFunction = Rc::new(|_| Err(HostError::Marked { error: Box::new(HostError::Syntax("bad file".into())), mark: 7 })); + r.define_host_object("host", vec![("f".into(), HostItem::Function(f))]).unwrap(); + let cases = [ + "function d() { try { host.f(); } finally { return; } }\nfor (var i = 0; i < 20000; i++) d();", + "for (var i = 0; i < 20000; i++) { try { host.f(); } finally { continue; } }", + "for (var i = 0; i < 20000; i++) { do { try { host.f(); } finally { break; } } while (false); }", + ]; + for source in cases { + r.run(source).unwrap_or_else(|e| panic!("{source}: {e:?}")); + assert_eq!(r.error_mark(), None, "{source}"); + } + assert!(r.run("try { host.f(); } finally { 1; }").is_err()); + assert_eq!(r.error_mark(), Some(7)); +} + #[test] fn an_error_says_where_in_the_source_it_was_raised() { let mut r = realm(1_000_000); diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 0e2f1e7ed9..e3cb102c72 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -201,10 +201,10 @@ pub(crate) struct Interp { /// propagating. pub(crate) thrown_mark: Option, /// The marked exceptions `finally` blocks are running for, innermost last, each with its mark, - /// which the block's end throws it on with. A block left by `break`, `continue` or `return` - /// leaves its entry until the run ends, so there are at most as many as the host errors the - /// run's host marked. - pub(crate) finally_marks: Vec<(ObjId, u32)>, + /// which the block's end throws it on with, and the stack slot that holds it while the block + /// runs. An entry whose slot no longer holds its exception belongs to a block that `break`, + /// `continue` or `return` left, and [`Self::drop_left_finally_marks`] removes it. + pub(crate) finally_marks: Vec<(ObjId, u32, usize)>, /// The mark of the host error the last run ended with. pub(crate) error_mark: Option, } @@ -560,12 +560,20 @@ impl Interp { Ok(()) } + /// Removes the [`Self::finally_marks`] entries of `finally` blocks that `break`, `continue` or + /// `return` left: those whose stack slot no longer holds their exception. + fn drop_left_finally_marks(&mut self) { + let stack = &self.stack; + self.finally_marks.retain(|&(id, _, slot)| matches!(stack.get(slot), Some(Value::Object(o)) if *o == id)); + } + pub(crate) fn collect_garbage(&mut self) { + self.drop_left_finally_marks(); let mut roots = Roots::default(); roots.values.extend(self.stack.iter()); roots.values.extend(self.temp_roots.iter()); roots.values.push(&self.last_thrown); - roots.objects.extend(self.finally_marks.iter().map(|&(id, _)| id)); + roots.objects.extend(self.finally_marks.iter().map(|&(id, _, _)| id)); for f in &self.frames { roots.values.push(&f.this); roots.values.push(&f.completion); @@ -668,8 +676,9 @@ impl Interp { return internal("handler stack deeper than the stack"); } self.stack.truncate(target_len); + self.drop_left_finally_marks(); if let (Some(mark), false, Value::Object(id)) = (self.thrown_mark.take(), caught, &v) { - self.finally_marks.push((*id, mark)); + self.finally_marks.push((*id, mark, target_len)); } self.stack.push(v); self.throw_site = None; @@ -859,9 +868,9 @@ impl Interp { if kind == 1.0 { let v = self.pop()?; if let Value::Object(o) = v - && let Some(i) = self.finally_marks.iter().rposition(|&(id, _)| id == o) + && let Some(i) = self.finally_marks.iter().rposition(|&(id, _, _)| id == o) { - self.thrown_mark = self.finally_marks.get(i).map(|&(_, mark)| mark); + self.thrown_mark = self.finally_marks.get(i).map(|&(_, mark, _)| mark); self.finally_marks.truncate(i); } return Err(Abrupt::Throw(v));