Initial demutate impl

This commit is contained in:
2026-09-07 13:13:03 +00:00
parent 457e63ad8e
commit be53d3a5e5
4 changed files with 337 additions and 3 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ mod step_2;
use crate::{declass::step_1::ClassSequence, regify::RegFn}; use crate::{declass::step_1::ClassSequence, regify::RegFn};
use step_1::{find_class_seqs, rewrite_class_to_proto}; 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<RegFn>) -> Vec<RegFn> { pub fn declass(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
let class_seqs = reg_fns let class_seqs = reg_fns
@@ -25,6 +25,6 @@ pub fn declass(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
.zip(&class_seqs) .zip(&class_seqs)
.for_each(|(f, s)| rewrite_class_to_proto(f, s)); .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 reg_fns
} }
+1 -1
View File
@@ -30,7 +30,7 @@ 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 fn splice_ctor(fns: &mut [RegFn], class_seqs: &[Vec<ClassSequence>]) { pub fn splice_ctors(fns: &mut [RegFn], class_seqs: &[Vec<ClassSequence>]) {
// Only a class with instance fields has an initialiser to splice. // Only a class with instance fields has an initialiser to splice.
for seq in class_seqs.iter().flatten() { for seq in class_seqs.iter().flatten() {
if let Some(field_init_fn) = seq.field_init_fn { if let Some(field_init_fn) = seq.field_init_fn {
+328
View File
@@ -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 ; ( <value> ; define_field @F )+
//! mutation ( get_loc L ; <value> ; 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<ObjectRun> {
let barriers = find_barriers(&reg_fn.code);
collect_runs(&reg_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<ObjectRun> {
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<usize>,
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<Param>,
}
/// 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<bool> {
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<ObjectRun>) -> Vec<Param> {
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 ; ( <value> ; define_field @F )+
// mutation ( get_loc L ; <value> ; put_field @F )+ (same L)
//
// <value> 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<usize> {
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<usize> {
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<Source> {
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,
}
}
+6
View File
@@ -1,5 +1,6 @@
mod declass; mod declass;
mod declosure; mod declosure;
mod demutate;
mod flatten; mod flatten;
mod parse; mod parse;
mod regify; mod regify;
@@ -43,6 +44,11 @@ fn main() {
let declassed = declass(reg_fns); let declassed = declass(reg_fns);
println!("{declassed:#?}"); println!("{declassed:#?}");
println!("=== demutate runs ===");
for f in &declassed {
println!("{}: {:#?}", f.name, demutate::find_object_runs(f));
}
println!("=== after declosure ==="); println!("=== after declosure ===");
let declosured = declosure(declassed); let declosured = declosure(declassed);
println!("{declosured:#?}"); println!("{declosured:#?}");