From 457e63ad8e446cd9c81d9906b01c2f1ede2052e6 Mon Sep 17 00:00:00 2001 From: uditdey Date: Mon, 7 Sep 2026 11:44:42 +0000 Subject: [PATCH] Modifications --- compiler/src/declass/mod.rs | 27 ++-- compiler/src/declass/step_1.rs | 247 +++++++++++++++------------------ compiler/src/declass/step_2.rs | 43 +++--- compiler/src/declosure.rs | 95 +++++++------ compiler/src/main.rs | 2 +- compiler/src/regify.rs | 7 +- docs/declass_algo.txt | 63 +++++++-- 7 files changed, 259 insertions(+), 225 deletions(-) diff --git a/compiler/src/declass/mod.rs b/compiler/src/declass/mod.rs index a3a2d1e..77e2c11 100644 --- a/compiler/src/declass/mod.rs +++ b/compiler/src/declass/mod.rs @@ -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, _atoms: &[Atom]) -> Vec { - // Read the constructor <-> field-initialiser links before step 1 rewrites - // the class builds that carry them. - let field_inits = step_1::field_init_links(®_fns); +pub fn declass(mut reg_fns: Vec) -> Vec { + let class_seqs = reg_fns + .iter() + .map(find_class_seqs) + .collect::>>(); - let mut reg_fns: Vec = 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 } diff --git a/compiler/src/declass/step_1.rs b/compiler/src/declass/step_1.rs index d78e63e..b9ee6d1 100644 --- a/compiler/src/declass/step_1.rs +++ b/compiler/src/declass/step_1.rs @@ -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 { + 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 { - 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(®_fn.cpool, as_const(seq_instr[0].imm)), + field_init_fn: find_field_init_fn(seq_instr, num_methods, ®_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(®_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 = 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, +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, + pub num_methods: usize, + pub ctor_fn: usize, // Fn id of the class ctor + pub field_init_fn: Option, // 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 { + 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, 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(&self, matched: &[RegInstr], fresh_val: &mut F, buf: &mut Vec) + 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]) } @@ -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 { - 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 { - 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, diff --git a/compiler/src/declass/step_2.rs b/compiler/src/declass/step_2.rs index 0709fbf..8efc82a 100644 --- a/compiler/src/declass/step_2.rs +++ b/compiler/src/declass/step_2.rs @@ -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 //! `` 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 //! `` 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 = ""; -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]) { + // 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 { - 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 { + 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 diff --git a/compiler/src/declosure.rs b/compiler/src/declosure.rs index 31fceaa..f2a8f2b 100644 --- a/compiler/src/declosure.rs +++ b/compiler/src/declosure.rs @@ -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) -> Vec { - // 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> = reg_fns - .iter() - .map(|f| f.captures.iter().map(|c| c.from).collect()) +pub fn declosure(mut reg_fns: Vec) -> Vec { + // 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> = 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]) -> RegFn { +fn declosure_one(reg_fn: RegFn, captures: &[Vec]) -> 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 { +fn is_capture_access(i: &RegInstr) -> Option { // 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 { /// 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]) -> RegFn { +fn build_closure_objects(mut reg_fn: RegFn, captures: &[Vec]) -> 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] }; // 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] 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; + // 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 + }; - 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() { + 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 { diff --git a/compiler/src/main.rs b/compiler/src/main.rs index 033f634..154ba0a 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -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 ==="); diff --git a/compiler/src/regify.rs b/compiler/src/regify.rs index b4b7e1e..6729b0b 100644 --- a/compiler/src/regify.rs +++ b/compiler/src/regify.rs @@ -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, 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, pub captures: Vec, pub cpool: Vec, pub code: Vec, @@ -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, diff --git a/docs/declass_algo.txt b/docs/declass_algo.txt index 026e099..b477616 100644 --- a/docs/declass_algo.txt +++ b/docs/declass_algo.txt @@ -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.