Compare commits

..
2 Commits
Author SHA1 Message Date
uditdey 457e63ad8e Modifications 2026-09-07 11:44:42 +00:00
uditdey 617dbeb988 First declosure impl 2026-09-04 13:53:59 +00:00
9 changed files with 576 additions and 177 deletions
+13 -14
View File
@@ -9,23 +9,22 @@
mod step_1;
mod step_2;
use crate::{parse::Atom, regify::RegFn};
use crate::{declass::step_1::ClassSequence, regify::RegFn};
/// A class's constructor and its hoisted field initialiser, as fn ids into the
/// flat function list. Read by step 1, consumed by step 2.
#[derive(Clone, Copy)]
struct FieldInit {
ctor: usize,
fields: usize,
}
use step_1::{find_class_seqs, rewrite_class_to_proto};
use step_2::splice_ctor;
pub fn declass(reg_fns: Vec<RegFn>, _atoms: &[Atom]) -> Vec<RegFn> {
// Read the constructor <-> field-initialiser links before step 1 rewrites
// the class builds that carry them.
let field_inits = step_1::field_init_links(&reg_fns);
pub fn declass(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
let class_seqs = reg_fns
.iter()
.map(find_class_seqs)
.collect::<Vec<Vec<ClassSequence>>>();
let mut reg_fns: Vec<RegFn> = reg_fns.into_iter().map(step_1::rewrite).collect();
reg_fns
.iter_mut()
.zip(&class_seqs)
.for_each(|(f, s)| rewrite_class_to_proto(f, s));
step_2::splice(&mut reg_fns, &field_inits);
splice_ctor(&mut reg_fns, &class_seqs);
reg_fns
}
+126 -118
View File
@@ -1,16 +1,3 @@
//! Step 1 of declass: rewrite how a class is built, in prototype form.
//!
//! A class-build cluster (declass_algo.txt, step 1):
//!
//! push_const8 #ctor ; define_class @Name
//! ( fclosure8 #m ; define_method @meth )*
//! ( fclosure8 #fields ; set_home_object ; put_loc ; drop )?
//!
//! becomes `fclosure8 #ctor` followed, per method, by
//! `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.
use std::range::Range;
use crate::{
@@ -18,81 +5,124 @@ use crate::{
regify::{RegFn, RegInstr},
};
use super::FieldInit;
pub fn find_class_seqs(reg_fn: &RegFn) -> Vec<ClassSequence> {
let mut ins = reg_fn.code.as_slice();
let mut seqs = vec![];
let mut offset = 0;
/// Read each constructor <-> field-initialiser link off the class builds, as
/// fn ids. Run before `rewrite`, which drops the instructions carrying them.
pub(super) fn field_init_links(fns: &[RegFn]) -> Vec<FieldInit> {
let mut links = vec![];
for f in fns {
for seq in find_class_seqs(&f.code) {
if let Some(link) = seq.field_init(&f.code, &f.cpool) {
links.push(link);
while !ins.is_empty() {
let seq = match_class_seq(ins); // Try to match a class decl sequence
match seq {
// Found class sequence, store it and advance `ins`
Ok((rest, num_methods)) => {
let consumed = ins.len() - rest.len(); // How many instrs were consumed
let seq_instr = &ins[..consumed]; // The class seq's instrs
seqs.push(ClassSequence {
range: Range {
start: offset,
end: offset + consumed,
},
num_methods,
ctor_fn: fn_id(&reg_fn.cpool, as_const(seq_instr[0].imm)),
field_init_fn: find_field_init_fn(seq_instr, num_methods, &reg_fn.cpool),
});
offset += consumed;
ins = rest;
}
}
}
links
}
/// Rewrite every class build in one function to prototype form.
pub(super) fn rewrite(mut reg_fn: RegFn) -> RegFn {
let seqs = find_class_seqs(&reg_fn.code);
// Move the code out of reg_fn so we can consume it while still calling
// reg_fn.fresh_val(); the empty Vec left behind costs nothing.
let code = std::mem::take(&mut reg_fn.code);
let total = code.len();
let mut out = Vec::with_capacity(total);
let mut instrs = code.into_iter();
let mut seqs = seqs.iter();
let mut next_seq = seqs.next();
let mut pos = 0;
while pos < total {
match next_seq {
Some(seq) if seq.range.start == pos => {
let adv = seq.range.end - seq.range.start;
let matched: Vec<RegInstr> = instrs.by_ref().take(adv).collect();
seq.emit_prototype(&mut reg_fn, &mut out, &matched);
pos += adv;
next_seq = seqs.next();
}
_ => {
out.push(instrs.next().unwrap());
pos += 1;
// Did not find sequence, advance `ins` by 1 and try again
Err(rest) => {
let consumed = ins.len() - rest.len(); // Handle partial consumption
let consumed = consumed.max(1);
offset += consumed;
ins = &ins[consumed..];
}
}
}
reg_fn.code = out;
reg_fn
seqs
}
struct ClassSequence {
range: Range<usize>,
pub fn rewrite_class_to_proto(reg_fn: &mut RegFn, class_seqs: &[ClassSequence]) {
// The prototype form is never longer than the class build it replaces, so
// it drops straight over it and every instr outside a class seq stays put.
// The code moves out only so fresh_val() can borrow reg_fn beside it.
let mut code = std::mem::take(&mut reg_fn.code);
let mut buf = vec![]; // Reused across seqs, so it allocates once
for seq in class_seqs {
seq.emit_prototype(&code[seq.range], &mut || reg_fn.fresh_val(), &mut buf);
code[seq.range].swap_with_slice(&mut buf);
}
reg_fn.code = code;
}
pub struct ClassSequence {
pub range: Range<usize>,
pub num_methods: usize,
pub ctor_fn: usize, // Fn id of the class ctor
pub field_init_fn: Option<usize>, // Fn id of the field init, if any
}
/// The field initialiser's fn id, if the class has a field-initialiser tail.
fn find_field_init_fn(
matched: &[RegInstr],
num_methods: usize,
cpool: &[JsValue],
) -> Option<usize> {
let tail = 2 + 2 * num_methods; // The ctor and its methods, before the tail
if matched.len() == tail {
return None; // No field-initialiser tail
}
Some(fn_id(cpool, as_const(matched[tail].imm)))
}
impl ClassSequence {
/// Emit the prototype form, reading operands straight out of `matched` --
/// the class sequence's own instructions.
fn emit_prototype(&self, reg_fn: &mut RegFn, out: &mut Vec<RegInstr>, matched: &[RegInstr]) {
/// Emit the prototype form into the scratchpad `buf`, reading operands
/// straight out of `matched` -- this class sequence's own instructions.
/// `buf` comes back padded with nops to exactly the length of the class
/// build, so the caller can drop it over the original in place.
fn emit_prototype<F>(&self, matched: &[RegInstr], fresh_val: &mut F, buf: &mut Vec<RegInstr>)
where
F: FnMut() -> usize,
{
buf.clear();
// push_const8 #ctor ; define_class -> fclosure8 #ctor (constructor kept)
let (ctor_const, ctor_val) = self.ctor(matched);
out.push(fclosure8(ctor_const, ctor_val));
buf.push(fclosure8(ctor_const, ctor_val));
// each ( fclosure8 #m ; define_method @meth ) -> get_field + put_field
for k in 0..self.num_methods {
let (method_const, method_val, meth) = self.method(matched, k);
out.push(fclosure8(method_const, method_val));
let proto = reg_fn.fresh_val();
out.push(get_field(proto_atom(), proto, ctor_val));
out.push(put_field(ImmOperand::Atom(meth), method_val, proto));
// A class with no methods has no prototype to read
if self.num_methods > 0 {
// The prototype cannot change between the put_fields below, so read
// it once for the whole class instead of once per method
let proto = fresh_val();
buf.push(get_field(proto_atom(), proto, ctor_val));
// each ( fclosure8 #m ; define_method @meth ) -> fclosure8 + put_field
for k in 0..self.num_methods {
let (method_const, method_val, meth) = self.method(matched, k);
buf.push(fclosure8(method_const, method_val));
buf.push(put_field(ImmOperand::Atom(meth), method_val, proto));
}
}
// the fclosure8 #fields ; set_home_object ; put_loc ; drop tail is dropped.
// 2 + 2n emitted against the 2 + 2n matched, less the 4-instr field
// initialiser tail, so the emission always fits. Nop pads the slack; it
// pops and pushes nothing, so it cannot disturb the value flow.
let len = self.range.end - self.range.start;
debug_assert!(buf.len() <= len, "class rewrite outgrew its range");
buf.resize_with(len, nop);
}
/// The constructor's constant and value: push_const8 #ctor ; define_class.
/// The constructor's constant and register ID it was written to
/// push_const8 #ctor ; define_class.
fn ctor(&self, matched: &[RegInstr]) -> (u32, usize) {
(as_const(matched[0].imm), matched[1].dsts[0])
}
@@ -108,58 +138,26 @@ impl ClassSequence {
atom_of(define_method.imm),
)
}
/// The constructor and field-initialiser fn ids, if the class has a
/// field-initialiser tail. `code` is the whole enclosing function, so the
/// range indexes it directly.
fn field_init(&self, code: &[RegInstr], cpool: &[JsValue]) -> Option<FieldInit> {
let base = 2 + 2 * self.num_methods; // constructor + methods, before the tail
if self.range.end - self.range.start == base {
return None; // no field-initialiser tail
}
let ctor_const = as_const(code[self.range.start].imm);
let fields_const = as_const(code[self.range.start + base].imm);
Some(FieldInit {
ctor: fn_id(cpool, ctor_const),
fields: fn_id(cpool, fields_const),
})
}
}
fn find_class_seqs(mut ins: &[RegInstr]) -> Vec<ClassSequence> {
let mut seqs = vec![];
let mut offset = 0;
while !ins.is_empty() {
match match_class_seq(ins) {
Ok((rest, num_methods)) => {
let adv = ins.len() - rest.len();
seqs.push(ClassSequence {
range: Range {
start: offset,
end: offset + adv,
},
num_methods,
});
offset += adv;
ins = rest;
}
Err(_) => {
offset += 1;
ins = &ins[1..];
}
}
}
seqs
}
// ── the grammar, as matchers over the instruction stream ───────────────────
// ─────────────────────────── Class Sequence Grammar ─────────────────────────────────────
// push_const8 #ctor ; define_class @Name <-- Class decl header
// ( fclosure8 #method ; define_method @meth )* <-- Methods
// ( fclosure8 #fields ; set_home_object ; put_loc ; drop )? <-- Field initializer stuff
//
// match fns return Ok(advanced_instrs) or Err(original_instrs)
fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> {
let ins = match_one(ins, |i| i.op == OpCode::PushConst8)?;
if let Some(i) = ins.first() {
let is_derived_class = matches!(i.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1);
if i.op == OpCode::DefineClass && is_derived_class {
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 +165,7 @@ fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]
Ok((ins, num_methods))
}
// TODO: accessors are not handled
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 +176,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
)
};
@@ -188,7 +192,7 @@ fn match_field_init_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> {
Ok(ins)
}
// ── matcher primitives ─────────────────────────────────────────────────────
// ───────────────────────────── Matcher Primitives ───────────────────────────────────────
fn match_one(i: &[RegInstr], f: impl Fn(&RegInstr) -> bool) -> Result<&[RegInstr], &[RegInstr]> {
match i.first() {
@@ -223,6 +227,10 @@ fn match_opt<'a>(
// ── emitted instructions ───────────────────────────────────────────────────
fn nop() -> RegInstr {
RegInstr::new(OpCode::Nop, ImmOperand::None, vec![], vec![])
}
fn fclosure8(const_index: u32, dst: usize) -> RegInstr {
RegInstr::new(
OpCode::Fclosure8,
+26 -17
View File
@@ -10,7 +10,8 @@
//! This step takes the initialiser's field writes -- its body minus the
//! this/home-object prologue and the trailing return -- and splices them into
//! the constructor in place of that call, renumbered onto fresh value ids. The
//! initialiser function is left unreferenced.
//! initialiser is left with an empty body. It cannot be dropped from the
//! function list, because a function id is an index into that list.
//!
//! Deficiency, left for step 3: a class with NO instance fields compiles
//! `<class_fields_init>` to `Undefined` rather than a closure, so there is no
@@ -19,7 +20,8 @@
//! time -- a harmless no-op. Step 3 must strip that block when it drops the
//! `<class_fields_init>` capture, or the `get_var_ref_check` would dangle.
use super::FieldInit;
use super::step_1::ClassSequence;
use crate::{
parse::{ImmOperand, opcodes::OpCode},
regify::{RegFn, RegInstr},
@@ -28,21 +30,27 @@ use crate::{
/// The name quickjs gives the hoisted field initialiser.
const FIELDS_INIT: &str = "<class_fields_init>";
pub(super) fn splice(fns: &mut [RegFn], field_inits: &[FieldInit]) {
for &FieldInit { ctor, fields } in field_inits {
splice_one(fns, ctor, fields);
pub fn splice_ctor(fns: &mut [RegFn], class_seqs: &[Vec<ClassSequence>]) {
// Only a class with instance fields has an initialiser to splice.
for seq in class_seqs.iter().flatten() {
if let Some(field_init_fn) = seq.field_init_fn {
splice_one(fns, seq.ctor_fn, field_init_fn);
}
}
}
fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) {
// Pull the field writes out of the initialiser (a separate function), then
// hand them to the constructor.
let writes = field_writes(&fns[fields]);
let ctor_fn = &mut fns[ctor];
let Some((start, end)) = call_span(ctor_fn) else {
// Find the call block first. If the constructor does not have one there is
// nothing to splice into, and the initialiser has to keep working.
let Some((start, end)) = call_span(&fns[ctor]) else {
return;
};
// Take the field writes out of the initialiser (a separate function), then
// hand them to the constructor.
let writes = take_field_writes(&mut fns[fields]);
let ctor_fn = &mut fns[ctor];
let writes = renumber(writes, ctor_fn);
ctor_fn.code.splice(start..=end, writes);
}
@@ -50,12 +58,13 @@ fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) {
/// The initialiser's field writes: its body minus the this/home-object prologue
/// and the trailing return. They reach `this` through get_loc0, which is `this`
/// in the constructor too, so they carry over unchanged.
fn field_writes(init: &RegFn) -> Vec<RegInstr> {
init.code
.iter()
.filter(|i| !is_prologue(i.op))
.map(|i| RegInstr::new(i.op, i.imm, i.dsts.clone(), i.args.clone()))
.collect()
///
/// This empties the initialiser, which is dead once its writes move into the
/// constructor. The function itself has to stay: a function id is an index into
/// the function list, so removing one would renumber every function after it.
fn take_field_writes(init: &mut RegFn) -> Vec<RegInstr> {
init.code.retain(|i| !is_prologue(i.op));
std::mem::take(&mut init.code)
}
/// Renumber the writes' value ids onto fresh ids in the constructor. Each write
+328
View File
@@ -0,0 +1,328 @@
//! 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::{Capture, CaptureFrom},
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
regify::{RegFn, RegInstr},
};
pub fn declosure(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
// Every function's captures, per fn id. Step 2 runs on a parent but needs
// its CHILD's captures, and `declosure_one` only ever holds one function,
// so the cross-function view has to be gathered here. Taking the lists
// moves them -- no per-function allocation -- and leaves every function
// with the empty capture list declosure ends with anyway.
let captures: Vec<Vec<Capture>> = reg_fns
.iter_mut()
.map(|f| std::mem::take(&mut f.captures))
.collect();
reg_fns
.into_iter()
.map(|f| declosure_one(f, &captures))
.collect()
}
fn declosure_one(reg_fn: RegFn, captures: &[Vec<Capture>]) -> RegFn {
let reg_fn = take_env_param(reg_fn);
let reg_fn = build_closure_objects(reg_fn, captures);
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);
// Nothing reads a capture, so there is nothing to rewrite. The parameter
// is still added, to keep step 3's call sites uniform.
if !code.iter().any(|i| is_capture_access(i).is_some()) {
reg_fn.code = code;
return reg_fn;
}
// Load the env once at entry.
let mut out = Vec::with_capacity(code.len() + 1);
let env = reg_fn.fresh_val();
out.push(get_arg(env, env_arg));
for instr in code {
match is_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
}
/// Which capture an instruction reads or writes, if any.
enum CaptureAccess {
Read(u32),
Write(u32),
}
fn is_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, captures: &[Vec<Capture>]) -> 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, &reg_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 child_captures = &captures[child];
// 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));
// Field 0 is the function, fields 1.. are the captures. Each
// define_field returns the object, so the stores chain -- and the LAST
// must land in `closure`.
for k in 0..=child_captures.len() {
let value = if k == 0 {
func
} else {
let value = reg_fn.fresh_val();
out.push(load_capture(child_captures[k - 1].from, value));
value
};
let dst = if k == child_captures.len() {
closure
} else {
reg_fn.fresh_val()
};
out.push(define_field(int_field(k as u32), 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))
}
+1 -1
View File
@@ -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),
+7 -1
View File
@@ -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;
@@ -38,9 +40,13 @@ fn main() {
println!("{reg_fns:#?}");
println!("=== after declass ===");
let declassed = declass(reg_fns, &atoms);
let declassed = declass(reg_fns);
println!("{declassed:#?}");
println!("=== after declosure ===");
let declosured = declosure(declassed);
println!("{declosured:#?}");
println!("{atoms:#?}");
/*println!("=== after deprototype ===");
+3 -8
View File
@@ -1,12 +1,11 @@
#![allow(non_snake_case)]
use std::fmt::Debug;
use crate::{
flatten::{Capture, FlatFn},
parse::{AtomRef, ImmOperand, Instr, JsValue, opcodes::OpCode},
parse::{ImmOperand, JsValue, opcodes::OpCode},
};
#[derive(Clone)]
pub struct RegInstr {
pub dsts: Vec<usize>,
pub op: OpCode,
@@ -39,8 +38,6 @@ impl Debug for RegInstr {
pub struct RegFn {
pub name: String,
pub arg_count: usize,
pub var_count: usize,
pub value_names: Vec<AtomRef>,
pub captures: Vec<Capture>,
pub cpool: Vec<JsValue>,
pub code: Vec<RegInstr>,
@@ -51,7 +48,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;
@@ -100,8 +97,6 @@ impl RegFn {
Self {
name: flat_fn.name,
arg_count: flat_fn.arg_count,
var_count: flat_fn.var_count,
value_names: flat_fn.value_names,
captures: flat_fn.captures,
cpool: flat_fn.cpool,
code: output,
+66 -16
View File
@@ -36,23 +36,50 @@ which in the example is:
REPLACE it with:
[6] = Fclosure8 Const(1) [],
[8] = Fclosure8 Const(0) [],
[18] = GetField Atom(atom("prototype")) [6],
[8] = Fclosure8 Const(0) [],
[] = PutField Atom(file_atom[5]) [8, 18],
[] = Nop None [],
[] = Nop None [],
[] = Nop None [],
[] = Nop None [],
INSTRUCTION BY INSTRUCTION:
PushConst8 Const(1) + DefineClass [5,4] -> Fclosure8 Const(1), keep dst [6]
Fclosure8 Const(0) -> unchanged (the method closure)
DefineMethod atom=A [8,7] -> GetField "prototype" [6]
then PutField atom=A [8,18]
Fclosure8 Const(2) -> deleted
SetHomeObject -> deleted
PutLoc1 [12] -> deleted
Drop [11] -> deleted
PushConst8 Const(1) ; DefineClass [5,4]
-> Fclosure8 Const(1), which keeps the DefineClass dst [6].
[18] is any fresh value id.
Repeat the DefineMethod line for each method in the class.
GetField "prototype" [6]
-> emitted once for the whole class, into a fresh value id [18].
It is NOT emitted per method: the prototype cannot change
between the PutFields below. A class with no methods does not
emit it at all.
Fclosure8 Const(0)
-> unchanged. It is the method closure.
DefineMethod atom=A [8,7]
-> PutField atom=A [8, 18], reading the [18] from above.
Repeat the Fclosure8/PutField pair for each method.
Fclosure8 Const(2) ; SetHomeObject ; PutLoc1 [12] ; Drop [11]
-> deleted. This is the field-initialiser tail.
Nop
-> padding, described below.
LENGTH, and why the rewrite is written in place:
matched = 2 + 2n instructions, plus 4 more if the field-initialiser
tail is there (n = the number of methods).
emitted = 2 + 2n instructions, or 1 when n is 0.
So the emission is never longer than what it replaces. It is written
straight over the matched range and the slack is padded with Nop,
which pops and pushes nothing. Every instruction outside a class
build keeps its position, so no other range has to be adjusted.
In the example: 8 matched, 4 emitted, 4 Nop.
REMEMBER for step 2 and 3:
@@ -175,19 +202,42 @@ 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)
LEFT BEHIND (an empty function)
====================================================================
FnById(4), the fields function. Nothing references it anymore.
FnById(4), the fields function. Step 1 deleted the fclosure8 that
built it, and step 2 moved its writes into the constructor, so
nothing references it anymore.
Step 2 empties it:
code: [],
It is NOT removed from the function list. A function id is an index
into that list, so removing one would renumber every function after
it and break every FnById that points past it.
+6 -2
View File
@@ -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: