|
|
|
@@ -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))
|
|
|
|
|
}
|