//! 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, } }