First declosure impl
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
//! `get_field "prototype" ; put_field @meth`; the field-initialiser tail is
|
||||
//! dropped. Detection (`find_class_seqs`) records the ranges; replacement
|
||||
//! (`ClassSequence::emit_prototype`) reads operands out of each matched range.
|
||||
//!
|
||||
//! Derived classes (`extends`) are unsupported and panic: prototype form would
|
||||
//! have to flatten the parent's prototype in and lower `super`.
|
||||
|
||||
use std::range::Range;
|
||||
|
||||
@@ -160,6 +163,16 @@ fn find_class_seqs(mut ins: &[RegInstr]) -> Vec<ClassSequence> {
|
||||
|
||||
fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> {
|
||||
let ins = match_one(ins, |i| i.op == OpCode::PushConst8)?;
|
||||
|
||||
// A class build is `push_const8 ; define_class`. Derived classes (`extends`)
|
||||
// are unsupported: prototype form would have to flatten the parent's
|
||||
// prototype in and lower `super`, neither of which is implemented.
|
||||
if let Some(i) = ins.first() {
|
||||
if i.op == OpCode::DefineClass && is_derived(i) {
|
||||
panic!("declass: derived classes (extends) are not supported");
|
||||
}
|
||||
}
|
||||
|
||||
let ins = match_one(ins, |i| i.op == OpCode::DefineClass)?;
|
||||
let (ins, num_methods) = match_many(ins, match_method_seq);
|
||||
let ins = match_opt(ins, match_field_init_seq);
|
||||
@@ -167,6 +180,15 @@ fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]
|
||||
Ok((ins, num_methods))
|
||||
}
|
||||
|
||||
/// A derived class: `define_class` with the derived flag set.
|
||||
fn is_derived(i: &RegInstr) -> bool {
|
||||
matches!(i.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1)
|
||||
}
|
||||
|
||||
// TODO: accessors are not handled. A getter/setter is a `define_method` with a
|
||||
// non-zero kind in `arg`; this matcher takes it for an ordinary method, so it
|
||||
// is wrongly lowered to a `put_field` (a data property) instead of an accessor
|
||||
// property. It should instead make the whole class pass through untouched.
|
||||
fn match_method_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> {
|
||||
let ins = match_one(ins, |i| i.op == OpCode::Fclosure8)?;
|
||||
let ins = match_one(ins, |i| i.op == OpCode::DefineMethod)?;
|
||||
@@ -177,7 +199,12 @@ fn match_field_init_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> {
|
||||
let is_put_loc = |op| {
|
||||
matches!(
|
||||
op,
|
||||
OpCode::PutLoc | OpCode::PutLoc0 | OpCode::PutLoc1 | OpCode::PutLoc2 | OpCode::PutLoc3
|
||||
OpCode::PutLoc
|
||||
| OpCode::PutLoc8
|
||||
| OpCode::PutLoc0
|
||||
| OpCode::PutLoc1
|
||||
| OpCode::PutLoc2
|
||||
| OpCode::PutLoc3
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
//! declosure: closures become plain objects.
|
||||
//!
|
||||
//! A closure becomes an object holding its function plus its captured
|
||||
//! variables (docs/declosure_algo.txt). Field 0 is the function; fields 1..
|
||||
//! are the captures, in capture order. The object doubles as the environment,
|
||||
//! so there is no second env value to track. Field names are plain integers,
|
||||
//! so a field operand reads `Atom(int(k))`.
|
||||
//!
|
||||
//! Three steps, one property each:
|
||||
//!
|
||||
//! 1. `take_env_param` -- every function takes the env as its
|
||||
//! LAST parameter and reads its captures out of it: `get_var_ref` /
|
||||
//! `put_var_ref` become field access on the env.
|
||||
//! 2. `build_closure_objects` -- every `fclosure` builds the object: the
|
||||
//! function in field 0, then one field per capture, loaded from the
|
||||
//! parent frame.
|
||||
//! 3. `route_calls_through_object` -- every plain call reads field 0 and
|
||||
//! passes the object itself as the trailing env argument.
|
||||
//!
|
||||
//! Every function takes an env whether it captures anything or not, so step 3
|
||||
//! can append the object at a uniform call site.
|
||||
//!
|
||||
//! Two facts about the register form this leans on:
|
||||
//!
|
||||
//! * A function is a plain constant -- an index into the cpool. `fclosure`
|
||||
//! becomes `push_const`, so it loads like any other const, and storing it
|
||||
//! in a field is no different from storing a number.
|
||||
//! * The callee of a call is its LAST argument (the deepest stack slot,
|
||||
//! popped last), so step 3 reads `args.last()`.
|
||||
//!
|
||||
//! Known gaps, in rough order of how likely they are to bite:
|
||||
//!
|
||||
//! * A captured variable the parent later writes. The write produces a new
|
||||
//! value instead of storing through the alias, so the closure never sees
|
||||
//! it. A capture aliases the value a slot held at capture time.
|
||||
//! * Non-local capture sources. Step 2 can materialise a capture that comes
|
||||
//! from a parent local or arg. A capture that itself comes from the
|
||||
//! parent's captures, a module binding, or a global is left as an
|
||||
//! `undefined` placeholder -- it needs the parent to forward its own env.
|
||||
//! * Foreign callees. Step 3 rewrites every plain call into a field-0 read.
|
||||
//! That is only valid for functions we produced; a host builtin such as
|
||||
//! `Math.sqrt` has no such field.
|
||||
//! * Entry points. Step 1 gives every function an env parameter, but the
|
||||
//! module root and any host-invoked function have no caller of ours to
|
||||
//! supply one.
|
||||
//! * `call_method` and `call_constructor` carry a receiver, so they belong
|
||||
//! to the class passes; step 3 leaves them untouched. Same for `apply`.
|
||||
|
||||
use crate::{
|
||||
flatten::CaptureFrom,
|
||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||
regify::{RegFn, RegInstr},
|
||||
};
|
||||
|
||||
pub fn declosure(reg_fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||
// Where each function's captures come from, per fn id. Snapshotted first,
|
||||
// because step 1 clears the capture lists that step 2 reads.
|
||||
let capture_sources: Vec<Vec<CaptureFrom>> = reg_fns
|
||||
.iter()
|
||||
.map(|f| f.captures.iter().map(|c| c.from).collect())
|
||||
.collect();
|
||||
|
||||
reg_fns
|
||||
.into_iter()
|
||||
.map(|f| declosure_one(f, &capture_sources))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn declosure_one(reg_fn: RegFn, capture_sources: &[Vec<CaptureFrom>]) -> RegFn {
|
||||
let reg_fn = take_env_param(reg_fn);
|
||||
let reg_fn = build_closure_objects(reg_fn, capture_sources);
|
||||
route_calls_through_object(reg_fn)
|
||||
}
|
||||
|
||||
// ── 1. every function takes an env ─────────────────────────────────────────
|
||||
|
||||
/// Append `env` as the last parameter -- existing arguments keep their numbers
|
||||
/// -- and turn every capture access into a field access on it.
|
||||
fn take_env_param(mut reg_fn: RegFn) -> RegFn {
|
||||
let env_arg = reg_fn.arg_count;
|
||||
reg_fn.arg_count += 1;
|
||||
|
||||
let code = std::mem::take(&mut reg_fn.code);
|
||||
let touches_captures = code.iter().any(|i| capture_access(i).is_some());
|
||||
|
||||
let mut out = Vec::with_capacity(code.len() + 1);
|
||||
let env = reg_fn.fresh_val();
|
||||
|
||||
// Load the env once at entry -- only if a capture is actually accessed.
|
||||
if touches_captures {
|
||||
out.push(get_arg(env, env_arg));
|
||||
}
|
||||
|
||||
for instr in code {
|
||||
match capture_access(&instr) {
|
||||
// A read keeps its destination, so downstream uses do not change.
|
||||
Some(CaptureAccess::Read(k)) => {
|
||||
out.push(get_field(int_field(k + 1), instr.dsts[0], env))
|
||||
}
|
||||
Some(CaptureAccess::Write(k)) => {
|
||||
out.push(put_field(int_field(k + 1), instr.args[0], env))
|
||||
}
|
||||
None => out.push(instr),
|
||||
}
|
||||
}
|
||||
|
||||
reg_fn.code = out;
|
||||
reg_fn.captures.clear();
|
||||
reg_fn
|
||||
}
|
||||
|
||||
/// Which capture an instruction reads or writes, if any.
|
||||
enum CaptureAccess {
|
||||
Read(u32),
|
||||
Write(u32),
|
||||
}
|
||||
|
||||
fn capture_access(i: &RegInstr) -> Option<CaptureAccess> {
|
||||
// The numbered forms fold the slot into the opcode; the rest carry it.
|
||||
let slot = || match i.imm {
|
||||
ImmOperand::VarRef(k) => k as u32,
|
||||
_ => unreachable!("a var_ref opcode without a var_ref operand"),
|
||||
};
|
||||
Some(match i.op {
|
||||
OpCode::GetVarRef | OpCode::GetVarRefCheck => CaptureAccess::Read(slot()),
|
||||
OpCode::GetVarRef0 => CaptureAccess::Read(0),
|
||||
OpCode::GetVarRef1 => CaptureAccess::Read(1),
|
||||
OpCode::GetVarRef2 => CaptureAccess::Read(2),
|
||||
OpCode::GetVarRef3 => CaptureAccess::Read(3),
|
||||
OpCode::PutVarRef | OpCode::PutVarRefCheck | OpCode::PutVarRefCheckInit => {
|
||||
CaptureAccess::Write(slot())
|
||||
}
|
||||
OpCode::PutVarRef0 => CaptureAccess::Write(0),
|
||||
OpCode::PutVarRef1 => CaptureAccess::Write(1),
|
||||
OpCode::PutVarRef2 => CaptureAccess::Write(2),
|
||||
OpCode::PutVarRef3 => CaptureAccess::Write(3),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// ── 2. every fclosure builds the object ────────────────────────────────────
|
||||
|
||||
/// Replace each `fclosure` with the object that stands for it: the function in
|
||||
/// field 0, then one field per capture, each loaded from the parent frame.
|
||||
fn build_closure_objects(mut reg_fn: RegFn, capture_sources: &[Vec<CaptureFrom>]) -> RegFn {
|
||||
let code = std::mem::take(&mut reg_fn.code);
|
||||
let mut out = Vec::with_capacity(code.len());
|
||||
|
||||
for instr in code {
|
||||
let Some(child) = closure_child(&instr, ®_fn.cpool) else {
|
||||
out.push(instr);
|
||||
continue;
|
||||
};
|
||||
// The value the rest of the code already uses for this closure.
|
||||
let closure = instr.dsts[0];
|
||||
let captures: &[CaptureFrom] = capture_sources
|
||||
.get(child)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
|
||||
// The function is a plain constant, so it loads with `push_const`.
|
||||
let load = match instr.op {
|
||||
OpCode::Fclosure8 => OpCode::PushConst8,
|
||||
_ => OpCode::PushConst,
|
||||
};
|
||||
let func = reg_fn.fresh_val();
|
||||
out.push(RegInstr::new(load, instr.imm, vec![func], vec![]));
|
||||
|
||||
let mut obj = reg_fn.fresh_val();
|
||||
out.push(object(obj));
|
||||
|
||||
// Each define_field returns the object, so the stores chain. The LAST
|
||||
// store must land in `closure`.
|
||||
let dst = if captures.is_empty() {
|
||||
closure
|
||||
} else {
|
||||
reg_fn.fresh_val()
|
||||
};
|
||||
out.push(define_field(int_field(0), dst, func, obj));
|
||||
obj = dst;
|
||||
|
||||
for (k, source) in captures.iter().enumerate() {
|
||||
let value = reg_fn.fresh_val();
|
||||
out.push(load_capture(*source, value));
|
||||
|
||||
let dst = if k + 1 == captures.len() {
|
||||
closure
|
||||
} else {
|
||||
reg_fn.fresh_val()
|
||||
};
|
||||
out.push(define_field(int_field(k as u32 + 1), dst, value, obj));
|
||||
obj = dst;
|
||||
}
|
||||
}
|
||||
|
||||
reg_fn.code = out;
|
||||
reg_fn
|
||||
}
|
||||
|
||||
/// The nested function an `fclosure` instantiates, as a fn id.
|
||||
fn closure_child(i: &RegInstr, cpool: &[JsValue]) -> Option<usize> {
|
||||
if !matches!(i.op, OpCode::Fclosure | OpCode::Fclosure8) {
|
||||
return None;
|
||||
}
|
||||
let ImmOperand::Const(index) = i.imm else {
|
||||
return None;
|
||||
};
|
||||
match cpool.get(index as usize) {
|
||||
Some(JsValue::FnById(id)) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load one captured value out of the parent frame into `into`. Only parent
|
||||
/// locals and args are supported; any other source is a preserved gap and
|
||||
/// loads `undefined`.
|
||||
fn load_capture(source: CaptureFrom, into: usize) -> RegInstr {
|
||||
match source {
|
||||
CaptureFrom::ParentLocal(slot) => get_loc(into, slot),
|
||||
CaptureFrom::ParentArg(slot) => get_arg(into, slot),
|
||||
_ => RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![into], vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. every call goes through the object ──────────────────────────────────
|
||||
|
||||
/// A call names a closure object now, not a function. Read the function out of
|
||||
/// field 0, and pass the object itself as the trailing env argument.
|
||||
fn route_calls_through_object(mut reg_fn: RegFn) -> RegFn {
|
||||
let code = std::mem::take(&mut reg_fn.code);
|
||||
let mut out = Vec::with_capacity(code.len());
|
||||
|
||||
for instr in code {
|
||||
let Some((op, argc)) = plain_call(&instr) else {
|
||||
out.push(instr);
|
||||
continue;
|
||||
};
|
||||
|
||||
// The callee is the deepest stack slot, so it is popped last.
|
||||
let closure = *instr.args.last().unwrap();
|
||||
let func = reg_fn.fresh_val();
|
||||
out.push(get_field(int_field(0), func, closure));
|
||||
|
||||
// Args become [ env, <original args>, func ]: the env is pushed last so
|
||||
// it pops first, and the callee stays last.
|
||||
let mut args = Vec::with_capacity(instr.args.len() + 1);
|
||||
args.push(closure);
|
||||
args.extend_from_slice(&instr.args[..instr.args.len() - 1]);
|
||||
args.push(func);
|
||||
|
||||
out.push(RegInstr::new(
|
||||
op,
|
||||
ImmOperand::Npop(argc + 1),
|
||||
instr.dsts,
|
||||
args,
|
||||
));
|
||||
}
|
||||
|
||||
reg_fn.code = out;
|
||||
reg_fn
|
||||
}
|
||||
|
||||
/// The opcode to emit and the argument count, for a plain call. `None` if this
|
||||
/// is not one: the receiver-carrying calls (`call_method`, `call_constructor`,
|
||||
/// `apply`) are deliberately excluded.
|
||||
///
|
||||
/// A tail call stays a tail call -- it pushes nothing and returns, so turning
|
||||
/// it into an ordinary `call` would leave a value behind and drop the return.
|
||||
fn plain_call(i: &RegInstr) -> Option<(OpCode, u16)> {
|
||||
if i.args.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let npop = || match i.imm {
|
||||
ImmOperand::Npop(n) => n,
|
||||
_ => unreachable!("a call opcode without an npop operand"),
|
||||
};
|
||||
Some(match i.op {
|
||||
OpCode::Call0 => (OpCode::Call, 0),
|
||||
OpCode::Call1 => (OpCode::Call, 1),
|
||||
OpCode::Call2 => (OpCode::Call, 2),
|
||||
OpCode::Call3 => (OpCode::Call, 3),
|
||||
OpCode::Call => (OpCode::Call, npop()),
|
||||
OpCode::TailCall => (OpCode::TailCall, npop()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// ── emitted instructions ───────────────────────────────────────────────────
|
||||
|
||||
fn get_arg(dst: usize, arg: usize) -> RegInstr {
|
||||
RegInstr::new(OpCode::GetArg, ImmOperand::Arg(arg as u16), vec![dst], vec![])
|
||||
}
|
||||
|
||||
fn get_loc(dst: usize, loc: usize) -> RegInstr {
|
||||
RegInstr::new(OpCode::GetLoc, ImmOperand::Loc(loc as u16), vec![dst], vec![])
|
||||
}
|
||||
|
||||
fn object(dst: usize) -> RegInstr {
|
||||
RegInstr::new(OpCode::Object, ImmOperand::None, vec![dst], vec![])
|
||||
}
|
||||
|
||||
fn get_field(field: ImmOperand, dst: usize, obj: usize) -> RegInstr {
|
||||
RegInstr::new(OpCode::GetField, field, vec![dst], vec![obj])
|
||||
}
|
||||
|
||||
fn put_field(field: ImmOperand, value: usize, obj: usize) -> RegInstr {
|
||||
RegInstr::new(OpCode::PutField, field, vec![], vec![value, obj])
|
||||
}
|
||||
|
||||
fn define_field(field: ImmOperand, dst: usize, value: usize, obj: usize) -> RegInstr {
|
||||
RegInstr::new(OpCode::DefineField, field, vec![dst], vec![value, obj])
|
||||
}
|
||||
|
||||
/// A plain integer field name, as an operand.
|
||||
fn int_field(n: u32) -> ImmOperand {
|
||||
ImmOperand::Atom(AtomRef::TaggedInt(n))
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::parse::{
|
||||
Atom, AtomRef, ClosureKind, Function, Instr, JsString, JsValue, QuickJSBinary, atom_names,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CaptureFrom {
|
||||
/// `parent.var_defs[i]`
|
||||
ParentArg(usize),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod declass;
|
||||
mod declosure;
|
||||
mod flatten;
|
||||
mod parse;
|
||||
mod regify;
|
||||
@@ -6,6 +7,7 @@ mod regify;
|
||||
use std::process::Command;
|
||||
|
||||
use declass::declass;
|
||||
use declosure::declosure;
|
||||
use flatten::flatten;
|
||||
use parse::QuickJSBinary;
|
||||
use regify::regify;
|
||||
@@ -41,6 +43,10 @@ fn main() {
|
||||
let declassed = declass(reg_fns, &atoms);
|
||||
println!("{declassed:#?}");
|
||||
|
||||
println!("=== after declosure ===");
|
||||
let declosured = declosure(declassed);
|
||||
println!("{declosured:#?}");
|
||||
|
||||
println!("{atoms:#?}");
|
||||
|
||||
/*println!("=== after deprototype ===");
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
flatten::{Capture, FlatFn},
|
||||
parse::{AtomRef, ImmOperand, Instr, JsValue, opcodes::OpCode},
|
||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||
};
|
||||
|
||||
pub struct RegInstr {
|
||||
@@ -51,7 +49,7 @@ impl RegFn {
|
||||
fn from_flat(flat_fn: FlatFn) -> Self {
|
||||
let mut stack = vec![];
|
||||
let mut output = Vec::with_capacity(flat_fn.code.len());
|
||||
let mut next_val = flat_fn.var_count;
|
||||
let mut next_val = flat_fn.var_count; // Reserve values for locals
|
||||
|
||||
let mut fresh_val = || {
|
||||
let v = next_val;
|
||||
|
||||
+16
-3
@@ -175,16 +175,29 @@ Swap, CallMethod, PutLoc1, Drop) still occur elsewhere for ordinary reasons.
|
||||
Only the specific class-pattern instances listed above are removed.
|
||||
|
||||
|
||||
====================================================================
|
||||
UNSUPPORTED (the pass panics)
|
||||
====================================================================
|
||||
|
||||
DefineClass AtomU8 { arg: 1 } derived class (extends)
|
||||
|
||||
Prototype form would have to flatten the parent's prototype into the
|
||||
child's and lower `super`. Neither is implemented, so declass panics
|
||||
rather than emit a class that has silently lost its parent.
|
||||
|
||||
|
||||
====================================================================
|
||||
NOT TOUCHED (passed through unchanged)
|
||||
====================================================================
|
||||
|
||||
DefineClass AtomU8 { arg: 1 } derived class
|
||||
DefineMethod AtomU8 { arg: 1 } getter
|
||||
DefineMethod AtomU8 { arg: 2 } setter
|
||||
DefineMethodComputed ... computed method name
|
||||
DefineClassComputed ... computed class name
|
||||
|
||||
Known gap: a getter or setter (DefineMethod with a non-zero kind) is
|
||||
NOT yet passed through. It is wrongly lowered as an ordinary method,
|
||||
to a put_field, giving a data property instead of an accessor. It
|
||||
should instead make the whole class pass through untouched.
|
||||
|
||||
|
||||
====================================================================
|
||||
LEFT BEHIND (removed by a later pass)
|
||||
|
||||
@@ -120,8 +120,12 @@ last argument, so it is args.last().
|
||||
|
||||
DETECT, abstractly:
|
||||
|
||||
plain call call | call0..3 (callee = last arg; not call_method,
|
||||
call_constructor, or apply)
|
||||
plain call call | call0..3 | tail_call (callee = last arg; not
|
||||
call_method, call_constructor,
|
||||
or apply)
|
||||
|
||||
A tail call stays a tail call. It pushes nothing and returns, so turning it
|
||||
into an ordinary call would leave a value behind and drop the return.
|
||||
|
||||
outer, which in the example is:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user