JavaScript: a finally block keeps the try block's completion value, and host strings count toward the heap

In script and eval code a finally block that ends normally leaves the completion value of its try or catch block (ECMA-262 5.1 section 12.14), so try { 1; } finally { 2; } completes with 1. A new GetCompletion instruction saves the value when the finally block starts, and SetCompletion sets it back when the block ends normally; a break, continue or throw out of the finally block still replaces it.

A string returned by a host function is charged to the heap, and one that does not fit after a collection ends the run with the heap limit.
This commit is contained in:
dotCooCoo
2026-11-07 13:32:53 -08:00
parent d16ff70fd3
commit dfd0b1c949
4 changed files with 64 additions and 6 deletions
+4 -1
View File
@@ -142,6 +142,9 @@ pub(crate) enum Op {
ForInNext(u32),
/// Pops the value of an expression statement into the completion value.
SetCompletion,
/// Pushes the completion value, which a `finally` block in script and eval code keeps and
/// sets back with `SetCompletion` when it ends normally.
GetCompletion,
}
/// What code is: a function body, a script, or eval code.
@@ -237,7 +240,7 @@ fn effect(op: Op, finally_tables: &[Vec<u32>]) -> Result<Effect, String> {
Op::GetVar(_) | Op::TypeofVar(_) | Op::DeleteVar(_) | Op::IncVar { .. } | Op::RefVar(_) => e(0, 1),
Op::GetRef(_) => e(1, 1),
Op::PutRef(_) => e(2, 1),
Op::NewObject | Op::NewArray | Op::Closure(_) | Op::RegExp(_) => e(0, 1),
Op::NewObject | Op::NewArray | Op::Closure(_) | Op::RegExp(_) | Op::GetCompletion => e(0, 1),
Op::Pop | Op::PushWith | Op::PushCatch(_) | Op::SetCompletion => e(1, 0),
Op::PopUnder(n) => e(n.checked_add(1).ok_or("operand too large")?, 1),
Op::Dup => e(1, 2),
+9
View File
@@ -708,7 +708,16 @@ impl<'a> FnCompiler<'a> {
self.emit(Op::Undefined);
self.push_number(0.0);
self.bind(fin_entry);
// In script and eval code the finally block keeps the completion value of the try or
// catch block when it ends normally (ECMA-262 5.1 section 12.14).
let keeps_completion = self.kind != CodeKind::Function;
if keeps_completion {
self.emit(Op::GetCompletion);
}
self.stmts(fin)?;
if keeps_completion {
self.emit(Op::SetCompletion);
}
let dispatch: Vec<Label> = exits.iter().map(|_| self.new_label()).collect();
let table = u32::try_from(self.finally_tables.len()).unwrap_or(u32::MAX);
self.finally_tables.push(dispatch.clone());
+17 -5
View File
@@ -586,6 +586,10 @@ impl Interp {
self.frame_mut()?.completion = v;
Ok(())
}
Op::GetCompletion => {
let v = self.frame()?.completion.clone();
self.push(v)
}
Op::Return | Op::ReturnCompletion => internal("return reached the dispatcher"),
}
}
@@ -1452,7 +1456,7 @@ impl Interp {
Func::Host { f: host, .. } => {
let js_args: Vec<crate::JsValue> = args.iter().map(|v| self.to_js_value(v)).collect();
return match host(&js_args) {
Ok(v) => Ok(Prepared::Done(self.from_js_value(v))),
Ok(v) => Ok(Prepared::Done(self.from_js_value(v)?)),
Err(message) => Err(self.throw(ErrorType::Error, &message)),
};
}
@@ -2087,16 +2091,24 @@ impl Interp {
}
}
/// A host value as a JavaScript value. A string is charged to the heap, and one that does not
/// fit in it ends the run with the heap limit. Everything the caller still needs must be
/// reachable from the roots.
#[allow(clippy::wrong_self_convention)]
pub(crate) fn from_js_value(&mut self, v: crate::JsValue) -> Value {
match v {
pub(crate) fn from_js_value(&mut self, v: crate::JsValue) -> JsResult<Value> {
Ok(match v {
crate::JsValue::Undefined => Value::Undefined,
crate::JsValue::Null => Value::Null,
crate::JsValue::Bool(b) => Value::Bool(b),
crate::JsValue::Number(n) => Value::Number(n),
crate::JsValue::String(s) => Value::str(&s),
crate::JsValue::String(s) => {
let bytes = string_bytes(s.encode_utf16().count());
self.reserve_heap(bytes)?;
self.heap.bytes += bytes;
Value::str(&s)
}
crate::JsValue::Object(_) => Value::Object(self.new_object()),
}
})
}
/// A description of a thrown value that runs no JavaScript: `Name: message` for an error,
+34
View File
@@ -162,6 +162,21 @@ fn control_flow_with_labels_switch_and_finally() {
assert_eq!(num("function f() { var r = 0; with ({a: 1}) { try { return a; } finally { r = 1; } } } f()"), 1.0);
}
#[test]
fn a_finally_block_keeps_the_completion_value_unless_it_ends_abruptly() {
// Section 12.14: a finally block that ends normally leaves the try or catch block's value.
let cases = [
("try { 1; } finally { 2; }", 1.0),
("try { throw 0; } catch (e) { 1; } finally { 2; }", 1.0),
("eval('try { 1; } finally { 2; }')", 1.0),
("try { try { 1; } finally { 2; } } finally { 3; }", 1.0),
("do { try { 1; } finally { 2; break; } } while (false)", 2.0),
("do { try { 1; } finally { 2; continue; } } while (false)", 2.0),
];
let wrong: Vec<(&str, f64, f64)> = cases.iter().map(|&(src, want)| (src, want, num(src))).filter(|&(_, want, got)| want != got).collect();
assert!(wrong.is_empty(), "{wrong:?}");
}
#[test]
fn with_eval_and_new_function() {
assert_eq!(num("var o = {a: 2}; with (o) { a = 3; } o.a"), 3.0);
@@ -243,6 +258,25 @@ fn limit(realm: &mut Realm, src: &str) -> LimitKind {
}
}
fn with_big(limits: Limits) -> Realm {
let mut realm = small(limits);
realm.define_function("big", |args| match args {
[JsValue::Number(n)] => Ok(JsValue::String("x".repeat(*n as usize))),
_ => Err("big takes a length".into()),
});
realm
}
#[test]
fn strings_from_host_functions_count_toward_the_heap() {
let limits = Limits { heap_bytes: 1 << 20, fuel: 100_000, ..Limits::default() };
let mut realm = with_big(limits.clone());
assert_eq!(realm.run("big(1000).length"), Ok(JsValue::Number(1000.0)));
assert_eq!(realm.run("var a = []; for (var i = 0; i < 4; i++) a[i] = big(50000); a[3].length"), Ok(JsValue::Number(50000.0)));
assert_eq!(limit(&mut with_big(limits.clone()), "var a = []; for (var i = 0; ; i++) a[i] = big(100000);"), LimitKind::Heap);
assert_eq!(limit(&mut with_big(limits), "big(1000000).length"), LimitKind::Heap);
}
#[test]
fn an_infinite_loop_ends_with_the_fuel_limit() {
let mut realm = small(Limits { fuel: 100_000, ..Limits::default() });