Files
vectorcraft/crates/js/src/bytecode.rs
T
dotCooCoo dfd0b1c949 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.
2026-11-07 13:32:53 -08:00

363 lines
14 KiB
Rust

//! Bytecode: the instructions a function, a script or eval code compiles to, and the verifier
//! that checks them before they run.
//!
//! The interpreter is a stack machine. Each instruction takes its operands from the top of the
//! frame's operand stack and pushes its result there; jumps name an instruction index. The
//! verifier walks every path through the code and checks that each jump target, exception handler
//! and `finally` dispatch target is an instruction, that each constant, name and function index is
//! in its table, that the stack never goes below the frame's base, that every path reaching an
//! instruction arrives with the same stack depth, and that no path runs past the last
//! instruction. Code that fails is reported as an internal error and never runs.
use std::rc::Rc;
use crate::LineTable;
use crate::ast::BinaryOp;
use crate::heap::Key;
use crate::value::Str;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Op {
Undefined,
Null,
True,
False,
/// Pushes `numbers[i]`.
Number(u32),
/// Pushes `strings[i]`.
String(u32),
This,
Pop,
/// Removes the `n` values under the top one.
PopUnder(u32),
Dup,
/// `a b` → `a b a b`.
Dup2,
Swap,
/// `a b c` → `b c a`.
Rot3,
/// Pushes the value of the variable `names[i]`.
GetVar(u32),
/// Pushes the value of the variable and the `this` a call to it gets.
GetVarThis(u32),
/// `value` → `value`, assigned to the variable.
SetVar(u32),
/// Pushes the base of the reference to the variable `names[i]`, resolved before the
/// right-hand side of an assignment runs.
RefVar(u32),
/// `base` → `value`: the variable's value read through the resolved base.
GetRef(u32),
/// `base value` → `value`, assigned to the variable through the resolved base.
PutRef(u32),
TypeofVar(u32),
DeleteVar(u32),
IncVar {
name: u32,
prefix: bool,
dec: bool,
},
/// `object` → `object.name`.
GetMember(u32),
/// `object` → `object.name object`.
GetMemberThis(u32),
/// `object value` → `value`.
SetMember(u32),
DeleteMember(u32),
IncMember {
name: u32,
prefix: bool,
dec: bool,
},
/// `object key` → `object name`: checks that the object can have properties and turns the key
/// into a property name, before the right-hand side of an assignment runs.
ToKey,
/// `object` → `object`: throws a `TypeError` when it is `undefined` or `null`, before the
/// right-hand side of an assignment to one of its properties runs.
CheckObject,
/// `object key` → `object[key]`.
GetIndex,
/// `object key` → `object[key] object`.
GetIndexThis,
/// `object key value` → `value`.
SetIndex,
DeleteIndex,
IncIndex {
prefix: bool,
dec: bool,
},
/// `function this arg…` → result.
Call(u32),
/// Like `Call`, but a direct eval when the function is the global `eval`.
CallEval(u32),
/// `function arg…` → the new object.
New(u32),
Binary(BinaryOp),
Neg,
Plus,
BitNot,
Not,
Typeof,
ToNumber,
Jump(u32),
/// Pops the top value and jumps when it is falsy.
JumpIfFalse(u32),
JumpIfTrue(u32),
/// Jumps, keeping the top value, when it is falsy; otherwise pops it.
JumpIfFalseKeep(u32),
JumpIfTrueKeep(u32),
Return,
/// Returns the value of the last expression statement (script and eval code).
ReturnCompletion,
Throw,
/// Throws a `ReferenceError` with the message `strings[i]` (an assignment to something that
/// cannot be assigned to).
ThrowReferenceError(u32),
/// Ends a `finally` block: `value kind` on the stack say how it was entered. Kind 0 (normal)
/// pops both and goes on, kind 1 throws the value, and kind `2 + k` jumps to entry `k` of
/// `finally_tables[i]` with the value left on the stack.
EndFinally(u32),
NewObject,
NewArray,
/// `array value` → `array`, the value appended.
ArrayPush,
/// `array` → `array`, one longer.
ArrayHole,
/// `object value` → `object`.
InitProp(u32),
/// `object function` → `object`.
InitGetter(u32),
InitSetter(u32),
/// Pushes a new function made from `functions[i]` and the current environment.
Closure(u32),
/// Pushes a new regular expression object for `regexps[i]`.
RegExp(u32),
/// `object` → nothing: enters a `with` statement's environment.
PushWith,
/// `exception` → nothing: enters a catch clause's environment, binding `names[i]`.
PushCatch(u32),
PopEnv,
/// `object` → `iterator`.
ForInStart,
/// `iterator` → `iterator name`, or jumps with `iterator` when the names are used up.
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.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CodeKind {
Function,
Global,
Eval,
}
/// A range of instructions an exception thrown in jumps out of: to `target`, with the operand
/// stack cut back to `depth` values and the environments to `env_depth`, and the exception pushed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Handler {
pub(crate) start: u32,
pub(crate) end: u32,
pub(crate) target: u32,
pub(crate) depth: u32,
pub(crate) env_depth: u32,
}
/// The source a piece of code came from, for error positions and `Function.prototype.toString`.
#[derive(Debug)]
pub(crate) struct Source {
pub(crate) lines: LineTable,
pub(crate) text: Rc<[u16]>,
}
#[derive(Debug)]
pub(crate) struct Code {
pub(crate) ops: Vec<Op>,
/// The source offset each instruction comes from.
pub(crate) positions: Vec<u32>,
pub(crate) numbers: Vec<f64>,
pub(crate) strings: Vec<Str>,
pub(crate) names: Vec<Key>,
pub(crate) functions: Vec<Rc<Code>>,
pub(crate) regexps: Vec<(Str, Str)>,
pub(crate) handlers: Vec<Handler>,
pub(crate) finally_tables: Vec<Vec<u32>>,
/// The most values the code keeps on the operand stack, set by the verifier.
pub(crate) max_stack: u32,
pub(crate) strict: bool,
/// A function expression's own name, bound inside it.
pub(crate) self_name: Option<Key>,
pub(crate) params: Vec<Key>,
pub(crate) vars: Vec<Key>,
/// Function declarations to instantiate on entry: their names and indices in `functions`.
pub(crate) fun_decls: Vec<(Key, u32)>,
/// The body refers to `arguments` or calls `eval`.
pub(crate) uses_arguments: bool,
pub(crate) source: Rc<Source>,
/// Where the function's text starts and ends in the source.
pub(crate) span: (u32, u32),
}
impl Code {
/// The innermost handler around instruction `pc`.
pub(crate) fn handler(&self, pc: usize) -> Option<Handler> {
self.handlers.iter().copied().find(|h| (h.start as usize) <= pc && pc < h.end as usize)
}
/// The line and column of instruction `pc`.
pub(crate) fn line_column(&self, pc: usize) -> (u32, u32) {
let offset = self.positions.get(pc).copied().unwrap_or(self.span.0);
self.source.lines.line_column(offset)
}
}
/// What an instruction does to the stack on each way out of it.
struct Effect {
pops: u32,
/// Values pushed when it goes on to the next instruction; `None` when it never does.
next: Option<u32>,
/// Instruction indices it may jump to, each with the values it pushes on that path.
jumps: Vec<(u32, u32)>,
}
/// The values an instruction pops, and pushes when it goes on to the next instruction (0 for one
/// that never does), for the compiler's count of the stack depth.
pub(crate) fn fallthrough_effect(op: Op) -> (u32, u32) {
match op {
Op::EndFinally(_) => (2, 0),
_ => effect(op, &[]).map_or((0, 0), |fx| (fx.pops, fx.next.unwrap_or(0))),
}
}
fn effect(op: Op, finally_tables: &[Vec<u32>]) -> Result<Effect, String> {
let e = |pops, pushes| Effect { pops, next: Some(pushes), jumps: Vec::new() };
let end = |pops| Effect { pops, next: None, jumps: Vec::new() };
Ok(match op {
Op::Undefined | Op::Null | Op::True | Op::False | Op::Number(_) | Op::String(_) | Op::This => e(0, 1),
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(_) | 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),
Op::Dup2 => e(2, 4),
Op::Swap | Op::ToKey => e(2, 2),
Op::Rot3 => e(3, 3),
Op::GetVarThis(_) => e(0, 2),
Op::SetVar(_) | Op::GetMember(_) | Op::DeleteMember(_) | Op::IncMember { .. } => e(1, 1),
Op::Neg | Op::Plus | Op::BitNot | Op::Not | Op::Typeof | Op::ToNumber | Op::ArrayHole | Op::ForInStart | Op::CheckObject => e(1, 1),
Op::GetMemberThis(_) => e(1, 2),
Op::SetMember(_) | Op::GetIndex | Op::DeleteIndex | Op::IncIndex { .. } | Op::Binary(_) => e(2, 1),
Op::ArrayPush | Op::InitProp(_) | Op::InitGetter(_) | Op::InitSetter(_) => e(2, 1),
Op::GetIndexThis => e(2, 2),
Op::SetIndex => e(3, 1),
Op::Call(n) | Op::CallEval(n) => e(n.checked_add(2).ok_or("operand too large")?, 1),
Op::New(n) => e(n.checked_add(1).ok_or("operand too large")?, 1),
Op::PopEnv => e(0, 0),
Op::Jump(t) => Effect { pops: 0, next: None, jumps: vec![(t, 0)] },
Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => Effect { pops: 1, next: Some(0), jumps: vec![(t, 0)] },
Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) => Effect { pops: 1, next: Some(0), jumps: vec![(t, 1)] },
Op::Return | Op::Throw => end(1),
Op::ReturnCompletion | Op::ThrowReferenceError(_) => end(0),
Op::EndFinally(t) => {
let table = finally_tables.get(t as usize).ok_or_else(|| format!("finally table {t} out of range"))?;
Effect { pops: 2, next: Some(0), jumps: table.iter().map(|&target| (target, 1)).collect() }
}
Op::ForInNext(t) => Effect { pops: 1, next: Some(2), jumps: vec![(t, 1)] },
})
}
/// Checks the constant, name, function and regular expression indices an instruction uses.
fn check_indices(op: Op, code: &Code) -> Result<(), String> {
let check = |i: u32, len: usize, what: &str| if (i as usize) < len { Ok(()) } else { Err(format!("{what} {i} out of range")) };
match op {
Op::Number(i) => check(i, code.numbers.len(), "number"),
Op::String(i) | Op::ThrowReferenceError(i) => check(i, code.strings.len(), "string"),
Op::GetVar(i)
| Op::GetVarThis(i)
| Op::SetVar(i)
| Op::RefVar(i)
| Op::GetRef(i)
| Op::PutRef(i)
| Op::TypeofVar(i)
| Op::DeleteVar(i)
| Op::IncVar { name: i, .. }
| Op::GetMember(i)
| Op::GetMemberThis(i)
| Op::SetMember(i)
| Op::DeleteMember(i)
| Op::IncMember { name: i, .. }
| Op::InitProp(i)
| Op::InitGetter(i)
| Op::InitSetter(i)
| Op::PushCatch(i) => check(i, code.names.len(), "name"),
Op::Closure(i) => check(i, code.functions.len(), "function"),
Op::RegExp(i) => check(i, code.regexps.len(), "regular expression"),
_ => Ok(()),
}
}
/// Verifies `code` (not the functions inside it, which are verified when they are compiled) and
/// returns the most values it keeps on the stack.
pub(crate) fn verify(code: &Code) -> Result<u32, String> {
let len = code.ops.len();
if len == 0 {
return Err("no instructions".into());
}
if code.positions.len() != len {
return Err("the position table does not match the instructions".into());
}
for (name, _) in &code.fun_decls {
if name.is_empty() {
return Err("a function declaration without a name".into());
}
}
if code.fun_decls.iter().any(|(_, i)| *i as usize >= code.functions.len()) {
return Err("function declaration index out of range".into());
}
let mut depth: Vec<Option<u32>> = vec![None; len];
let mut work: Vec<(u32, u32)> = vec![(0, 0)];
for h in &code.handlers {
if h.start > h.end || h.end as usize > len {
return Err(format!("handler range {}..{} out of range", h.start, h.end));
}
work.push((h.target, h.depth.checked_add(1).ok_or("handler depth too large")?));
}
let mut max = 0;
while let Some((pc, d)) = work.pop() {
let slot = depth.get_mut(pc as usize).ok_or_else(|| format!("jump to {pc}, past the end"))?;
match *slot {
Some(seen) if seen == d => continue,
Some(seen) => return Err(format!("instruction {pc} reached with stack depths {seen} and {d}")),
None => *slot = Some(d),
}
max = max.max(d);
let op = code.ops.get(pc as usize).copied().ok_or("instruction out of range")?;
check_indices(op, code)?;
let fx = effect(op, &code.finally_tables)?;
let base = d.checked_sub(fx.pops).ok_or_else(|| format!("instruction {pc} ({op:?}) pops below the stack"))?;
if let Some(pushes) = fx.next {
if pc as usize + 1 >= len {
return Err("the code runs past its last instruction".into());
}
work.push((pc + 1, base + pushes));
}
for (target, pushes) in fx.jumps {
work.push((target, base + pushes));
}
}
for h in &code.handlers {
if let Some(Some(d)) = depth.get(h.start as usize)
&& *d < h.depth
&& h.start < h.end
{
return Err(format!("handler at {} expects {} values but its range starts with {d}", h.target, h.depth));
}
}
Ok(max)
}