Files
jsrt/compiler/src/transform/mod.rs
T
2026-10-01 13:58:50 +00:00

449 lines
17 KiB
Rust

mod declass;
use std::collections::HashMap;
use declass::declass;
use crate::{
flatten::FlatFn,
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
native,
regify::{is_var_read, is_var_write},
};
/// The field an object's prototype is kept in. In JS, `o.__proto__` is the
/// prototype of an ordinary object, so user code that reads or writes it
/// gets the right behaviour with no extra work.
pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(72); // __proto__
/// The field a constructor keeps the prototype of its instances in
pub const PROTOTYPE_FIELD: AtomRef = AtomRef::Predefined(63); // prototype
/// Every fn takes its env and `this` as its first two params, and its declared
/// params after them. So the params of a call do not depend on how many args
/// the callee declares: a missing arg is not passed, and an extra one is not
/// read.
pub const ENV_ARG: u16 = 0;
pub const THIS_ARG: u16 = 1;
pub const FIRST_ARG: u16 = 2;
/// The field a box keeps its binding's value in (see replace_bindings())
pub const BOX_FIELD: AtomRef = AtomRef::Predefined(86); // <var>
pub fn transform(
flat_fns: Vec<FlatFn>,
captures: &[Vec<ClosureVar>],
atoms: &[Atom],
) -> Vec<FlatFn> {
flat_fns
.into_iter()
.map(|f| transform_one(f, captures, atoms))
.collect()
}
fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) -> FlatFn {
let code = declass(&f.code);
// Temp locals, after the fn's own, for the args of a `new` (see replace_new())
let temps = f.var_count;
let n_temps = code.iter().filter_map(is_new).max().unwrap_or(0);
let mut out = Vec::with_capacity(code.len());
for instr in &code {
if replace_new(instr, temps, &mut out) {
continue;
}
if replace_arg_access(instr, &mut out) {
continue;
}
if replace_expansion(instr, &mut out) {
continue;
}
if replace_get_var(instr, atoms, &mut f.cpool, &mut out) {
continue;
}
out.push(instr.clone());
}
// Last, so that it also sees the var accesses that the expansions make
let (code, var_count) = replace_bindings(&out, &f.cpool, captures, temps + n_temps);
FlatFn {
name: f.name,
arg_count: f.arg_count + FIRST_ARG as usize,
var_count,
cpool: f.cpool,
code,
}
}
/// Pushes instrs at the pc of the instr they replace
fn pusher(pc: usize, out: &mut Vec<Instr>) -> impl FnMut(OpCode, ImmOperand) + '_ {
move |op, imm| out.push(Instr { pc, op, imm })
}
// ───────────────────────────────────────────────── New Processing ──────────────────────────────────────────────────────────────
/// If `instr` is a `new`, the number of args it passes
fn is_new(instr: &Instr) -> Option<usize> {
match (instr.op, instr.imm) {
(OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize),
_ => None,
}
}
/// Replace:
/// call_constructor n [F, new_target, a0 .. an-1]
///
/// With:
/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top
/// drop new_target, always a copy of F here
/// dup [F, F]
/// get_field prototype [F, proto]
/// object [F, proto, obj]
/// swap [F, obj, proto]
/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype }
/// get_loc t0 .. get_loc t(n-1) the args back
/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj
///
/// Whether `new` gives the call's result or `obj` depends on types (see infer)
fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
let Some(n) = is_new(instr) else {
return false;
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
}
push(OpCode::Drop, ImmOperand::None);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD));
push(OpCode::Object, ImmOperand::None);
push(OpCode::Swap, ImmOperand::None);
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
for i in 0..n {
push(OpCode::GetLoc, temp(i));
}
push(OpCode::CallConstructor, instr.imm);
true
}
// ─────────────────────────────────────────────── Binding Processing ────────────────────────────────────────────────────────────
/// Put every binding that a closure captures in a box: an object whose
/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The
/// owner and the closures read and write the box. A closure keeps the boxes
/// it captured in its own fields, one for each of its `closure_vars`: field `i`
/// holds the box of capture `i`
///
/// The fn makes a box for each binding that its children capture, at entry.
/// A captured local holds its box in its own slot. A captured arg keeps its
/// box in a new local, and the box starts with the arg's value. A `var` starts
/// as undefined. A `let` or `const` has no value until it is written (its TDZ):
/// object; undefined; define_field <var>; put_loc k each captured `var`
/// object; put_loc k each captured `let`
/// object; get_arg a; define_field <var>; put_loc box each captured arg
///
/// A scope that is entered again is a loop body. QuickJS closes each captured
/// binding of the scope (`close_loc`) at every way out of it, so a new box
/// there gives each iteration its own binding. `set_loc_uninitialized` only
/// marks the TDZ, which is not checked, and hoisted fns are made before it, so
/// it does not make a box
///
/// These instrs take a pc that no jump goes to, so a jump to the fn's first
/// instr does not make them again
///
/// Then it replaces, for a captured binding:
/// get_loc k get_loc box; get_field <var>
/// put_loc k get_loc box; swap; put_field <var>
/// set_loc k dup; get_loc box; swap; put_field <var>
/// close_loc k object; get_loc k; get_field <var>;
/// define_field <var>; put_loc k
/// a way out of a loop body: a new binding, with
/// the current value
///
/// For any binding:
/// get_var_ref i get_arg env; get_field i; get_field <var>
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
/// fclosure f fclosure f; (push_box; define_field i)*
///
/// where `push_box` is the box of capture `i` of `f`: the local that holds it,
/// or field `j` of this fn's env for capture `j` of this fn. A binding with no
/// frame (a module binding) gets its box here, from `object`
///
/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure
/// captures, do nothing. Gives the code and the new number of locals
fn replace_bindings(
code: &[Instr],
cpool: &[JsValue],
captures: &[Vec<ClosureVar>],
var_count: usize,
) -> (Vec<Instr>, usize) {
use OpCode::*;
let children = cpool.iter().filter_map(|c| match c {
JsValue::FnById(child) => Some(*child),
_ => None,
});
let mut locals = vec![];
let mut args = vec![];
for c in children.flat_map(|child| &captures[child]) {
match c.flags.closure_kind {
ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)),
ClosureKind::Arg => args.push(c.var_idx),
_ => {}
}
}
locals.sort_unstable();
locals.dedup();
args.sort_unstable();
args.dedup();
// Value ids of bindings, as `is_var_read` gives them: args after every local
let new_var_count = var_count + args.len();
let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k;
// The local that holds the box of each captured binding, by value id
let boxes: HashMap<usize, usize> = (locals.iter().map(|&(k, _)| (k, k)))
.chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i)))
.collect();
let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32));
let value = ImmOperand::Atom(BOX_FIELD);
let none = ImmOperand::None;
let loc = |k: usize| ImmOperand::Loc(k as u16);
let mut out = Vec::with_capacity(code.len());
{
let mut push = pusher(usize::MAX, &mut out);
for &(k, lexical) in &locals {
push(Object, none);
if !lexical {
push(Undefined, none);
push(DefineField, value);
}
push(PutLoc, loc(k));
}
for (i, &k) in args.iter().enumerate() {
push(Object, none);
push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16));
push(DefineField, value);
push(PutLoc, loc(var_count + i));
}
}
for instr in code {
let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v));
let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v));
let own_box = |k: ImmOperand| match k {
ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(),
_ => None,
};
let mut push = pusher(instr.pc, &mut out);
if let Some(&b) = read {
push(GetLoc, loc(b));
push(GetField, value);
} else if let Some(&b) = write {
if instr.op.info().n_push == 1 {
push(Dup, none);
}
push(GetLoc, loc(b));
push(Swap, none);
push(PutField, value);
} else if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, none);
}
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(i));
if writes {
push(Swap, none);
push(PutField, value);
} else {
push(GetField, value);
}
} else if instr.op == SetLocUninitialized {
push(Nop, none);
} else if instr.op == CloseLoc {
match own_box(instr.imm) {
Some(b) => {
push(Object, none);
push(GetLoc, loc(b));
push(GetField, value);
push(DefineField, value);
push(PutLoc, loc(b));
}
None => push(Nop, none),
}
} else if matches!(instr.op, Fclosure8 | Fclosure) {
let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
for (i, c) in captures[child].iter().enumerate() {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])),
ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])),
ClosureKind::Ref => {
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(c.var_idx));
}
_ => push(Object, none),
}
push(DefineField, slot(i));
}
} else {
push(instr.op, instr.imm);
}
}
(out, new_var_count)
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes,
/// and whether a write also leaves the value on the stack
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
ImmOperand::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)),
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)),
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)),
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}
// ──────────────────────────────────────────────── Arg Access Processing ────────────────────────────────────────────────────────
/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`,
/// and `this` with a read of its param
fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let (op, arg) = match (instr.op, instr.imm) {
(PushThis, _) => (GetArg, THIS_ARG),
(GetArg0, _) => (GetArg, FIRST_ARG),
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
(PutArg0, _) => (PutArg, FIRST_ARG),
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
(SetArg0, _) => (SetArg, FIRST_ARG),
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
_ => return false,
};
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
true
}
// ───────────────────────────────────────────────── Misc Expansion ──────────────────────────────────────────────────────────────
/// Replace ops that are a fixed sequence of simpler ones
///
/// A sequence is never empty: jumps find their target by its pc (see regify),
/// so every instr must keep at least one instr at its pc
fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let none = ImmOperand::None;
let imm = instr.imm;
// A plain call has no `this`, so it passes `undefined`. It is pushed last,
// on top of the args, and the call pops it as one more arg (see regify)
let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))];
let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)];
let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) {
(Call0, _) => &plain_call(0),
(Call1, _) => &plain_call(1),
(Call2, _) => &plain_call(2),
(Call3, _) => &plain_call(3),
(Call, ImmOperand::Npop(n)) => &plain_call(n),
(TailCall, ImmOperand::Npop(n)) => &tail_call(n),
(TailCallMethod, _) => &[(CallMethod, imm), (Return, none)],
(GetField2, _) => &[(Dup, none), (GetField, imm)],
(GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)],
(IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)],
(DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)],
// The added value is on the stack, the local is the left operand
(AddLoc, _) => &[(GetLoc, imm), (Swap, none), (Add, none), (PutLoc, imm)],
(SetProto, _) => &[(DefineField, ImmOperand::Atom(PROTO_FIELD))],
// The low two bits of `arg`: 0 method, 1 getter, 2 setter
(DefineMethod, ImmOperand::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
&[(DefineField, ImmOperand::Atom(atom))]
}
// The fn itself, which is its env
(SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))],
// [[HomeObject]], which only `super` reads. `super` is not supported
// (see set_home_object in regify)
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
// arguments, new.target, import.meta
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
_ => return false,
};
let mut push = pusher(instr.pc, out);
seq.iter().for_each(|&(op, imm)| push(op, imm));
true
}
/// Replace a read of a global with the constant it is (see `native`)
fn replace_get_var(
instr: &Instr,
atoms: &[Atom],
cpool: &mut Vec<JsValue>,
out: &mut Vec<Instr>,
) -> bool {
if instr.op != OpCode::GetVar {
return false;
}
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}