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_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
}
+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 crate::{
@@ -21,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);
}
}
}
links
}
while !ins.is_empty() {
let seq = match_class_seq(ins); // Try to match a class decl sequence
/// 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();
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
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;
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),
});
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();
offset += consumed;
ins = rest;
}
_ => {
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
// 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);
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));
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])
}
@@ -111,64 +138,22 @@ 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)?;
// 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) {
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");
}
}
@@ -180,15 +165,7 @@ 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.
// 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)?;
@@ -215,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() {
@@ -250,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
+51 -40
View File
@@ -47,28 +47,31 @@
//! to the class passes; step 3 leaves them untouched. Same for `apply`.
use crate::{
flatten::CaptureFrom,
flatten::{Capture, 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())
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, &capture_sources))
.map(|f| declosure_one(f, &captures))
.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 = build_closure_objects(reg_fn, capture_sources);
let reg_fn = build_closure_objects(reg_fn, captures);
route_calls_through_object(reg_fn)
}
@@ -81,18 +84,21 @@ fn take_env_param(mut reg_fn: RegFn) -> RegFn {
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));
// 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 capture_access(&instr) {
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))
@@ -105,7 +111,6 @@ fn take_env_param(mut reg_fn: RegFn) -> RegFn {
}
reg_fn.code = out;
reg_fn.captures.clear();
reg_fn
}
@@ -115,7 +120,7 @@ enum CaptureAccess {
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.
let slot = || match i.imm {
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
/// 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 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.
let closure = instr.dsts[0];
let captures: &[CaptureFrom] = capture_sources
.get(child)
.map(Vec::as_slice)
.unwrap_or(&[]);
let child_captures = &captures[child];
// The function is a plain constant, so it loads with `push_const`.
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();
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
// 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 {
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));
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
} else {
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;
}
}
@@ -288,11 +289,21 @@ fn plain_call(i: &RegInstr) -> Option<(OpCode, u16)> {
// ── emitted instructions ───────────────────────────────────────────────────
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 {
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 {
+1 -1
View File
@@ -40,7 +40,7 @@ fn main() {
println!("{reg_fns:#?}");
println!("=== after declass ===");
let declassed = declass(reg_fns, &atoms);
let declassed = declass(reg_fns);
println!("{declassed:#?}");
println!("=== after declosure ===");
+2 -5
View File
@@ -2,9 +2,10 @@ use std::fmt::Debug;
use crate::{
flatten::{Capture, FlatFn},
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
parse::{ImmOperand, JsValue, opcodes::OpCode},
};
#[derive(Clone)]
pub struct RegInstr {
pub dsts: Vec<usize>,
pub op: OpCode,
@@ -37,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>,
@@ -98,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,
+50 -13
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:
@@ -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.