Initial type inference impl
This commit is contained in:
+158
-255
@@ -1,303 +1,191 @@
|
||||
//! demutate: object builds and in-place mutations become calls to a function
|
||||
//! that returns a fresh object (demutate_algo.txt).
|
||||
//! demutate: every static-key field write becomes a call to a function that
|
||||
//! RETURNS a fresh object.
|
||||
//!
|
||||
//! 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.
|
||||
//! There is no grouping and no analysis of what surrounds a write. Each one
|
||||
//! stands alone, because its two arguments are the call's two arguments and
|
||||
//! the enclosing code already holds both:
|
||||
//!
|
||||
//! A run is a maximal contiguous stretch of static-key field writes to ONE
|
||||
//! object, together with the value computations those writes consume:
|
||||
//! define_field @F [v, o] -> [n] n = <define F>(o, v)
|
||||
//! put_field @F [v, o] n = <put F>(o, v) ; store n back
|
||||
//!
|
||||
//! creation object ; ( <value> ; define_field @F )+
|
||||
//! mutation ( get_loc L ; <value> ; put_field @F )+ (same L)
|
||||
|
||||
use std::range::Range;
|
||||
//! `object` is left as it is. It already makes a fresh object, so it needs no
|
||||
//! function; infer reads it as `Type::Empty`, the base every shape builds on.
|
||||
//!
|
||||
//! The lifted functions have no body. `CodeKind::DefineField` / `PutField`
|
||||
//! name the operation and the field, and infer gives each a type rule, so ONE
|
||||
//! function serves every write of that field in the program. Their ids run on
|
||||
//! after the real functions.
|
||||
|
||||
use crate::{
|
||||
parse::{ImmOperand, opcodes::OpCode},
|
||||
infer::{CodeKind, InferFn, Type},
|
||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||
regify::{RegFn, RegInstr},
|
||||
};
|
||||
|
||||
pub fn find_object_runs(reg_fn: &RegFn) -> Vec<ObjectRun> {
|
||||
let barriers = find_barriers(®_fn.code);
|
||||
pub fn demutate(reg_fns: Vec<RegFn>) -> Vec<InferFn> {
|
||||
// The lifted functions take the ids after the real ones, so a write can
|
||||
// name the one it needs before the list of them is finished
|
||||
let first_lifted = reg_fns.len();
|
||||
let mut lifted = vec![];
|
||||
|
||||
collect_runs(®_fn.code, &barriers, 0, Look::Anything)
|
||||
let mut fns = reg_fns
|
||||
.into_iter()
|
||||
.map(|f| rewrite_fn(f, first_lifted, &mut lifted))
|
||||
.collect::<Vec<InferFn>>();
|
||||
|
||||
fns.extend(lifted.into_iter().map(|code| InferFn {
|
||||
name: lifted_name(&code),
|
||||
cpool: vec![],
|
||||
code,
|
||||
sigs: vec![],
|
||||
types: vec![],
|
||||
}));
|
||||
|
||||
fns
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
/// One scan of the code. Everything that is not a static-key write is moved
|
||||
/// across untouched; a write becomes a call, and a `put_field` a call and a
|
||||
/// store.
|
||||
fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec<CodeKind>) -> InferFn {
|
||||
let code = std::mem::take(&mut reg_fn.code);
|
||||
let mut new = Vec::with_capacity(code.len());
|
||||
|
||||
/// 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![];
|
||||
for instr in code {
|
||||
let Some(field) = static_key(&instr) else {
|
||||
new.push(instr); // Not a field write, so it passes through
|
||||
continue;
|
||||
};
|
||||
|
||||
while !ins.is_empty() {
|
||||
let run = match_run(ins, barriers, look); // Try to match a run
|
||||
let define = instr.op == OpCode::DefineField;
|
||||
|
||||
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
|
||||
// Where the object lives says where its replacement goes. The
|
||||
// instruction that made it is already in `new`.
|
||||
let object = instr.args[1];
|
||||
let slot = (!define).then(|| frame_slot(&new, object)).flatten();
|
||||
|
||||
let range = Range {
|
||||
start: offset,
|
||||
end: offset + consumed,
|
||||
};
|
||||
let callee =
|
||||
intern_const(&mut reg_fn.cpool, first_lifted + intern_fn(lifted, instr.op, field));
|
||||
|
||||
// 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,
|
||||
);
|
||||
let dst = match slot {
|
||||
// define_field hands the object back, so the call keeps its dst
|
||||
_ if define => instr.dsts[0],
|
||||
|
||||
found.push(ObjectRun {
|
||||
range,
|
||||
kind,
|
||||
params: find_params(run_instr, offset, nested),
|
||||
});
|
||||
// regify does not rename a local, so the local IS the object's
|
||||
// value: defining it again is the store, and costs no instruction
|
||||
Some(Source::Loc(_)) => object,
|
||||
|
||||
offset += consumed;
|
||||
ins = rest;
|
||||
barriers = &barriers[consumed..];
|
||||
}
|
||||
// Anything else needs a value of its own to be written back from,
|
||||
// and with no slot at all the write is lost -- `f().x = 1` does that
|
||||
_ => reg_fn.fresh_val(),
|
||||
};
|
||||
|
||||
// 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..];
|
||||
}
|
||||
new.push(call(callee, dst, instr.args));
|
||||
|
||||
if let Some(slot) = slot.filter(|s| !matches!(s, Source::Loc(_))) {
|
||||
new.push(store(slot, dst));
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
InferFn {
|
||||
types: vec![Type::Unknown; reg_fn.num_vals()],
|
||||
name: reg_fn.name,
|
||||
cpool: reg_fn.cpool,
|
||||
code: CodeKind::Code(new),
|
||||
sigs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
/// The field a write names, when the name is fixed at compile time. A computed
|
||||
/// key is `define_array_el` / `put_array_el`, and is left alone.
|
||||
fn static_key(i: &RegInstr) -> Option<AtomRef> {
|
||||
let is_write = matches!(i.op, OpCode::DefineField | OpCode::PutField) && i.args.len() == 2;
|
||||
|
||||
match i.imm {
|
||||
ImmOperand::Atom(a) if is_write => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
/// The index of the function for this write, adding it if it is the first of
|
||||
/// its kind. One function serves every write of one field by one opcode.
|
||||
fn intern_fn(lifted: &mut Vec<CodeKind>, op: OpCode, field: AtomRef) -> usize {
|
||||
let wanted = match op {
|
||||
OpCode::DefineField => CodeKind::DefineField(field),
|
||||
_ => CodeKind::PutField(field),
|
||||
};
|
||||
|
||||
let same = |c: &CodeKind| match (c, &wanted) {
|
||||
(CodeKind::DefineField(a), CodeKind::DefineField(b)) => a == b,
|
||||
(CodeKind::PutField(a), CodeKind::PutField(b)) => a == b,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
lifted.iter().position(same).unwrap_or_else(|| {
|
||||
lifted.push(wanted);
|
||||
lifted.len() - 1
|
||||
})
|
||||
}
|
||||
|
||||
/// 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),
|
||||
/// The cpool slot holding function `id`, adding it if it is not there yet. A
|
||||
/// call names its function the way every other reference to one does.
|
||||
fn intern_const(cpool: &mut Vec<JsValue>, id: usize) -> u32 {
|
||||
let at = cpool
|
||||
.iter()
|
||||
.position(|c| matches!(c, JsValue::FnById(f) if *f == id))
|
||||
.unwrap_or_else(|| {
|
||||
cpool.push(JsValue::FnById(id));
|
||||
cpool.len() - 1
|
||||
});
|
||||
|
||||
at as u32
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// The frame slot an object lives in, from whichever instruction defined it.
|
||||
fn frame_slot(code: &[RegInstr], object: usize) -> Option<Source> {
|
||||
code.iter()
|
||||
.find(|i| i.dsts.contains(&object))
|
||||
.and_then(frame_load)
|
||||
}
|
||||
|
||||
/// A frame slot, and the two instructions that read and write it.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
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()];
|
||||
// ── emitted instructions ───────────────────────────────────────────────────
|
||||
|
||||
for (at, i) in code.iter().enumerate() {
|
||||
if let ImmOperand::Label { target } = i.imm {
|
||||
barriers[at] = true;
|
||||
barriers[target as usize] = true;
|
||||
}
|
||||
}
|
||||
|
||||
barriers
|
||||
/// A call to a function fixed at rewrite time. The callee is named by the
|
||||
/// immediate rather than pushed, so the call fits where the write stood.
|
||||
fn call(callee: u32, dst: usize, args: Vec<usize>) -> RegInstr {
|
||||
RegInstr::new(OpCode::Call, ImmOperand::Const(callee), vec![dst], args)
|
||||
}
|
||||
|
||||
/// 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);
|
||||
fn store(slot: Source, value: usize) -> RegInstr {
|
||||
let (op, imm) = match slot {
|
||||
Source::Arg(k) => (OpCode::PutArg, ImmOperand::Arg(k)),
|
||||
Source::Loc(k) => (OpCode::PutLoc, ImmOperand::Loc(k)),
|
||||
Source::VarRef(k) => (OpCode::PutVarRef, ImmOperand::VarRef(k)),
|
||||
};
|
||||
|
||||
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..])
|
||||
RegInstr::new(op, imm, vec![], vec![value])
|
||||
}
|
||||
|
||||
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;
|
||||
fn lifted_name(code: &CodeKind) -> String {
|
||||
match code {
|
||||
CodeKind::DefineField(a) => format!("<define {a:?}>"),
|
||||
CodeKind::PutField(a) => format!("<put {a:?}>"),
|
||||
CodeKind::Code(_) => unreachable!("a lifted function has no body"),
|
||||
}
|
||||
|
||||
if writes == 0 {
|
||||
return Err(ins);
|
||||
}
|
||||
|
||||
Ok((&ins[end..], RunKind::Mutate(loc)))
|
||||
}
|
||||
|
||||
// ───────────────────────────── Matcher Primitives ───────────────────────────
|
||||
// ── operand helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// 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.
|
||||
/// The frame slot an instruction reads. Only plain one-value loads count;
|
||||
/// `get_loc0_loc1` pushes two.
|
||||
fn frame_load(i: &RegInstr) -> Option<Source> {
|
||||
let slot = |imm| match imm {
|
||||
ImmOperand::Loc(k) | ImmOperand::Arg(k) | ImmOperand::VarRef(k) => k,
|
||||
@@ -315,7 +203,22 @@ fn frame_load(i: &RegInstr) -> Option<Source> {
|
||||
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))),
|
||||
// Every local op names the same value, so any of them identifies it
|
||||
OpCode::GetLoc
|
||||
| OpCode::GetLoc8
|
||||
| OpCode::GetLocCheck
|
||||
| OpCode::PutLoc
|
||||
| OpCode::PutLoc8
|
||||
| OpCode::PutLocCheck
|
||||
| OpCode::PutLocCheckInit
|
||||
| OpCode::SetLoc
|
||||
| OpCode::SetLoc8
|
||||
| OpCode::SetLocUninitialized => Some(Source::Loc(slot(i.imm))),
|
||||
|
||||
OpCode::PutLoc0 | OpCode::SetLoc0 => Some(Source::Loc(0)),
|
||||
OpCode::PutLoc1 | OpCode::SetLoc1 => Some(Source::Loc(1)),
|
||||
OpCode::PutLoc2 | OpCode::SetLoc2 => Some(Source::Loc(2)),
|
||||
OpCode::PutLoc3 | OpCode::SetLoc3 => Some(Source::Loc(3)),
|
||||
|
||||
OpCode::GetVarRef0 => Some(Source::VarRef(0)),
|
||||
OpCode::GetVarRef1 => Some(Source::VarRef(1)),
|
||||
|
||||
Reference in New Issue
Block a user