Files
vectorcraft/crates/js/src/compiler.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

1261 lines
47 KiB
Rust

//! The compiler: a syntax tree to bytecode, one [`Code`] per function, script or eval code.
//!
//! Variables are looked up by name at run time through the environment chain, as ECMA-262 5.1
//! section 10 describes, so `with` and direct `eval` need no special analysis. Declarations are
//! hoisted: each code lists its `var` names and function declarations, and the interpreter binds
//! them when the code is entered (section 10.5).
//!
//! The compiler counts the operand stack depth as it emits, so `break`, `continue` and `return`
//! know how many values to drop. A jump out of a `try` with a `finally` block goes through the
//! block: it pushes a completion (a value and a kind number) and jumps to the block, whose
//! `EndFinally` instruction goes on with the jump afterwards. Long left-nested runs of binary
//! operators and member accesses, right-nested assignment chains, prefix operators, `else if`
//! chains and labels are compiled with loops; every other kind of nesting is counted, and code
//! nested deeper than [`MAX_DEPTH`] gives a `SyntaxError` instead of exhausting the stack.
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use crate::ast::{
AssignOp, Ast, BinaryOp, Expr, ExprId, ForInTarget, ForInit, FunctionId, FunctionKind, LogicalOp, PropertyValue, Stmt, StmtId, UnaryOp, UpdateOp,
VarDecl,
};
use crate::bytecode::{Code, CodeKind, Handler, Op, Source, fallthrough_effect, verify};
use crate::error::{SyntaxError, TOO_DEEP};
use crate::heap::Key;
use crate::value::Str;
/// How deeply the compiler may recurse: past the parser's default nesting limit, and shallow
/// enough for a 1 MiB stack.
pub(crate) const MAX_DEPTH: u32 = 600;
/// Why code could not be compiled.
#[derive(Debug)]
pub(crate) enum CompileFail {
Syntax(SyntaxError),
/// The compiler made code the verifier rejects.
Internal(String),
}
type CResult<T> = Result<T, CompileFail>;
fn internal<T>(message: impl Into<String>) -> CResult<T> {
Err(CompileFail::Internal(message.into()))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Label(usize);
/// Where `break`, `continue` and `return` go.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Exit {
/// To the end of the jump context at this index.
Break(usize),
/// To the continue point of the loop at this index.
Continue(usize),
Return,
}
enum Ctx {
/// A loop, a `switch` or a labeled statement.
Jump { labels: Vec<String>, is_loop: bool, plain_break: bool, brk: Label, cont: Option<Label>, depth: u32, env_depth: u32 },
/// The protected part (and catch clause) of a `try` with a `finally` block.
Finally { try_depth: u32, env_depth: u32, entry: Label, exits: Vec<Exit> },
}
struct FnCompiler<'a> {
ast: &'a Ast,
source: &'a Rc<Source>,
kind: CodeKind,
strict: bool,
ops: Vec<Op>,
positions: Vec<u32>,
pos: u32,
numbers: Vec<f64>,
number_index: HashMap<u64, u32>,
strings: Vec<Str>,
string_index: HashMap<Vec<u16>, u32>,
names: Vec<Key>,
name_index: HashMap<Key, u32>,
functions: Vec<Rc<Code>>,
regexps: Vec<(Str, Str)>,
handlers: Vec<(u32, u32, Label, u32, u32)>,
finally_tables: Vec<Vec<Label>>,
labels: Vec<Option<u32>>,
patches: Vec<(usize, Label)>,
depth: u32,
env_depth: u32,
contexts: Vec<Ctx>,
uses_arguments: bool,
recursion: u32,
}
/// The `var` names and function declarations a body hoists.
#[derive(Default)]
struct Hoisted {
vars: Vec<Key>,
fun_decls: Vec<(Key, u32)>,
}
fn key_of(name: &str) -> Key {
crate::heap::key(name)
}
impl<'a> FnCompiler<'a> {
fn new(ast: &'a Ast, source: &'a Rc<Source>, kind: CodeKind, strict: bool, recursion: u32) -> Self {
Self {
ast,
source,
kind,
strict,
ops: Vec::new(),
positions: Vec::new(),
pos: 0,
numbers: Vec::new(),
number_index: HashMap::new(),
strings: Vec::new(),
string_index: HashMap::new(),
names: Vec::new(),
name_index: HashMap::new(),
functions: Vec::new(),
regexps: Vec::new(),
handlers: Vec::new(),
finally_tables: Vec::new(),
labels: Vec::new(),
patches: Vec::new(),
depth: 0,
env_depth: 0,
contexts: Vec::new(),
uses_arguments: false,
recursion,
}
}
// ---- emitting ----
fn emit(&mut self, op: Op) {
let (pops, pushes) = fallthrough_effect(op);
self.depth = self.depth.saturating_sub(pops) + pushes;
self.ops.push(op);
self.positions.push(self.pos);
}
fn new_label(&mut self) -> Label {
self.labels.push(None);
Label(self.labels.len() - 1)
}
fn bind(&mut self, label: Label) {
if let Some(slot) = self.labels.get_mut(label.0) {
*slot = Some(u32::try_from(self.ops.len()).unwrap_or(u32::MAX));
}
}
/// Emits a jump instruction (`make` builds it from its target) to `label`, patched when the
/// code is finished.
fn jump(&mut self, make: fn(u32) -> Op, label: Label) {
self.patches.push((self.ops.len(), label));
self.emit(make(0));
}
fn at(&mut self, offset: u32) {
self.pos = offset;
}
fn number(&mut self, n: f64) -> u32 {
if let Some(&i) = self.number_index.get(&n.to_bits()) {
return i;
}
let i = u32::try_from(self.numbers.len()).unwrap_or(u32::MAX);
self.numbers.push(n);
self.number_index.insert(n.to_bits(), i);
i
}
fn string(&mut self, units: &[u16]) -> u32 {
if let Some(&i) = self.string_index.get(units) {
return i;
}
let i = u32::try_from(self.strings.len()).unwrap_or(u32::MAX);
self.strings.push(Str::from_units(units.to_vec()));
self.string_index.insert(units.to_vec(), i);
i
}
fn name_key(&mut self, key: Key) -> u32 {
if let Some(&i) = self.name_index.get(&key) {
return i;
}
let i = u32::try_from(self.names.len()).unwrap_or(u32::MAX);
self.names.push(key.clone());
self.name_index.insert(key, i);
i
}
/// The index of a variable name, noting a use of `arguments`.
fn var_name(&mut self, name: &str) -> u32 {
if name == "arguments" {
self.uses_arguments = true;
}
self.name_key(key_of(name))
}
fn push_number(&mut self, n: f64) {
let i = self.number(n);
self.emit(Op::Number(i));
}
fn enter(&mut self) -> CResult<()> {
self.recursion += 1;
if self.recursion > MAX_DEPTH {
let (line, column) = self.source.lines.line_column(self.pos);
return Err(CompileFail::Syntax(SyntaxError { message: TOO_DEEP.into(), line, column, offset: self.pos }));
}
Ok(())
}
fn leave(&mut self) {
self.recursion -= 1;
}
// ---- declarations ----
/// Collects the `var` names and function declarations of a body, outside nested functions,
/// in source order, and compiles the functions.
fn hoist(&mut self, body: &[StmtId]) -> CResult<Hoisted> {
let mut out = Hoisted::default();
let mut seen = HashSet::new();
let mut work: Vec<StmtId> = body.iter().rev().copied().collect();
let mut add_var = |out: &mut Hoisted, d: &VarDecl| {
let k = key_of(&d.name.name);
if seen.insert(k.clone()) {
out.vars.push(k);
}
};
while let Some(id) = work.pop() {
let Some(stmt) = self.ast.stmt(id) else { return internal("statement out of range") };
match stmt {
Stmt::Var(decls) => decls.iter().for_each(|d| add_var(&mut out, d)),
Stmt::FunctionDecl(fid) => {
let Some(name) = self.ast.function(*fid).and_then(|f| f.name.as_ref()) else {
return internal("function declaration without a name");
};
let k = key_of(&name.name);
let i = self.function(*fid)?;
out.fun_decls.push((k, i));
}
Stmt::Block(list) => work.extend(list.iter().rev()),
Stmt::If { consequent, alternate, .. } => {
work.extend(alternate.iter());
work.push(*consequent);
}
Stmt::DoWhile { body, .. } | Stmt::While { body, .. } | Stmt::With { body, .. } | Stmt::Labeled { body, .. } => work.push(*body),
Stmt::For { init, body, .. } => {
work.push(*body);
if let Some(ForInit::Var(decls)) = init {
decls.iter().for_each(|d| add_var(&mut out, d));
}
}
Stmt::ForIn { target, body, .. } => {
work.push(*body);
if let ForInTarget::Var(d) = target {
add_var(&mut out, d);
}
}
Stmt::Switch { cases, .. } => {
for case in cases.iter().rev() {
work.extend(case.body.iter().rev());
}
}
Stmt::Try { block, handler, finalizer } => {
if let Some(f) = finalizer {
work.extend(f.iter().rev());
}
if let Some(h) = handler {
work.extend(h.body.iter().rev());
}
work.extend(block.iter().rev());
}
Stmt::Expr(_) | Stmt::Empty | Stmt::Continue(_) | Stmt::Break(_) | Stmt::Return(_) | Stmt::Throw(_) | Stmt::Debugger => {}
}
}
Ok(out)
}
/// Compiles a nested function and returns its index in the function table.
fn function(&mut self, fid: FunctionId) -> CResult<u32> {
let code = compile_function(self.ast, fid, self.source, self.recursion, true)?;
self.functions.push(code);
Ok(u32::try_from(self.functions.len() - 1).unwrap_or(u32::MAX))
}
// ---- statements ----
fn stmts(&mut self, list: &[StmtId]) -> CResult<()> {
for &s in list {
self.stmt(s)?;
}
Ok(())
}
fn stmt(&mut self, id: StmtId) -> CResult<()> {
self.enter()?;
let saved = self.depth;
let r = self.stmt_inner(id);
self.depth = saved;
self.leave();
r
}
fn stmt_inner(&mut self, id: StmtId) -> CResult<()> {
let Some(stmt) = self.ast.stmt(id) else { return internal("statement out of range") };
self.at(self.ast.stmt_span(id).start);
match stmt {
Stmt::Var(decls) => self.var_decls(decls),
Stmt::FunctionDecl(_) | Stmt::Empty | Stmt::Debugger => Ok(()),
Stmt::Expr(e) => {
self.expr(*e)?;
self.emit(if self.kind == CodeKind::Function { Op::Pop } else { Op::SetCompletion });
Ok(())
}
Stmt::Block(list) => self.stmts(list),
Stmt::If { .. } => self.if_chain(id),
Stmt::DoWhile { .. } | Stmt::While { .. } | Stmt::For { .. } | Stmt::ForIn { .. } => self.loop_stmt(id, Vec::new()),
Stmt::Continue(label) => self.jump_stmt(label.as_ref().map(|l| l.name.as_str()), true),
Stmt::Break(label) => self.jump_stmt(label.as_ref().map(|l| l.name.as_str()), false),
Stmt::Return(value) => {
match value {
Some(e) => self.expr(*e)?,
None => self.emit(Op::Undefined),
}
self.emit_exit(Exit::Return)
}
Stmt::With { object, body } => {
self.expr(*object)?;
self.emit(Op::PushWith);
self.env_depth += 1;
self.stmt(*body)?;
self.emit(Op::PopEnv);
self.env_depth -= 1;
Ok(())
}
Stmt::Switch { .. } => self.switch_stmt(id, Vec::new()),
Stmt::Labeled { .. } => self.labeled(id),
Stmt::Throw(e) => {
self.expr(*e)?;
self.at(self.ast.stmt_span(id).start);
self.emit(Op::Throw);
Ok(())
}
Stmt::Try { block, handler, finalizer } => self.try_stmt(block, handler.as_ref(), finalizer.as_deref()),
}
}
fn var_decls(&mut self, decls: &[VarDecl]) -> CResult<()> {
for d in decls {
if let Some(init) = d.init {
self.expr(init)?;
self.at(d.name.span.start);
let n = self.var_name(&d.name.name);
self.emit(Op::SetVar(n));
self.emit(Op::Pop);
}
}
Ok(())
}
fn if_chain(&mut self, id: StmtId) -> CResult<()> {
let saved = self.depth;
let end = self.new_label();
let mut cur = id;
while let Some(Stmt::If { test, consequent, alternate }) = self.ast.stmt(cur) {
self.at(self.ast.stmt_span(cur).start);
self.expr(*test)?;
let otherwise = self.new_label();
self.jump(Op::JumpIfFalse, otherwise);
self.stmt(*consequent)?;
let Some(alt) = *alternate else {
self.bind(otherwise);
break;
};
self.jump(Op::Jump, end);
self.bind(otherwise);
self.depth = saved;
if matches!(self.ast.stmt(alt), Some(Stmt::If { .. })) {
cur = alt;
} else {
self.stmt(alt)?;
break;
}
}
self.bind(end);
Ok(())
}
fn labeled(&mut self, id: StmtId) -> CResult<()> {
let mut labels = Vec::new();
let mut cur = id;
while let Some(Stmt::Labeled { label, body }) = self.ast.stmt(cur) {
labels.push(label.name.clone());
cur = *body;
}
match self.ast.stmt(cur) {
Some(Stmt::DoWhile { .. } | Stmt::While { .. } | Stmt::For { .. } | Stmt::ForIn { .. }) => self.loop_stmt(cur, labels),
Some(Stmt::Switch { .. }) => self.switch_stmt(cur, labels),
_ => {
let brk = self.new_label();
self.push_jump_ctx(labels, false, false, brk, None);
self.stmt(cur)?;
self.contexts.pop();
self.bind(brk);
Ok(())
}
}
}
fn push_jump_ctx(&mut self, labels: Vec<String>, is_loop: bool, plain_break: bool, brk: Label, cont: Option<Label>) {
self.contexts.push(Ctx::Jump { labels, is_loop, plain_break, brk, cont, depth: self.depth, env_depth: self.env_depth });
}
/// Compiles a loop body inside its jump context.
fn loop_body(&mut self, body: StmtId, labels: Vec<String>, brk: Label, cont: Label) -> CResult<()> {
self.push_jump_ctx(labels, true, true, brk, Some(cont));
let r = self.stmt(body);
self.contexts.pop();
r
}
fn loop_stmt(&mut self, id: StmtId, labels: Vec<String>) -> CResult<()> {
let brk = self.new_label();
let cont = self.new_label();
match self.ast.stmt(id) {
Some(Stmt::While { test, body }) => {
self.bind(cont);
self.expr(*test)?;
self.jump(Op::JumpIfFalse, brk);
self.loop_body(*body, labels, brk, cont)?;
self.jump(Op::Jump, cont);
}
Some(Stmt::DoWhile { body, test }) => {
let top = self.new_label();
self.bind(top);
self.loop_body(*body, labels, brk, cont)?;
self.bind(cont);
self.expr(*test)?;
self.jump(Op::JumpIfTrue, top);
}
Some(Stmt::For { init, test, update, body }) => {
match init {
Some(ForInit::Var(decls)) => self.var_decls(decls)?,
Some(ForInit::Expr(e)) => {
self.expr(*e)?;
self.emit(Op::Pop);
}
None => {}
}
let head = self.new_label();
self.bind(head);
if let Some(t) = test {
self.expr(*t)?;
self.jump(Op::JumpIfFalse, brk);
}
self.loop_body(*body, labels, brk, cont)?;
self.bind(cont);
if let Some(u) = update {
self.expr(*u)?;
self.emit(Op::Pop);
}
self.jump(Op::Jump, head);
}
Some(Stmt::ForIn { target, object, body }) => return self.for_in(target, *object, *body, labels),
_ => return internal("not a loop"),
}
self.bind(brk);
Ok(())
}
fn for_in(&mut self, target: &ForInTarget, object: ExprId, body: StmtId, labels: Vec<String>) -> CResult<()> {
if let ForInTarget::Var(d) = target {
self.var_decls(std::slice::from_ref(d))?;
}
self.expr(object)?;
self.emit(Op::ForInStart);
let iter_depth = self.depth;
let head = self.new_label();
let brk = self.new_label();
self.bind(head);
self.jump(Op::ForInNext, brk);
match target {
ForInTarget::Var(d) => {
let n = self.var_name(&d.name.name);
self.emit(Op::SetVar(n));
}
ForInTarget::Expr(e) => self.store_key_to(*e)?,
}
self.emit(Op::Pop);
self.loop_body(body, labels, brk, head)?;
self.jump(Op::Jump, head);
self.bind(brk);
self.depth = iter_depth;
self.emit(Op::Pop);
Ok(())
}
/// Assigns the name on top of the stack to a `for-in` target expression, leaving it on top.
fn store_key_to(&mut self, target: ExprId) -> CResult<()> {
let target = self.ast.unparenthesized(target);
self.at(self.ast.expr_span(target).start);
match self.ast.expr(target) {
Some(Expr::Ident(name)) => {
let n = self.var_name(name);
self.emit(Op::SetVar(n));
}
Some(Expr::Member { object, property }) => {
self.expr(*object)?;
self.emit(Op::Swap);
let n = self.name_key(key_of(&property.name));
self.emit(Op::SetMember(n));
}
Some(Expr::Index { object, index }) => {
self.expr(*object)?;
self.expr(*index)?;
self.emit(Op::ToKey);
self.emit(Op::Rot3);
self.emit(Op::SetIndex);
}
_ => {
self.expr(target)?;
self.emit(Op::Pop);
self.throw_invalid_target();
}
}
Ok(())
}
fn throw_invalid_target(&mut self) {
let s = self.string(&"invalid assignment target".encode_utf16().collect::<Vec<u16>>());
let depth = self.depth;
self.emit(Op::ThrowReferenceError(s));
self.depth = depth;
}
fn switch_stmt(&mut self, id: StmtId, labels: Vec<String>) -> CResult<()> {
let Some(Stmt::Switch { discriminant, cases }) = self.ast.stmt(id) else { return internal("not a switch") };
self.expr(*discriminant)?;
let d = self.depth;
let brk = self.new_label();
let case_labels: Vec<Label> = cases.iter().map(|_| self.new_label()).collect();
for (case, &label) in cases.iter().zip(&case_labels) {
if let Some(test) = case.test {
self.emit(Op::Dup);
self.expr(test)?;
self.at(case.span.start);
self.emit(Op::Binary(BinaryOp::StrictEq));
self.jump(Op::JumpIfTrue, label);
}
}
match cases.iter().position(|c| c.test.is_none()).and_then(|i| case_labels.get(i)) {
Some(&default) => self.jump(Op::Jump, default),
None => self.jump(Op::Jump, brk),
}
self.push_jump_ctx(labels, false, true, brk, None);
for (case, &label) in cases.iter().zip(&case_labels) {
self.bind(label);
self.depth = d;
self.stmts(&case.body)?;
}
self.contexts.pop();
self.bind(brk);
self.depth = d;
self.emit(Op::Pop);
Ok(())
}
fn jump_stmt(&mut self, label: Option<&str>, is_continue: bool) -> CResult<()> {
let target = self.contexts.iter().enumerate().rev().find_map(|(i, c)| match c {
Ctx::Jump { labels, is_loop, plain_break, .. } => {
let hit = match label {
Some(l) => labels.iter().any(|x| x == l) && (!is_continue || *is_loop),
None if is_continue => *is_loop,
None => *plain_break,
};
hit.then_some(i)
}
Ctx::Finally { .. } => None,
});
match target {
Some(i) => self.emit_exit(if is_continue { Exit::Continue(i) } else { Exit::Break(i) }),
None => internal("break or continue without a target"),
}
}
/// Emits a jump out to `exit`, dropping the values and environments in between and going
/// through each `finally` block on the way.
fn emit_exit(&mut self, exit: Exit) -> CResult<()> {
let (saved, saved_env) = (self.depth, self.env_depth);
let mut i = self.contexts.len();
loop {
let Some(next) = i.checked_sub(1) else {
if exit != Exit::Return {
return internal("jump target not found");
}
self.emit(Op::Return);
break;
};
i = next;
match self.contexts.get(i) {
Some(&Ctx::Jump { brk, cont, depth, env_depth, .. }) => {
let label = match exit {
Exit::Break(t) if t == i => Some(brk),
Exit::Continue(t) if t == i => cont,
_ => None,
};
if let Some(label) = label {
self.unwind_to(depth, env_depth)?;
self.jump(Op::Jump, label);
break;
}
}
Some(&Ctx::Finally { try_depth, env_depth, entry, .. }) => {
self.route_through_finally(i, exit, try_depth, env_depth, entry)?;
break;
}
None => return internal("context out of range"),
}
}
self.depth = saved;
self.env_depth = saved_env;
Ok(())
}
fn route_through_finally(&mut self, ctx: usize, exit: Exit, try_depth: u32, env_depth: u32, entry: Label) -> CResult<()> {
let Some(Ctx::Finally { exits, .. }) = self.contexts.get_mut(ctx) else { return internal("not a finally context") };
let k = match exits.iter().position(|e| *e == exit) {
Some(k) => k,
None => {
exits.push(exit);
exits.len() - 1
}
};
if exit == Exit::Return {
let Some(under) = self.depth.checked_sub(1 + try_depth) else { return internal("stack below a try block") };
if under > 0 {
self.emit(Op::PopUnder(under));
}
self.pop_envs(env_depth)?;
} else {
self.unwind_to(try_depth, env_depth)?;
self.emit(Op::Undefined);
}
self.push_number((k + 2) as f64);
self.jump(Op::Jump, entry);
Ok(())
}
fn unwind_to(&mut self, depth: u32, env_depth: u32) -> CResult<()> {
if self.depth < depth {
return internal("jump to a deeper stack");
}
while self.depth > depth {
self.emit(Op::Pop);
}
self.pop_envs(env_depth)
}
fn pop_envs(&mut self, env_depth: u32) -> CResult<()> {
if self.env_depth < env_depth {
return internal("jump into an environment");
}
for _ in env_depth..self.env_depth {
self.ops.push(Op::PopEnv);
self.positions.push(self.pos);
}
Ok(())
}
fn try_stmt(&mut self, block: &[StmtId], handler: Option<&crate::ast::CatchClause>, finalizer: Option<&[StmtId]>) -> CResult<()> {
let try_depth = self.depth;
let env_depth = self.env_depth;
let fin_entry = self.new_label();
if finalizer.is_some() {
self.contexts.push(Ctx::Finally { try_depth, env_depth, entry: fin_entry, exits: Vec::new() });
}
let start = self.pc();
self.stmts(block)?;
if let Some(h) = handler {
let after = self.new_label();
let catch = self.new_label();
self.jump(Op::Jump, after);
let end = self.pc();
self.handlers.push((start, end, catch, try_depth, env_depth));
self.bind(catch);
self.depth = try_depth + 1;
self.at(h.param.span.start);
let n = self.var_name(&h.param.name);
self.emit(Op::PushCatch(n));
self.env_depth += 1;
self.stmts(&h.body)?;
self.emit(Op::PopEnv);
self.env_depth -= 1;
self.bind(after);
}
let Some(fin) = finalizer else { return Ok(()) };
let protected_end = self.pc();
let Some(Ctx::Finally { exits, .. }) = self.contexts.pop() else { return internal("finally context missing") };
let after = self.new_label();
let stub = self.new_label();
self.handlers.push((start, protected_end, stub, try_depth, env_depth));
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());
self.emit(Op::EndFinally(table));
self.jump(Op::Jump, after);
self.bind(stub);
self.depth = try_depth + 1;
self.push_number(1.0);
self.jump(Op::Jump, fin_entry);
for (exit, label) in exits.into_iter().zip(dispatch) {
self.bind(label);
self.depth = try_depth + 1;
if exit != Exit::Return {
self.emit(Op::Pop);
}
self.emit_exit(exit)?;
}
self.bind(after);
self.depth = try_depth;
Ok(())
}
fn pc(&self) -> u32 {
u32::try_from(self.ops.len()).unwrap_or(u32::MAX)
}
// ---- expressions ----
fn expr(&mut self, id: ExprId) -> CResult<()> {
self.enter()?;
let r = self.expr_inner(self.ast.unparenthesized(id));
self.leave();
r
}
fn expr_inner(&mut self, id: ExprId) -> CResult<()> {
let Some(e) = self.ast.expr(id) else { return internal("expression out of range") };
self.at(self.ast.expr_span(id).start);
match e {
Expr::This => self.emit(Op::This),
Expr::Ident(name) => {
let n = self.var_name(name);
self.emit(Op::GetVar(n));
}
Expr::Null => self.emit(Op::Null),
Expr::Bool(b) => self.emit(if *b { Op::True } else { Op::False }),
Expr::Number(n) => self.push_number(*n),
Expr::String(s) => {
let i = self.string(s);
self.emit(Op::String(i));
}
Expr::RegExp { pattern, flags } => {
self.regexps.push((Str::from_units(pattern.clone()), Str::from(flags.as_str())));
let i = u32::try_from(self.regexps.len() - 1).unwrap_or(u32::MAX);
self.emit(Op::RegExp(i));
}
Expr::Array(elements) => {
self.emit(Op::NewArray);
for el in elements {
match el {
Some(e) => {
self.expr(*e)?;
self.emit(Op::ArrayPush);
}
None => self.emit(Op::ArrayHole),
}
}
}
Expr::Object(props) => self.object_literal(props)?,
Expr::Function(fid) => {
let i = self.function(*fid)?;
self.emit(Op::Closure(i));
}
Expr::Member { .. } | Expr::Index { .. } | Expr::Call { .. } => self.chain(id)?,
Expr::New { callee, args } => {
self.expr(*callee)?;
self.args(args)?;
self.at(self.ast.expr_span(id).start);
self.emit(Op::New(len_u32(args.len())));
}
Expr::Update { op, prefix, target } => self.update(*op, *prefix, *target)?,
Expr::Unary { .. } => self.unary(id)?,
Expr::Binary { .. } | Expr::Logical { .. } => self.binary_chain(id)?,
Expr::Conditional { test, consequent, alternate } => {
self.expr(*test)?;
let d = self.depth;
let otherwise = self.new_label();
let end = self.new_label();
self.jump(Op::JumpIfFalse, otherwise);
self.expr(*consequent)?;
self.jump(Op::Jump, end);
self.bind(otherwise);
self.depth = d.saturating_sub(1);
self.expr(*alternate)?;
self.bind(end);
}
Expr::Assign { .. } => self.assign_chain(id)?,
Expr::Sequence(list) => {
for (i, e) in list.iter().enumerate() {
if i > 0 {
self.emit(Op::Pop);
}
self.expr(*e)?;
}
if list.is_empty() {
self.emit(Op::Undefined);
}
}
Expr::Paren(inner) => self.expr(*inner)?,
}
Ok(())
}
fn args(&mut self, args: &[ExprId]) -> CResult<()> {
for a in args {
self.expr(*a)?;
}
Ok(())
}
fn object_literal(&mut self, props: &[crate::ast::Property]) -> CResult<()> {
self.emit(Op::NewObject);
for p in props {
let n = self.name_key(p.key.to_utf16().into());
match &p.value {
PropertyValue::Init(e) => {
self.expr(*e)?;
self.emit(Op::InitProp(n));
}
PropertyValue::Get(f) => {
let i = self.function(*f)?;
self.emit(Op::Closure(i));
self.emit(Op::InitGetter(n));
}
PropertyValue::Set(f) => {
let i = self.function(*f)?;
self.emit(Op::Closure(i));
self.emit(Op::InitSetter(n));
}
}
}
Ok(())
}
/// A run of member accesses and calls (`a.b[c](d).e`), compiled from the innermost object out
/// with a loop.
fn chain(&mut self, id: ExprId) -> CResult<()> {
let mut suffixes = Vec::new();
let mut cur = id;
loop {
match self.ast.expr(cur) {
Some(Expr::Member { object, .. } | Expr::Index { object, .. }) => {
suffixes.push(cur);
cur = self.ast.unparenthesized(*object);
}
Some(Expr::Call { callee, .. }) => {
suffixes.push(cur);
cur = self.ast.unparenthesized(*callee);
}
_ => break,
}
}
let is_call = |c: &Self, e: Option<&ExprId>| e.is_some_and(|&e| matches!(c.ast.expr(e), Some(Expr::Call { .. })));
let first_is_call = is_call(self, suffixes.last());
let mut base_has_this = false;
let mut eval_call = false;
match self.ast.expr(cur) {
Some(Expr::Ident(name)) if first_is_call => {
self.at(self.ast.expr_span(cur).start);
let n = self.var_name(name);
self.emit(Op::GetVarThis(n));
base_has_this = true;
eval_call = name == "eval";
}
_ => self.expr(cur)?,
}
let count = suffixes.len();
for j in (0..count).rev() {
let Some(&s) = suffixes.get(j) else { return internal("suffix out of range") };
let next_is_call = j > 0 && is_call(self, suffixes.get(j - 1));
let span = self.ast.expr_span(s);
match self.ast.expr(s) {
Some(Expr::Member { property, .. }) => {
self.at(span.start);
let n = self.name_key(key_of(&property.name));
self.emit(if next_is_call { Op::GetMemberThis(n) } else { Op::GetMember(n) });
}
Some(Expr::Index { index, .. }) => {
self.expr(*index)?;
self.at(span.start);
self.emit(if next_is_call { Op::GetIndexThis } else { Op::GetIndex });
}
Some(Expr::Call { args, .. }) => {
let innermost = j + 1 == count;
let has_this = if innermost {
base_has_this
} else {
matches!(suffixes.get(j + 1).and_then(|&e| self.ast.expr(e)), Some(Expr::Member { .. } | Expr::Index { .. }))
};
if !has_this {
self.emit(Op::Undefined);
}
self.args(args)?;
self.at(span.start);
let n = len_u32(args.len());
if innermost && eval_call {
self.uses_arguments = true;
self.emit(Op::CallEval(n));
} else {
self.emit(Op::Call(n));
}
}
_ => return internal("not a member or call"),
}
}
Ok(())
}
fn unary(&mut self, id: ExprId) -> CResult<()> {
let mut ops = Vec::new();
let mut cur = id;
while let Some(Expr::Unary { op, arg }) = self.ast.expr(cur) {
if matches!(op, UnaryOp::Delete | UnaryOp::Typeof) {
break;
}
ops.push(*op);
cur = self.ast.unparenthesized(*arg);
}
match self.ast.expr(cur) {
Some(Expr::Unary { op: UnaryOp::Typeof, arg }) => self.typeof_expr(*arg)?,
Some(Expr::Unary { op: UnaryOp::Delete, arg }) => self.delete_expr(*arg)?,
_ => self.expr(cur)?,
}
self.at(self.ast.expr_span(id).start);
for op in ops.into_iter().rev() {
match op {
UnaryOp::Plus => self.emit(Op::Plus),
UnaryOp::Minus => self.emit(Op::Neg),
UnaryOp::BitNot => self.emit(Op::BitNot),
UnaryOp::Not => self.emit(Op::Not),
UnaryOp::Void => {
self.emit(Op::Pop);
self.emit(Op::Undefined);
}
UnaryOp::Delete | UnaryOp::Typeof => return internal("unexpected operator"),
}
}
Ok(())
}
fn typeof_expr(&mut self, arg: ExprId) -> CResult<()> {
let arg = self.ast.unparenthesized(arg);
if let Some(Expr::Ident(name)) = self.ast.expr(arg) {
let n = self.var_name(name);
self.emit(Op::TypeofVar(n));
} else {
self.expr(arg)?;
self.emit(Op::Typeof);
}
Ok(())
}
fn delete_expr(&mut self, arg: ExprId) -> CResult<()> {
let arg = self.ast.unparenthesized(arg);
match self.ast.expr(arg) {
Some(Expr::Ident(name)) => {
let n = self.var_name(name);
self.emit(Op::DeleteVar(n));
}
Some(Expr::Member { object, property }) => {
self.expr(*object)?;
let n = self.name_key(key_of(&property.name));
self.emit(Op::DeleteMember(n));
}
Some(Expr::Index { object, index }) => {
self.expr(*object)?;
self.expr(*index)?;
self.emit(Op::DeleteIndex);
}
_ => {
self.expr(arg)?;
self.emit(Op::Pop);
self.emit(Op::True);
}
}
Ok(())
}
/// A run of binary and logical operators nested on the left (`a + b + c`), compiled with a
/// loop.
fn binary_chain(&mut self, id: ExprId) -> CResult<()> {
enum Step {
Binary(BinaryOp),
Logical(LogicalOp),
}
let mut spine = Vec::new();
let mut cur = id;
loop {
match self.ast.expr(cur) {
Some(Expr::Binary { op, left, right }) => {
spine.push((Step::Binary(*op), *right, cur));
cur = self.ast.unparenthesized(*left);
}
Some(Expr::Logical { op, left, right }) => {
spine.push((Step::Logical(*op), *right, cur));
cur = self.ast.unparenthesized(*left);
}
_ => break,
}
}
self.expr(cur)?;
for (step, right, node) in spine.into_iter().rev() {
match step {
Step::Binary(op) => {
self.expr(right)?;
self.at(self.ast.expr_span(node).start);
self.emit(Op::Binary(op));
}
Step::Logical(op) => {
let end = self.new_label();
self.jump(if op == LogicalOp::And { Op::JumpIfFalseKeep } else { Op::JumpIfTrueKeep }, end);
self.expr(right)?;
self.bind(end);
}
}
}
Ok(())
}
/// A chain of assignments nested on the right (`a = b.c = d[e] = 1`), compiled with a loop:
/// each target's object and key first, from the left, then the value, then the stores from
/// the right.
fn assign_chain(&mut self, id: ExprId) -> CResult<()> {
let mut chain: Vec<(AssignOp, ExprId, ExprId)> = Vec::new();
let mut cur = id;
while let Some(Expr::Assign { op, target, value }) = self.ast.expr(cur) {
chain.push((*op, self.ast.unparenthesized(*target), cur));
cur = self.ast.unparenthesized(*value);
}
for &(op, target, node) in &chain {
self.at(self.ast.expr_span(node).start);
self.assign_target(op, target)?;
}
self.expr(cur)?;
for &(op, target, node) in chain.iter().rev() {
self.at(self.ast.expr_span(node).start);
if let Some(b) = op {
self.emit(Op::Binary(b));
}
self.store(target)?;
}
Ok(())
}
/// Pushes what an assignment to `target` needs before its value: the object and key, and the
/// current value for a compound assignment.
fn assign_target(&mut self, op: AssignOp, target: ExprId) -> CResult<()> {
match self.ast.expr(target) {
Some(Expr::Ident(name)) => {
let n = self.var_name(name);
self.emit(Op::RefVar(n));
if op.is_some() {
self.emit(Op::Dup);
self.emit(Op::GetRef(n));
}
}
Some(Expr::Member { object, property }) => {
self.expr(*object)?;
if op.is_some() {
self.emit(Op::Dup);
let n = self.name_key(key_of(&property.name));
self.emit(Op::GetMember(n));
} else {
self.emit(Op::CheckObject);
}
}
Some(Expr::Index { object, index }) => {
self.expr(*object)?;
self.expr(*index)?;
self.emit(Op::ToKey);
if op.is_some() {
self.emit(Op::Dup2);
self.emit(Op::GetIndex);
}
}
_ => {
self.expr(target)?;
if op.is_none() {
self.emit(Op::Pop);
}
}
}
Ok(())
}
fn store(&mut self, target: ExprId) -> CResult<()> {
match self.ast.expr(target) {
Some(Expr::Ident(name)) => {
let n = self.var_name(name);
self.emit(Op::PutRef(n));
}
Some(Expr::Member { property, .. }) => {
let n = self.name_key(key_of(&property.name));
self.emit(Op::SetMember(n));
}
Some(Expr::Index { .. }) => self.emit(Op::SetIndex),
_ => self.throw_invalid_target(),
}
Ok(())
}
fn update(&mut self, op: UpdateOp, prefix: bool, target: ExprId) -> CResult<()> {
let target = self.ast.unparenthesized(target);
let dec = op == UpdateOp::Decrement;
match self.ast.expr(target) {
Some(Expr::Ident(name)) => {
let n = self.var_name(name);
self.emit(Op::IncVar { name: n, prefix, dec });
}
Some(Expr::Member { object, property }) => {
self.expr(*object)?;
let n = self.name_key(key_of(&property.name));
self.emit(Op::IncMember { name: n, prefix, dec });
}
Some(Expr::Index { object, index }) => {
self.expr(*object)?;
self.expr(*index)?;
self.emit(Op::IncIndex { prefix, dec });
}
_ => {
self.expr(target)?;
self.emit(Op::ToNumber);
self.throw_invalid_target();
}
}
Ok(())
}
// ---- finishing ----
fn finish(mut self, hoisted: Hoisted, meta: Meta) -> CResult<Rc<Code>> {
let resolve = |labels: &[Option<u32>], l: Label| labels.get(l.0).copied().flatten();
for &(i, label) in &self.patches {
let Some(target) = resolve(&self.labels, label) else { return internal("jump to an unplaced label") };
match self.ops.get_mut(i) {
Some(Op::Jump(t) | Op::JumpIfFalse(t) | Op::JumpIfTrue(t) | Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) | Op::ForInNext(t)) => {
*t = target
}
_ => return internal("patching an instruction that is not a jump"),
}
}
let mut handlers = Vec::new();
for &(start, end, label, depth, env_depth) in &self.handlers {
let Some(target) = resolve(&self.labels, label) else { return internal("handler at an unplaced label") };
handlers.push(Handler { start, end, target, depth, env_depth });
}
let mut finally_tables = Vec::new();
for table in &self.finally_tables {
let resolved: Option<Vec<u32>> = table.iter().map(|&l| resolve(&self.labels, l)).collect();
let Some(resolved) = resolved else { return internal("finally exit at an unplaced label") };
finally_tables.push(resolved);
}
let mut code = Code {
ops: std::mem::take(&mut self.ops),
positions: std::mem::take(&mut self.positions),
numbers: std::mem::take(&mut self.numbers),
strings: std::mem::take(&mut self.strings),
names: std::mem::take(&mut self.names),
functions: std::mem::take(&mut self.functions),
regexps: std::mem::take(&mut self.regexps),
handlers,
finally_tables,
max_stack: 0,
strict: self.strict,
self_name: meta.self_name,
params: meta.params,
vars: hoisted.vars,
fun_decls: hoisted.fun_decls,
uses_arguments: self.uses_arguments,
source: self.source.clone(),
span: meta.span,
};
code.max_stack = verify(&code).map_err(CompileFail::Internal)?;
Ok(Rc::new(code))
}
}
/// What a code knows about the function it is, if it is one.
#[derive(Default)]
struct Meta {
self_name: Option<Key>,
params: Vec<Key>,
span: (u32, u32),
}
fn len_u32(n: usize) -> u32 {
u32::try_from(n).unwrap_or(u32::MAX)
}
/// Compiles function `fid` of `ast`. With `bind_name`, a function expression's name is bound
/// inside it.
pub(crate) fn compile_function(ast: &Ast, fid: FunctionId, source: &Rc<Source>, recursion: u32, bind_name: bool) -> CResult<Rc<Code>> {
let Some(f) = ast.function(fid) else { return internal("function out of range") };
let mut c = FnCompiler::new(ast, source, CodeKind::Function, f.strict, recursion);
c.at(f.span.start);
c.enter()?;
let hoisted = c.hoist(&f.body)?;
c.stmts(&f.body)?;
c.at(f.span.end.saturating_sub(1));
c.emit(Op::Undefined);
c.emit(Op::Return);
let name = f.name.as_ref().map(|n| key_of(&n.name));
let meta = Meta {
self_name: if bind_name && f.kind == FunctionKind::Expression { name } else { None },
params: f.params.iter().map(|p| key_of(&p.name)).collect(),
span: (f.span.start, f.span.end),
};
c.finish(hoisted, meta)
}
/// Compiles a script (`kind` [`CodeKind::Global`]) or eval code ([`CodeKind::Eval`]).
pub(crate) fn compile_program(program: &crate::Program, source: &Rc<Source>, kind: CodeKind) -> CResult<Rc<Code>> {
let mut c = FnCompiler::new(&program.ast, source, kind, program.strict, 0);
let hoisted = c.hoist(&program.body)?;
c.stmts(&program.body)?;
c.emit(Op::ReturnCompletion);
let end = u32::try_from(source.text.len()).unwrap_or(u32::MAX);
c.finish(hoisted, Meta { span: (0, end), ..Meta::default() })
}
/// The function of a `Function` constructor's source, `(function anonymous(…) {…})`: the program
/// must be that one parenthesized function expression and nothing else. The name `anonymous` is
/// not bound inside the function (section 15.3.2.1).
pub(crate) fn compile_constructed_function(program: &crate::Program, source: &Rc<Source>) -> CResult<Option<Rc<Code>>> {
let [stmt] = program.body.as_slice() else { return Ok(None) };
let Some(Stmt::Expr(e)) = program.ast.stmt(*stmt) else { return Ok(None) };
let Some(Expr::Paren(inner)) = program.ast.expr(*e) else { return Ok(None) };
let Some(Expr::Function(fid)) = program.ast.expr(*inner) else { return Ok(None) };
compile_function(&program.ast, *fid, source, 0, false).map(Some)
}