From be53d3a5e5e4ff7aa2962969ee8b2b7c3020d970 Mon Sep 17 00:00:00 2001 From: uditdey Date: Mon, 7 Sep 2026 13:13:03 +0000 Subject: [PATCH] Initial demutate impl --- compiler/src/declass/mod.rs | 4 +- compiler/src/declass/step_2.rs | 2 +- compiler/src/demutate.rs | 328 +++++++++++++++++++++++++++++++++ compiler/src/main.rs | 6 + 4 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 compiler/src/demutate.rs diff --git a/compiler/src/declass/mod.rs b/compiler/src/declass/mod.rs index 77e2c11..675e5ec 100644 --- a/compiler/src/declass/mod.rs +++ b/compiler/src/declass/mod.rs @@ -12,7 +12,7 @@ mod step_2; use crate::{declass::step_1::ClassSequence, regify::RegFn}; use step_1::{find_class_seqs, rewrite_class_to_proto}; -use step_2::splice_ctor; +use step_2::splice_ctors; pub fn declass(mut reg_fns: Vec) -> Vec { let class_seqs = reg_fns @@ -25,6 +25,6 @@ pub fn declass(mut reg_fns: Vec) -> Vec { .zip(&class_seqs) .for_each(|(f, s)| rewrite_class_to_proto(f, s)); - splice_ctor(&mut reg_fns, &class_seqs); + splice_ctors(&mut reg_fns, &class_seqs); reg_fns } diff --git a/compiler/src/declass/step_2.rs b/compiler/src/declass/step_2.rs index 8efc82a..2245cfc 100644 --- a/compiler/src/declass/step_2.rs +++ b/compiler/src/declass/step_2.rs @@ -30,7 +30,7 @@ use crate::{ /// The name quickjs gives the hoisted field initialiser. const FIELDS_INIT: &str = ""; -pub fn splice_ctor(fns: &mut [RegFn], class_seqs: &[Vec]) { +pub fn splice_ctors(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 { diff --git a/compiler/src/demutate.rs b/compiler/src/demutate.rs new file mode 100644 index 0000000..2d522ba --- /dev/null +++ b/compiler/src/demutate.rs @@ -0,0 +1,328 @@ +//! demutate: object builds and in-place mutations become calls to a function +//! that returns a fresh object (demutate_algo.txt). +//! +//! This file is the detector. It finds the runs and works out what each one +//! reads from outside itself; lifting a run into its own function, and putting +//! the call in its place, is the next step. +//! +//! A run is a maximal contiguous stretch of static-key field writes to ONE +//! object, together with the value computations those writes consume: +//! +//! creation object ; ( ; define_field @F )+ +//! mutation ( get_loc L ; ; put_field @F )+ (same L) + +use std::range::Range; + +use crate::{ + parse::{ImmOperand, opcodes::OpCode}, + regify::{RegFn, RegInstr}, +}; + +pub fn find_object_runs(reg_fn: &RegFn) -> Vec { + let barriers = find_barriers(®_fn.code); + + collect_runs(®_fn.code, &barriers, 0, Look::Anything) +} + +/// What to match at this level. Inside a run only nested BUILDS count: a +/// mutation writes its object over and over, so matching anything would find +/// the run's own second write as a run of its own. +#[derive(Clone, Copy, PartialEq)] +enum Look { + Anything, + BuildsOnly, +} + +/// The runs in `ins`, each holding whatever runs are nested inside it. +fn collect_runs( + mut ins: &[RegInstr], + mut barriers: &[bool], + mut offset: usize, + look: Look, +) -> Vec { + let mut found = vec![]; + + while !ins.is_empty() { + let run = match_run(ins, barriers, look); // Try to match a run + + match run { + // Found a run, store it and advance `ins` + Ok((rest, kind)) => { + let consumed = ins.len() - rest.len(); // How many instrs were consumed + let run_instr = &ins[..consumed]; // The run's instrs + + let range = Range { + start: offset, + end: offset + consumed, + }; + + // Walk the run's own instructions to find the builds nested in + // it. Skip instr 0: it starts this run, and would match it a + // second time. + let nested = collect_runs( + &run_instr[1..], + &barriers[1..consumed], + offset + 1, + Look::BuildsOnly, + ); + + found.push(ObjectRun { + range, + kind, + params: find_params(run_instr, offset, nested), + }); + + offset += consumed; + ins = rest; + barriers = &barriers[consumed..]; + } + + // Did not find a run, advance past what was tried and go again + Err(rest) => { + let consumed = ins.len() - rest.len(); // Handle partial consumption + let consumed = consumed.max(1); + offset += consumed; + ins = &ins[consumed..]; + barriers = &barriers[consumed..]; + } + } + } + + found +} + +#[derive(Debug)] +pub struct ObjectRun { + pub range: Range, + pub kind: RunKind, + /// What the run cannot make for itself, in first-use order. Each one + /// becomes a parameter of the lifted function. + pub params: Vec, +} + +/// A value the lifted function is handed rather than making. +#[derive(Debug)] +pub enum Param { + /// Read out of the enclosing frame. The load stays at the call site. + Frame(Source), + /// A build nested inside this run. It is a run in its own right, and it + /// lives here because being a parameter is the whole of its part in the + /// outer run: lift it first, then pass its call result in. + Run(ObjectRun), +} + +/// Where the run's object comes from. It is the only difference between the +/// two forms: a creation makes the object, a mutation is handed one. +#[derive(Debug)] +pub enum RunKind { + /// The run makes the object itself, with `object`. + Create, + /// The run loads the object from the frame before each write. It is + /// parameter 0 of the lifted function, and the result is stored back here. + Mutate(Source), +} + +/// A frame slot a run reads. Two loads of one slot are the same object, whatever +/// value ids regify gave them, so sameness is judged here and not on the value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Source { + Arg(u16), + Loc(u16), + VarRef(u16), +} + +/// Where a run may not reach across: a jump, and anything a jump lands on. A +/// run is one straight line of code, so that both ends of a branch are cut off. +fn find_barriers(code: &[RegInstr]) -> Vec { + let mut barriers = vec![false; code.len()]; + + for (at, i) in code.iter().enumerate() { + if let ImmOperand::Label { target } = i.imm { + barriers[at] = true; + barriers[target as usize] = true; + } + } + + barriers +} + +/// What the run has to be handed, in first-use order. A frame slot appears +/// once however many times it is loaded, which folds a mutation's repeated +/// loads of its object into the one parameter. +fn find_params(run: &[RegInstr], offset: usize, nested: Vec) -> Vec { + let mut params = vec![]; + let mut nested = nested.into_iter().peekable(); + let mut at = 0; + + while at < run.len() { + // A nested build is its own run, so step over it whole and take the run + // itself instead of the instructions that make it. They come in + // instruction order, so the next one is always the one to look at. + if nested.peek().is_some_and(|n| n.range.start == offset + at) { + let child = nested.next().unwrap(); + at += child.range.end - child.range.start; + params.push(Param::Run(child)); + continue; + } + + if let Some(src) = frame_load(&run[at]) { + if !params.iter().any(|p| matches!(p, Param::Frame(s) if *s == src)) { + params.push(Param::Frame(src)); + } + } + + at += 1; + } + + params +} + +// ─────────────────────────────── Run Grammar ──────────────────────────────── +// creation object ; ( ; define_field @F )+ +// mutation ( get_loc L ; ; put_field @F )+ (same L) +// +// is any expression, so a run cannot be matched opcode by opcode the +// way declass matches a class build. The object is FOLLOWED instead: skip to +// the next instruction that uses it, and carry on only while that use is a +// static-key write. Whatever was passed over on the way is the value +// computation, and it belongs to the run. +// +// match fns return Ok(advanced_instrs) or Err(original_instrs) + +fn match_run<'a>( + ins: &'a [RegInstr], + barriers: &[bool], + look: Look, +) -> Result<(&'a [RegInstr], RunKind), &'a [RegInstr]> { + match match_creation_run(ins, barriers) { + Ok(rest) => Ok((rest, RunKind::Create)), + Err(_) if look == Look::BuildsOnly => Err(ins), + Err(_) => match_mutation_run(ins, barriers), + } +} + +fn match_creation_run<'a>( + ins: &'a [RegInstr], + barriers: &[bool], +) -> Result<&'a [RegInstr], &'a [RegInstr]> { + let Some(first) = ins.first() else { + return Err(ins); + }; + + if first.op != OpCode::Object { + return Err(ins); + } + + // define_field hands the object back as a NEW value each time, so the run + // holds together as a chain: follow it from one write to the next + let mut obj = first.dsts[0]; + let mut end = 1; + let mut writes = 0; + + while let Some(at) = next_use(&ins[end..], &barriers[end..], obj) { + let Some(next_obj) = chained_write(&ins[end + at], obj) else { + break; // The object is read for something other than a field write + }; + + obj = next_obj; + end += at + 1; + writes += 1; + } + + if writes == 0 { + return Err(ins); // A bare `object` with no writes is not a run + } + + Ok(&ins[end..]) +} + +fn match_mutation_run<'a>( + ins: &'a [RegInstr], + barriers: &[bool], +) -> Result<(&'a [RegInstr], RunKind), &'a [RegInstr]> { + let Some(loc) = ins.first().and_then(frame_load) else { + return Err(ins); + }; + + let mut end = 0; + let mut writes = 0; + + // put_field hands nothing back, so each write re-loads the object. The run + // carries on while the load is of the SAME slot + while ins.get(end).and_then(frame_load) == Some(loc) { + if end > 0 && barriers[end] { + break; // The next write is on the far side of a branch + } + + let obj = ins[end].dsts[0]; + + let Some(at) = next_use(&ins[end + 1..], &barriers[end + 1..], obj) else { + break; // The load goes nowhere + }; + + if !is_write(&ins[end + 1 + at], OpCode::PutField, obj) { + break; // The object is read for something other than a field write + } + + end += at + 2; + writes += 1; + } + + if writes == 0 { + return Err(ins); + } + + Ok((&ins[end..], RunKind::Mutate(loc))) +} + +// ───────────────────────────── Matcher Primitives ─────────────────────────── + +/// Where `v` is next read. Everything before it is the value computation, so +/// it all joins the run -- which is why a barrier in between ends the search. +fn next_use(ins: &[RegInstr], barriers: &[bool], v: usize) -> Option { + let at = ins.iter().position(|i| i.args.contains(&v))?; + barriers[..=at].iter().all(|b| !b).then_some(at) +} + +/// The object as `define_field` hands it back, if this is a static-key write +/// to `obj`. +fn chained_write(i: &RegInstr, obj: usize) -> Option { + is_write(i, OpCode::DefineField, obj).then(|| i.dsts[0]) +} + +/// A static-key field write to `obj`: `op @F [value, obj]`. A computed key is +/// `define_array_el` / `put_array_el` instead, and does not match. +fn is_write(i: &RegInstr, op: OpCode, obj: usize) -> bool { + i.op == op && matches!(i.imm, ImmOperand::Atom(_)) && i.args.len() == 2 && i.args[1] == obj +} + +/// The frame slot an instruction loads. Only plain one-value loads count; +/// `get_loc0_loc1` pushes two and is left to cut the run. +fn frame_load(i: &RegInstr) -> Option { + let slot = |imm| match imm { + ImmOperand::Loc(k) | ImmOperand::Arg(k) | ImmOperand::VarRef(k) => k, + _ => unreachable!("a frame load without a slot operand"), + }; + + match i.op { + OpCode::GetArg0 => Some(Source::Arg(0)), + OpCode::GetArg1 => Some(Source::Arg(1)), + OpCode::GetArg2 => Some(Source::Arg(2)), + OpCode::GetArg3 => Some(Source::Arg(3)), + OpCode::GetArg => Some(Source::Arg(slot(i.imm))), + + OpCode::GetLoc0 => Some(Source::Loc(0)), + OpCode::GetLoc1 => Some(Source::Loc(1)), + OpCode::GetLoc2 => Some(Source::Loc(2)), + OpCode::GetLoc3 => Some(Source::Loc(3)), + OpCode::GetLoc | OpCode::GetLoc8 | OpCode::GetLocCheck => Some(Source::Loc(slot(i.imm))), + + OpCode::GetVarRef0 => Some(Source::VarRef(0)), + OpCode::GetVarRef1 => Some(Source::VarRef(1)), + OpCode::GetVarRef2 => Some(Source::VarRef(2)), + OpCode::GetVarRef3 => Some(Source::VarRef(3)), + OpCode::GetVarRef | OpCode::GetVarRefCheck => Some(Source::VarRef(slot(i.imm))), + + _ => None, + } +} diff --git a/compiler/src/main.rs b/compiler/src/main.rs index 154ba0a..5202d18 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -1,5 +1,6 @@ mod declass; mod declosure; +mod demutate; mod flatten; mod parse; mod regify; @@ -43,6 +44,11 @@ fn main() { let declassed = declass(reg_fns); println!("{declassed:#?}"); + println!("=== demutate runs ==="); + for f in &declassed { + println!("{}: {:#?}", f.name, demutate::find_object_runs(f)); + } + println!("=== after declosure ==="); let declosured = declosure(declassed); println!("{declosured:#?}");