Files
vectorcraft/crates/js/src/bytecode.rs
T
dotCooCoo f85c555014 JavaScript: run E4X XML literals and the XML, XMLList, Namespace and QName objects behind an option
With `Options::e4x` set, vectorcraft-js runs ECMA-357 (2nd edition):
XML and XMLList literals with embedded expressions, the `..`, `.@`,
`.*`, `.()` and `::` selectors, assignment and `delete` on selected
nodes, `for each`, `default xml namespace`, and the four objects with
their methods and settings. The option is off by default, and without
it the syntax and globals do not exist.

Parsing, serializing, comparing and copying trees use no native stack,
and each takes fuel and counts against the heap limit. Inserting a node
into itself or its descendants throws a TypeError before the tree
changes. A literal or XML string nested past `Options::max_depth` gives
a SyntaxError, and a tree built past that depth stops at the heap limit.
`XML.prototype.xpath` is not provided.
2026-11-09 06:50:32 -08:00

398 lines
16 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,
/// `[qualifier] name` → an E4X name object.
XmlMakeName {
attribute: bool,
qualified: bool,
},
/// `object name` → the descendants that match.
XmlDescendants,
/// `value` → the string an embedded expression puts in a literal (0 content, 1 attribute
/// value, 2 name).
XmlEscape(u8),
/// The `parts` strings → the XML or XMLList they spell.
XmlLiteral {
parts: u32,
list: bool,
},
/// `name` → the value of the name on the nearest XML value in scope.
XmlScopeGet,
/// `object` → the filter's state.
XmlFilterStart,
/// `state` → `state item`, or jumps with `state` when the items are used up.
XmlFilterNext(u32),
/// `state value` → `state`, the current item kept when the value is truthy.
XmlFilterTest,
/// `state` → the XMLList of the items kept.
XmlFilterEnd,
/// `iterator name` → `iterator value`: the value under the name a `for each` got.
ForEachValue,
/// `value` → nothing: sets the default XML namespace of the running code.
SetDefaultNamespace,
}
/// 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) | Op::XmlFilterNext(t) => Effect { pops: 1, next: Some(2), jumps: vec![(t, 1)] },
Op::XmlMakeName { qualified, .. } => e(1 + u32::from(qualified), 1),
Op::XmlDescendants | Op::XmlFilterTest => e(2, 1),
Op::XmlEscape(_) | Op::XmlScopeGet | Op::XmlFilterStart | Op::XmlFilterEnd => e(1, 1),
Op::XmlLiteral { parts, .. } => e(parts, 1),
Op::ForEachValue => e(2, 2),
Op::SetDefaultNamespace => e(1, 0),
})
}
/// 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)
}