mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 06:06:38 +00:00
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:
@@ -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),
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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() });
|
||||
|
||||
Reference in New Issue
Block a user