Modifications

This commit is contained in:
2026-09-07 11:44:42 +00:00
parent 617dbeb988
commit 457e63ad8e
7 changed files with 259 additions and 225 deletions
+13 -14
View File
@@ -9,23 +9,22 @@
mod step_1; mod step_1;
mod step_2; 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 use step_1::{find_class_seqs, rewrite_class_to_proto};
/// flat function list. Read by step 1, consumed by step 2. use step_2::splice_ctor;
#[derive(Clone, Copy)]
struct FieldInit {
ctor: usize,
fields: usize,
}
pub fn declass(reg_fns: Vec<RegFn>, _atoms: &[Atom]) -> Vec<RegFn> { pub fn declass(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
// Read the constructor <-> field-initialiser links before step 1 rewrites let class_seqs = reg_fns
// the class builds that carry them. .iter()
let field_inits = step_1::field_init_links(&reg_fns); .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 reg_fns
} }
+109 -128
View File
@@ -1,19 +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.
//!
//! 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; use std::range::Range;
use crate::{ use crate::{
@@ -21,81 +5,124 @@ use crate::{
regify::{RegFn, RegInstr}, 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 while !ins.is_empty() {
/// fn ids. Run before `rewrite`, which drops the instructions carrying them. let seq = match_class_seq(ins); // Try to match a class decl sequence
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);
}
}
}
links
}
/// Rewrite every class build in one function to prototype form. match seq {
pub(super) fn rewrite(mut reg_fn: RegFn) -> RegFn { // Found class sequence, store it and advance `ins`
let seqs = find_class_seqs(&reg_fn.code); Ok((rest, num_methods)) => {
// Move the code out of reg_fn so we can consume it while still calling let consumed = ins.len() - rest.len(); // How many instrs were consumed
// reg_fn.fresh_val(); the empty Vec left behind costs nothing. let seq_instr = &ins[..consumed]; // The class seq's instrs
let code = std::mem::take(&mut reg_fn.code);
let total = code.len();
let mut out = Vec::with_capacity(total); seqs.push(ClassSequence {
let mut instrs = code.into_iter(); range: Range {
let mut seqs = seqs.iter(); start: offset,
let mut next_seq = seqs.next(); end: offset + consumed,
let mut pos = 0; },
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),
});
while pos < total { offset += consumed;
match next_seq { ins = rest;
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();
} }
_ => { // Did not find sequence, advance `ins` by 1 and try again
out.push(instrs.next().unwrap()); Err(rest) => {
pos += 1; let consumed = ins.len() - rest.len(); // Handle partial consumption
let consumed = consumed.max(1);
offset += consumed;
ins = &ins[consumed..];
} }
} }
} }
reg_fn.code = out; seqs
reg_fn
} }
struct ClassSequence { pub fn rewrite_class_to_proto(reg_fn: &mut RegFn, class_seqs: &[ClassSequence]) {
range: Range<usize>, // 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, 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 { impl ClassSequence {
/// Emit the prototype form, reading operands straight out of `matched` -- /// Emit the prototype form into the scratchpad `buf`, reading operands
/// the class sequence's own instructions. /// straight out of `matched` -- this class sequence's own instructions.
fn emit_prototype(&self, reg_fn: &mut RegFn, out: &mut Vec<RegInstr>, matched: &[RegInstr]) { /// `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) // push_const8 #ctor ; define_class -> fclosure8 #ctor (constructor kept)
let (ctor_const, ctor_val) = self.ctor(matched); 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 // 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 { for k in 0..self.num_methods {
let (method_const, method_val, meth) = self.method(matched, k); let (method_const, method_val, meth) = self.method(matched, k);
out.push(fclosure8(method_const, method_val)); buf.push(fclosure8(method_const, method_val));
let proto = reg_fn.fresh_val(); buf.push(put_field(ImmOperand::Atom(meth), method_val, proto));
out.push(get_field(proto_atom(), proto, ctor_val)); }
out.push(put_field(ImmOperand::Atom(meth), method_val, proto));
} }
// the fclosure8 #fields ; set_home_object ; put_loc ; drop tail is dropped. // 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) { fn ctor(&self, matched: &[RegInstr]) -> (u32, usize) {
(as_const(matched[0].imm), matched[1].dsts[0]) (as_const(matched[0].imm), matched[1].dsts[0])
} }
@@ -111,64 +138,22 @@ impl ClassSequence {
atom_of(define_method.imm), 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> { // ─────────────────────────── Class Sequence Grammar ─────────────────────────────────────
let mut seqs = vec![]; // push_const8 #ctor ; define_class @Name <-- Class decl header
let mut offset = 0; // ( fclosure8 #method ; define_method @meth )* <-- Methods
// ( fclosure8 #fields ; set_home_object ; put_loc ; drop )? <-- Field initializer stuff
while !ins.is_empty() { //
match match_class_seq(ins) { // match fns return Ok(advanced_instrs) or Err(original_instrs)
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 ───────────────────
fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> { fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> {
let ins = match_one(ins, |i| i.op == OpCode::PushConst8)?; 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 let Some(i) = ins.first() {
if i.op == OpCode::DefineClass && is_derived(i) { 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"); panic!("declass: derived classes (extends) are not supported");
} }
} }
@@ -180,15 +165,7 @@ fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]
Ok((ins, num_methods)) Ok((ins, num_methods))
} }
/// A derived class: `define_class` with the derived flag set. // TODO: accessors are not handled
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]> { 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::Fclosure8)?;
let ins = match_one(ins, |i| i.op == OpCode::DefineMethod)?; let ins = match_one(ins, |i| i.op == OpCode::DefineMethod)?;
@@ -215,7 +192,7 @@ fn match_field_init_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> {
Ok(ins) Ok(ins)
} }
// ── matcher primitives ───────────────────────────────────────────────────── // ───────────────────────────── Matcher Primitives ───────────────────────────────────────
fn match_one(i: &[RegInstr], f: impl Fn(&RegInstr) -> bool) -> Result<&[RegInstr], &[RegInstr]> { fn match_one(i: &[RegInstr], f: impl Fn(&RegInstr) -> bool) -> Result<&[RegInstr], &[RegInstr]> {
match i.first() { match i.first() {
@@ -250,6 +227,10 @@ fn match_opt<'a>(
// ── emitted instructions ─────────────────────────────────────────────────── // ── emitted instructions ───────────────────────────────────────────────────
fn nop() -> RegInstr {
RegInstr::new(OpCode::Nop, ImmOperand::None, vec![], vec![])
}
fn fclosure8(const_index: u32, dst: usize) -> RegInstr { fn fclosure8(const_index: u32, dst: usize) -> RegInstr {
RegInstr::new( RegInstr::new(
OpCode::Fclosure8, OpCode::Fclosure8,
+26 -17
View File
@@ -10,7 +10,8 @@
//! This step takes the initialiser's field writes -- its body minus the //! This step takes the initialiser's field writes -- its body minus the
//! this/home-object prologue and the trailing return -- and splices them into //! 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 //! 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 //! 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 //! `<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 //! 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. //! `<class_fields_init>` capture, or the `get_var_ref_check` would dangle.
use super::FieldInit; use super::step_1::ClassSequence;
use crate::{ use crate::{
parse::{ImmOperand, opcodes::OpCode}, parse::{ImmOperand, opcodes::OpCode},
regify::{RegFn, RegInstr}, regify::{RegFn, RegInstr},
@@ -28,21 +30,27 @@ use crate::{
/// The name quickjs gives the hoisted field initialiser. /// The name quickjs gives the hoisted field initialiser.
const FIELDS_INIT: &str = "<class_fields_init>"; const FIELDS_INIT: &str = "<class_fields_init>";
pub(super) fn splice(fns: &mut [RegFn], field_inits: &[FieldInit]) { pub fn splice_ctor(fns: &mut [RegFn], class_seqs: &[Vec<ClassSequence>]) {
for &FieldInit { ctor, fields } in field_inits { // Only a class with instance fields has an initialiser to splice.
splice_one(fns, ctor, fields); 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) { fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) {
// Pull the field writes out of the initialiser (a separate function), then // Find the call block first. If the constructor does not have one there is
// hand them to the constructor. // nothing to splice into, and the initialiser has to keep working.
let writes = field_writes(&fns[fields]); let Some((start, end)) = call_span(&fns[ctor]) else {
let ctor_fn = &mut fns[ctor];
let Some((start, end)) = call_span(ctor_fn) else {
return; 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); let writes = renumber(writes, ctor_fn);
ctor_fn.code.splice(start..=end, writes); 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 /// 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` /// and the trailing return. They reach `this` through get_loc0, which is `this`
/// in the constructor too, so they carry over unchanged. /// in the constructor too, so they carry over unchanged.
fn field_writes(init: &RegFn) -> Vec<RegInstr> { ///
init.code /// This empties the initialiser, which is dead once its writes move into the
.iter() /// constructor. The function itself has to stay: a function id is an index into
.filter(|i| !is_prologue(i.op)) /// the function list, so removing one would renumber every function after it.
.map(|i| RegInstr::new(i.op, i.imm, i.dsts.clone(), i.args.clone())) fn take_field_writes(init: &mut RegFn) -> Vec<RegInstr> {
.collect() 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 /// Renumber the writes' value ids onto fresh ids in the constructor. Each write
+51 -40
View File
@@ -47,28 +47,31 @@
//! to the class passes; step 3 leaves them untouched. Same for `apply`. //! to the class passes; step 3 leaves them untouched. Same for `apply`.
use crate::{ use crate::{
flatten::CaptureFrom, flatten::{Capture, CaptureFrom},
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode}, parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
regify::{RegFn, RegInstr}, regify::{RegFn, RegInstr},
}; };
pub fn declosure(reg_fns: Vec<RegFn>) -> Vec<RegFn> { pub fn declosure(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
// Where each function's captures come from, per fn id. Snapshotted first, // Every function's captures, per fn id. Step 2 runs on a parent but needs
// because step 1 clears the capture lists that step 2 reads. // its CHILD's captures, and `declosure_one` only ever holds one function,
let capture_sources: Vec<Vec<CaptureFrom>> = reg_fns // so the cross-function view has to be gathered here. Taking the lists
.iter() // moves them -- no per-function allocation -- and leaves every function
.map(|f| f.captures.iter().map(|c| c.from).collect()) // 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(); .collect();
reg_fns reg_fns
.into_iter() .into_iter()
.map(|f| declosure_one(f, &capture_sources)) .map(|f| declosure_one(f, &captures))
.collect() .collect()
} }
fn declosure_one(reg_fn: RegFn, capture_sources: &[Vec<CaptureFrom>]) -> RegFn { fn declosure_one(reg_fn: RegFn, captures: &[Vec<Capture>]) -> RegFn {
let reg_fn = take_env_param(reg_fn); let reg_fn = take_env_param(reg_fn);
let reg_fn = build_closure_objects(reg_fn, capture_sources); let reg_fn = build_closure_objects(reg_fn, captures);
route_calls_through_object(reg_fn) route_calls_through_object(reg_fn)
} }
@@ -81,18 +84,21 @@ fn take_env_param(mut reg_fn: RegFn) -> RegFn {
reg_fn.arg_count += 1; reg_fn.arg_count += 1;
let code = std::mem::take(&mut reg_fn.code); 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); // Nothing reads a capture, so there is nothing to rewrite. The parameter
let env = reg_fn.fresh_val(); // is still added, to keep step 3's call sites uniform.
if !code.iter().any(|i| is_capture_access(i).is_some()) {
// Load the env once at entry -- only if a capture is actually accessed. reg_fn.code = code;
if touches_captures { return reg_fn;
out.push(get_arg(env, env_arg));
} }
// 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 { for instr in code {
match capture_access(&instr) { match is_capture_access(&instr) {
// A read keeps its destination, so downstream uses do not change. // A read keeps its destination, so downstream uses do not change.
Some(CaptureAccess::Read(k)) => { Some(CaptureAccess::Read(k)) => {
out.push(get_field(int_field(k + 1), instr.dsts[0], env)) out.push(get_field(int_field(k + 1), instr.dsts[0], env))
@@ -105,7 +111,6 @@ fn take_env_param(mut reg_fn: RegFn) -> RegFn {
} }
reg_fn.code = out; reg_fn.code = out;
reg_fn.captures.clear();
reg_fn reg_fn
} }
@@ -115,7 +120,7 @@ enum CaptureAccess {
Write(u32), Write(u32),
} }
fn capture_access(i: &RegInstr) -> Option<CaptureAccess> { fn is_capture_access(i: &RegInstr) -> Option<CaptureAccess> {
// The numbered forms fold the slot into the opcode; the rest carry it. // The numbered forms fold the slot into the opcode; the rest carry it.
let slot = || match i.imm { let slot = || match i.imm {
ImmOperand::VarRef(k) => k as u32, ImmOperand::VarRef(k) => k as u32,
@@ -142,7 +147,7 @@ fn capture_access(i: &RegInstr) -> Option<CaptureAccess> {
/// Replace each `fclosure` with the object that stands for it: the function in /// 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. /// 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 { fn build_closure_objects(mut reg_fn: RegFn, captures: &[Vec<Capture>]) -> RegFn {
let code = std::mem::take(&mut reg_fn.code); let code = std::mem::take(&mut reg_fn.code);
let mut out = Vec::with_capacity(code.len()); let mut out = Vec::with_capacity(code.len());
@@ -153,10 +158,7 @@ fn build_closure_objects(mut reg_fn: RegFn, capture_sources: &[Vec<CaptureFrom>]
}; };
// The value the rest of the code already uses for this closure. // The value the rest of the code already uses for this closure.
let closure = instr.dsts[0]; let closure = instr.dsts[0];
let captures: &[CaptureFrom] = capture_sources let child_captures = &captures[child];
.get(child)
.map(Vec::as_slice)
.unwrap_or(&[]);
// The function is a plain constant, so it loads with `push_const`. // The function is a plain constant, so it loads with `push_const`.
let load = match instr.op { let load = match instr.op {
@@ -169,26 +171,25 @@ fn build_closure_objects(mut reg_fn: RegFn, capture_sources: &[Vec<CaptureFrom>]
let mut obj = reg_fn.fresh_val(); let mut obj = reg_fn.fresh_val();
out.push(object(obj)); out.push(object(obj));
// Each define_field returns the object, so the stores chain. The LAST // Field 0 is the function, fields 1.. are the captures. Each
// store must land in `closure`. // define_field returns the object, so the stores chain -- and the LAST
let dst = if captures.is_empty() { // must land in `closure`.
closure for k in 0..=child_captures.len() {
let value = if k == 0 {
func
} else { } 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(); let value = reg_fn.fresh_val();
out.push(load_capture(*source, value)); out.push(load_capture(child_captures[k - 1].from, value));
value
};
let dst = if k + 1 == captures.len() { let dst = if k == child_captures.len() {
closure closure
} else { } else {
reg_fn.fresh_val() reg_fn.fresh_val()
}; };
out.push(define_field(int_field(k as u32 + 1), dst, value, obj));
out.push(define_field(int_field(k as u32), dst, value, obj));
obj = dst; obj = dst;
} }
} }
@@ -288,11 +289,21 @@ fn plain_call(i: &RegInstr) -> Option<(OpCode, u16)> {
// ── emitted instructions ─────────────────────────────────────────────────── // ── emitted instructions ───────────────────────────────────────────────────
fn get_arg(dst: usize, arg: usize) -> RegInstr { fn get_arg(dst: usize, arg: usize) -> RegInstr {
RegInstr::new(OpCode::GetArg, ImmOperand::Arg(arg as u16), vec![dst], vec![]) RegInstr::new(
OpCode::GetArg,
ImmOperand::Arg(arg as u16),
vec![dst],
vec![],
)
} }
fn get_loc(dst: usize, loc: usize) -> RegInstr { fn get_loc(dst: usize, loc: usize) -> RegInstr {
RegInstr::new(OpCode::GetLoc, ImmOperand::Loc(loc as u16), vec![dst], vec![]) RegInstr::new(
OpCode::GetLoc,
ImmOperand::Loc(loc as u16),
vec![dst],
vec![],
)
} }
fn object(dst: usize) -> RegInstr { fn object(dst: usize) -> RegInstr {
+1 -1
View File
@@ -40,7 +40,7 @@ fn main() {
println!("{reg_fns:#?}"); println!("{reg_fns:#?}");
println!("=== after declass ==="); println!("=== after declass ===");
let declassed = declass(reg_fns, &atoms); let declassed = declass(reg_fns);
println!("{declassed:#?}"); println!("{declassed:#?}");
println!("=== after declosure ==="); println!("=== after declosure ===");
+2 -5
View File
@@ -2,9 +2,10 @@ use std::fmt::Debug;
use crate::{ use crate::{
flatten::{Capture, FlatFn}, flatten::{Capture, FlatFn},
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode}, parse::{ImmOperand, JsValue, opcodes::OpCode},
}; };
#[derive(Clone)]
pub struct RegInstr { pub struct RegInstr {
pub dsts: Vec<usize>, pub dsts: Vec<usize>,
pub op: OpCode, pub op: OpCode,
@@ -37,8 +38,6 @@ impl Debug for RegInstr {
pub struct RegFn { pub struct RegFn {
pub name: String, pub name: String,
pub arg_count: usize, pub arg_count: usize,
pub var_count: usize,
pub value_names: Vec<AtomRef>,
pub captures: Vec<Capture>, pub captures: Vec<Capture>,
pub cpool: Vec<JsValue>, pub cpool: Vec<JsValue>,
pub code: Vec<RegInstr>, pub code: Vec<RegInstr>,
@@ -98,8 +97,6 @@ impl RegFn {
Self { Self {
name: flat_fn.name, name: flat_fn.name,
arg_count: flat_fn.arg_count, arg_count: flat_fn.arg_count,
var_count: flat_fn.var_count,
value_names: flat_fn.value_names,
captures: flat_fn.captures, captures: flat_fn.captures,
cpool: flat_fn.cpool, cpool: flat_fn.cpool,
code: output, code: output,
+50 -13
View File
@@ -36,23 +36,50 @@ which in the example is:
REPLACE it with: REPLACE it with:
[6] = Fclosure8 Const(1) [], [6] = Fclosure8 Const(1) [],
[8] = Fclosure8 Const(0) [],
[18] = GetField Atom(atom("prototype")) [6], [18] = GetField Atom(atom("prototype")) [6],
[8] = Fclosure8 Const(0) [],
[] = PutField Atom(file_atom[5]) [8, 18], [] = PutField Atom(file_atom[5]) [8, 18],
[] = Nop None [],
[] = Nop None [],
[] = Nop None [],
[] = Nop None [],
INSTRUCTION BY INSTRUCTION: INSTRUCTION BY INSTRUCTION:
PushConst8 Const(1) + DefineClass [5,4] -> Fclosure8 Const(1), keep dst [6] PushConst8 Const(1) ; DefineClass [5,4]
Fclosure8 Const(0) -> unchanged (the method closure) -> Fclosure8 Const(1), which keeps the DefineClass dst [6].
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
[18] is any fresh value id. GetField "prototype" [6]
Repeat the DefineMethod line for each method in the class. -> 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: REMEMBER for step 2 and 3:
@@ -200,7 +227,17 @@ NOT TOUCHED (passed through unchanged)
==================================================================== ====================================================================
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.