Misc cleanups
This commit is contained in:
+173
-193
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user