Misc cleanups

This commit is contained in:
UditDey
2026-09-28 07:47:01 +00:00
parent e93ce94ac7
commit a4d295fbd9
18 changed files with 563 additions and 1388 deletions
+173 -193
View File
@@ -1,15 +1,16 @@
mod declass;
use declass::declass;
use crate::{
builtins,
flatten::FlatFn,
libqjs::parse::{
Atom, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, atom_to_str, opcodes::OpCode,
Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
model::{EXTRA_ARGS, FIELD_PROTO},
native::native_fn_idx,
};
use declass::declass;
pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(124); // TODO: Should this be <home_object> or __proto__ or what?
pub fn transform(
flat_fns: Vec<FlatFn>,
@@ -29,53 +30,43 @@ fn transform_one(
captures: &[Vec<ClosureVar>],
atoms: &[Atom],
) -> FlatFn {
let declassed = declass(&f.code);
let code = declass(&f.code);
// Every function gains two parameters after its own, in this order. The
// indices are static, so neither needs a sentinel.
let this_idx = f.arg_count as u16;
let env_idx = f.arg_count as u16 + 1;
// `this` and `env` are passed as the last 2 args to every fn
let _this_arg = f.arg_count;
let env_arg = f.arg_count + 1;
let mut out_len = 0;
// Calculate transformed out len
let mut len = 0;
for instr in &declassed {
if let Some(info) = is_fclosure(instr, &f, captures) {
// fclosure; (load; define_field)*
out_len += 1 + info.child_captures.iter().map(capture_len).sum::<usize>();
for instr in &code {
if let Some(child_captures) = is_fclosure(instr, &f, captures) {
len += 1 + child_captures.iter().map(capture_len).sum::<usize>(); // fclosure; (load; define_field)*
continue;
}
// get_arg env; swap; put_field
if let Some(CaptureAccess::Write(_)) = is_capture_acc(instr) {
out_len += 3;
if let Some(acc) = is_capture_acc(instr) {
len += acc.out_len();
continue;
}
if is_capture_acc(instr).is_some() {
out_len += 2;
if let Some(seq) = is_expansion(instr) {
len += seq.len();
continue;
}
if let Some(seq) = expansion(instr.op) {
out_len += seq.len();
continue;
}
out_len += 1;
len += 1;
}
let mut out = Vec::with_capacity(out_len);
// Emit transformed output
let mut out = Vec::with_capacity(len);
for instr in &declassed {
if replace_fclosure(instr, &f, captures, &captures[fn_id], env_idx, &mut out) {
for instr in &code {
if replace_fclosure(instr, &f, captures, &captures[fn_id], env_arg, &mut out) {
continue;
}
if replace_capture_accs(instr, &captures[fn_id], env_idx, &mut out) {
continue;
}
if replace_push_this(instr, this_idx, &mut out) {
if replace_capture_acc(instr, &captures[fn_id], env_arg, &mut out) {
continue;
}
@@ -87,128 +78,98 @@ fn transform_one(
continue;
}
out.push(renamed(instr).unwrap_or_else(|| instr.clone()));
out.push(instr.clone());
}
debug_assert!(out.len() == out_len);
debug_assert!(out.len() == len);
FlatFn {
name: f.name,
arg_count: f.arg_count + EXTRA_ARGS,
arg_count: f.arg_count + 2, // 2 extra args, `this` and `env`
var_count: f.var_count,
cpool: f.cpool,
code: out,
}
}
/// `this` is an ordinary parameter now, so reading it is an ordinary arg read.
fn replace_push_this(instr: &Instr, this_idx: u16, out: &mut Vec<Instr>) -> bool {
if instr.op != OpCode::PushThis {
return false;
}
out.push(Instr {
pc: instr.pc,
op: OpCode::GetArg,
imm: ImmOperand::Arg(this_idx),
});
true
}
struct FClosureInfo<'a> {
child_captures: &'a [ClosureVar],
}
// ─────────────────────────────────────────────── Closure Processing ────────────────────────────────────────────────────────────
/// If `instr` is a closure decl, return the child closure's captures
fn is_fclosure<'a>(
instr: &Instr,
f: &FlatFn,
captures: &'a [Vec<ClosureVar>],
) -> Option<FClosureInfo<'a>> {
) -> Option<&'a [ClosureVar]> {
if !matches!(instr.op, OpCode::Fclosure8 | OpCode::Fclosure) {
return None;
}
let cpool_idx = match instr.imm {
ImmOperand::Const(idx) => idx,
ImmOperand::Const(idx) => idx as usize,
_ => unreachable!(),
};
let child_fn_id = match f.cpool[cpool_idx as usize] {
let child_fn_id = match f.cpool[cpool_idx] {
JsValue::FnById(id) => id,
_ => unreachable!(),
};
Some(FClosureInfo {
child_captures: &captures[child_fn_id],
})
Some(&captures[child_fn_id])
}
/// Replace:
/// fclosure fn_const
///
/// With:
/// fclosure fn_const
/// (push_capture; define_field capture_name)*
///
/// ie the implicit closures are turned into explicit fields on the
/// fclosure object which serves as the environment record
fn replace_fclosure(
instr: &Instr,
f: &FlatFn,
captures: &[Vec<ClosureVar>],
my_captures: &[ClosureVar],
env_idx: u16,
env_arg: usize,
out: &mut Vec<Instr>,
) -> bool {
let info = match is_fclosure(instr, f, captures) {
Some(info) => info,
let child_captures = match is_fclosure(instr, f, captures) {
Some(c) => c,
None => return false,
};
// The closure keeps the op that makes it: `fclosure` creates a closure of the
// child with an empty environment, and each capture is written into that
// environment as an ordinary field.
out.push(instr.clone());
for c in info.child_captures {
if !load_capture(c, my_captures, env_idx, instr.pc, out) {
continue;
}
let mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op: OpCode::DefineField,
imm: ImmOperand::Atom(c.name),
});
}
op,
imm,
})
};
true
}
push(OpCode::Fclosure, instr.imm);
/// Load the value a child closure captures, from where it lives in this frame.
/// `var_idx` means something different per kind: a local slot, an argument, or
/// one of this function's own captures — which, after lowering, is a field of
/// its env. A module binding has no frame at all, so nothing is loaded and the
/// capture is not written: its field first appears when `<eval>` writes it,
/// and a read before that finds no field and gives undefined.
fn load_capture(
c: &ClosureVar,
my_captures: &[ClosureVar],
env_idx: u16,
pc: usize,
out: &mut Vec<Instr>,
) -> bool {
let mut push = |op, imm| out.push(Instr { pc, op, imm });
match c.flags.closure_kind {
ClosureKind::Local => push(OpCode::GetLoc, ImmOperand::Loc(c.var_idx as u16)),
ClosureKind::Arg => push(OpCode::GetArg, ImmOperand::Arg(c.var_idx as u16)),
ClosureKind::Ref => {
push(OpCode::GetArg, ImmOperand::Arg(env_idx));
push(
OpCode::GetField,
ImmOperand::Atom(my_captures[c.var_idx].name),
);
for c in child_captures {
match c.flags.closure_kind {
ClosureKind::Local => push(OpCode::GetLoc, ImmOperand::Loc(c.var_idx as u16)),
ClosureKind::Arg => push(OpCode::GetArg, ImmOperand::Arg(c.var_idx as u16)),
ClosureKind::Ref => {
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(
OpCode::GetField,
ImmOperand::Atom(my_captures[c.var_idx].name),
);
}
_ => return false,
}
_ => return false,
push(OpCode::DefineField, ImmOperand::Atom(c.name));
}
true
}
/// How many instructions one capture takes: its load, then its define_field.
/// How many instructions one capture takes: its load, then its define_field
fn capture_len(c: &ClosureVar) -> usize {
match c.flags.closure_kind {
ClosureKind::Local | ClosureKind::Arg => 2,
@@ -217,11 +178,22 @@ fn capture_len(c: &ClosureVar) -> usize {
}
}
// ──────────────────────────────────────────── Capture Access Processing ────────────────────────────────────────────────────────
enum CaptureAccess {
Read(u16),
Write(u16),
}
impl CaptureAccess {
fn out_len(&self) -> usize {
match self {
Self::Read(_) => 2, // See: replace_capture_acc()
Self::Write(_) => 3,
}
}
}
/// Is `instr` an access of a captured variable?
fn is_capture_acc(instr: &Instr) -> Option<CaptureAccess> {
let acc = match instr.op {
OpCode::GetVarRef0 => CaptureAccess::Read(0),
@@ -248,80 +220,120 @@ fn is_capture_acc(instr: &Instr) -> Option<CaptureAccess> {
Some(acc)
}
fn replace_capture_accs(
/// Replace:
/// read/write_captured_var
///
/// With:
/// get_env_arg
/// read_write_env_arg
fn replace_capture_acc(
instr: &Instr,
my_captures: &[ClosureVar],
env_idx: u16,
env_arg: usize,
out: &mut Vec<Instr>,
) -> bool {
let acc = is_capture_acc(instr);
let acc = match acc {
let acc = match is_capture_acc(instr) {
Some(acc) => acc,
None => return false,
};
let pc = instr.pc;
let mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
match acc {
CaptureAccess::Read(read_var) => {
let atom = my_captures[read_var as usize].name;
out.push(Instr {
pc,
op: OpCode::GetArg,
imm: ImmOperand::Arg(env_idx),
});
out.push(Instr {
pc,
op: OpCode::GetField,
imm: ImmOperand::Atom(atom),
});
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(OpCode::GetField, ImmOperand::Atom(atom));
}
CaptureAccess::Write(write_var) => {
let atom = my_captures[write_var as usize].name;
// The value is already on the stack, so env lands above it, and
// put_field wants the object below the value.
out.push(Instr {
pc,
op: OpCode::GetArg,
imm: ImmOperand::Arg(env_idx),
});
out.push(Instr {
pc,
op: OpCode::Swap,
imm: ImmOperand::None,
});
out.push(Instr {
pc,
op: OpCode::PutField,
imm: ImmOperand::Atom(atom),
});
// The value to write is already on the stack, so env lands above it,
// and put_field wants the object below the value
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(OpCode::Swap, ImmOperand::None);
push(OpCode::PutField, ImmOperand::Atom(atom));
}
}
true
}
/// Ops that are a fixed sequence of simpler ones. `true` passes the original
/// immediate on to that op.
fn expansion(op: OpCode) -> Option<&'static [(OpCode, bool)]> {
// ───────────────────────────────────────────────── Misc Expansion ──────────────────────────────────────────────────────────────
#[allow(non_snake_case)]
fn is_expansion(instr: &Instr) -> Option<[Instr; 4]> {
use OpCode::*;
let seq: &'static [(OpCode, bool)] = match op {
TailCall => &[(Call, true), (Return, false)],
TailCallMethod => &[(CallMethod, true), (Return, false)],
GetField2 => &[(Dup, false), (GetField, true)],
GetLoc0Loc1 => &[(GetLoc0, false), (GetLoc1, false)],
IncLoc => &[(GetLoc, true), (Inc, false), (PutLoc, true)],
DecLoc => &[(GetLoc, true), (Dec, false), (PutLoc, true)],
// The added value is on the stack; the local is the left operand.
AddLoc => &[(GetLoc, true), (Swap, false), (Add, false), (PutLoc, true)],
let ins = |op, imm| Instr {
pc: instr.pc,
op,
imm,
};
let nop = || Instr {
pc: instr.pc,
op: Nop,
imm: ImmOperand::None,
};
let seq = match instr.op {
TailCall => [
nop(),
nop(),
ins(Call, instr.imm),
ins(Return, ImmOperand::None),
],
TailCallMethod => [
nop(),
nop(),
ins(CallMethod, instr.imm),
ins(Return, ImmOperand::None),
],
GetField2 => [
nop(),
nop(),
ins(Dup, ImmOperand::None),
ins(GetField, instr.imm),
],
GetLoc0Loc1 => [
nop(),
nop(),
ins(GetLoc0, ImmOperand::None),
ins(GetLoc1, ImmOperand::None),
],
IncLoc => [
nop(),
ins(GetLoc, instr.imm),
ins(Inc, ImmOperand::None),
ins(PutLoc, instr.imm),
],
DecLoc => [
nop(),
ins(GetLoc, instr.imm),
ins(Dec, ImmOperand::None),
ins(PutLoc, instr.imm),
],
AddLoc => [
ins(GetLoc, instr.imm),
ins(Swap, ImmOperand::None), // The added value is on the stack, the local is the left operand
ins(Add, ImmOperand::None),
ins(PutLoc, instr.imm),
],
SetProto => [
nop(),
nop(),
nop(),
ins(DefineField, ImmOperand::Atom(PROTO_FIELD)),
],
DefineMethod => [nop(), nop(), nop(), ins(DefineField, instr.imm)], // TODO: should we discriminate on instr.imm?
SetLocUninitialized => [nop(), nop(), nop(), nop()],
_ => return None,
};
@@ -329,39 +341,31 @@ fn expansion(op: OpCode) -> Option<&'static [(OpCode, bool)]> {
}
fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
let Some(seq) = expansion(instr.op) else {
let Some(seq) = is_expansion(instr) else {
return false;
};
for &(op, keep_imm) in seq {
let imm = if keep_imm { instr.imm } else { ImmOperand::None };
out.push(Instr { pc: instr.pc, op, imm });
}
out.extend_from_slice(&seq);
true
}
/// The only globals a program reads are the C functions builtins call. Each
/// becomes a constant naming it.
fn replace_get_var(
instr: &Instr,
atoms: &[Atom],
cpool: &mut Vec<JsValue>,
out: &mut Vec<Instr>,
) -> bool {
let ImmOperand::Atom(atom) = instr.imm else {
return false;
};
if instr.op != OpCode::GetVar {
return false;
}
let name = atom_to_str(&atom, atoms);
let native = builtins::native(&name).unwrap_or_else(|| panic!("unknown global `{name}`"));
let ImmOperand::Atom(name) = instr.imm else {
return false;
};
cpool.push(JsValue::Native(native));
let native_idx = native_fn_idx(name, atoms).unwrap();
cpool.push(JsValue::NativeFn(native_idx));
out.push(Instr {
pc: instr.pc,
op: OpCode::PushConst,
@@ -370,27 +374,3 @@ fn replace_get_var(
true
}
/// Ops that are another op with a different immediate.
fn renamed(instr: &Instr) -> Option<Instr> {
let define_field = |atom| Instr {
pc: instr.pc,
op: OpCode::DefineField,
imm: ImmOperand::Atom(atom),
};
match (instr.op, instr.imm) {
// `{ __proto__: p }`: the prototype is an ordinary field.
(OpCode::SetProto, _) => Some(define_field(FIELD_PROTO)),
// A method in an object literal is a field holding a closure. The low
// two bits of `arg` say method, getter or setter; only methods are
// supported.
(OpCode::DefineMethod, ImmOperand::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
Some(define_field(atom))
}
_ => None,
}
}