Initial type inference impl
This commit is contained in:
+168
-265
@@ -1,303 +1,191 @@
|
|||||||
//! demutate: object builds and in-place mutations become calls to a function
|
//! demutate: every static-key field write becomes a call to a function that
|
||||||
//! that returns a fresh object (demutate_algo.txt).
|
//! RETURNS a fresh object.
|
||||||
//!
|
//!
|
||||||
//! This file is the detector. It finds the runs and works out what each one
|
//! There is no grouping and no analysis of what surrounds a write. Each one
|
||||||
//! reads from outside itself; lifting a run into its own function, and putting
|
//! stands alone, because its two arguments are the call's two arguments and
|
||||||
//! the call in its place, is the next step.
|
//! the enclosing code already holds both:
|
||||||
//!
|
//!
|
||||||
//! A run is a maximal contiguous stretch of static-key field writes to ONE
|
//! define_field @F [v, o] -> [n] n = <define F>(o, v)
|
||||||
//! object, together with the value computations those writes consume:
|
//! put_field @F [v, o] n = <put F>(o, v) ; store n back
|
||||||
//!
|
//!
|
||||||
//! creation object ; ( <value> ; define_field @F )+
|
//! `object` is left as it is. It already makes a fresh object, so it needs no
|
||||||
//! mutation ( get_loc L ; <value> ; put_field @F )+ (same L)
|
//! function; infer reads it as `Type::Empty`, the base every shape builds on.
|
||||||
|
//!
|
||||||
use std::range::Range;
|
//! 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::{
|
use crate::{
|
||||||
parse::{ImmOperand, opcodes::OpCode},
|
infer::{CodeKind, InferFn, Type},
|
||||||
|
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||||
regify::{RegFn, RegInstr},
|
regify::{RegFn, RegInstr},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn find_object_runs(reg_fn: &RegFn) -> Vec<ObjectRun> {
|
pub fn demutate(reg_fns: Vec<RegFn>) -> Vec<InferFn> {
|
||||||
let barriers = find_barriers(®_fn.code);
|
// 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
|
/// One scan of the code. Everything that is not a static-key write is moved
|
||||||
/// mutation writes its object over and over, so matching anything would find
|
/// across untouched; a write becomes a call, and a `put_field` a call and a
|
||||||
/// the run's own second write as a run of its own.
|
/// store.
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec<CodeKind>) -> InferFn {
|
||||||
enum Look {
|
let code = std::mem::take(&mut reg_fn.code);
|
||||||
Anything,
|
let mut new = Vec::with_capacity(code.len());
|
||||||
BuildsOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The runs in `ins`, each holding whatever runs are nested inside it.
|
for instr in code {
|
||||||
fn collect_runs(
|
let Some(field) = static_key(&instr) else {
|
||||||
mut ins: &[RegInstr],
|
new.push(instr); // Not a field write, so it passes through
|
||||||
mut barriers: &[bool],
|
continue;
|
||||||
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
|
let define = instr.op == OpCode::DefineField;
|
||||||
// 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 {
|
// Where the object lives says where its replacement goes. The
|
||||||
range,
|
// instruction that made it is already in `new`.
|
||||||
kind,
|
let object = instr.args[1];
|
||||||
params: find_params(run_instr, offset, nested),
|
let slot = (!define).then(|| frame_slot(&new, object)).flatten();
|
||||||
|
|
||||||
|
let callee =
|
||||||
|
intern_const(&mut reg_fn.cpool, first_lifted + intern_fn(lifted, instr.op, field));
|
||||||
|
|
||||||
|
let dst = match slot {
|
||||||
|
// define_field hands the object back, so the call keeps its dst
|
||||||
|
_ if define => instr.dsts[0],
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
new.push(call(callee, dst, instr.args));
|
||||||
|
|
||||||
|
if let Some(slot) = slot.filter(|s| !matches!(s, Source::Loc(_))) {
|
||||||
|
new.push(store(slot, dst));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InferFn {
|
||||||
|
types: vec![Type::Unknown; reg_fn.num_vals()],
|
||||||
|
name: reg_fn.name,
|
||||||
|
cpool: reg_fn.cpool,
|
||||||
|
code: CodeKind::Code(new),
|
||||||
|
sigs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
});
|
});
|
||||||
|
|
||||||
offset += consumed;
|
at as u32
|
||||||
ins = rest;
|
|
||||||
barriers = &barriers[consumed..];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Did not find a run, advance past what was tried and go again
|
/// The frame slot an object lives in, from whichever instruction defined it.
|
||||||
Err(rest) => {
|
fn frame_slot(code: &[RegInstr], object: usize) -> Option<Source> {
|
||||||
let consumed = ins.len() - rest.len(); // Handle partial consumption
|
code.iter()
|
||||||
let consumed = consumed.max(1);
|
.find(|i| i.dsts.contains(&object))
|
||||||
offset += consumed;
|
.and_then(frame_load)
|
||||||
ins = &ins[consumed..];
|
|
||||||
barriers = &barriers[consumed..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
found
|
/// A frame slot, and the two instructions that read and write it.
|
||||||
}
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Source {
|
||||||
#[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),
|
Arg(u16),
|
||||||
Loc(u16),
|
Loc(u16),
|
||||||
VarRef(u16),
|
VarRef(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a run may not reach across: a jump, and anything a jump lands on. A
|
// ── emitted instructions ───────────────────────────────────────────────────
|
||||||
/// 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() {
|
/// A call to a function fixed at rewrite time. The callee is named by the
|
||||||
if let ImmOperand::Label { target } = i.imm {
|
/// immediate rather than pushed, so the call fits where the write stood.
|
||||||
barriers[at] = true;
|
fn call(callee: u32, dst: usize, args: Vec<usize>) -> RegInstr {
|
||||||
barriers[target as usize] = true;
|
RegInstr::new(OpCode::Call, ImmOperand::Const(callee), vec![dst], args)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
barriers
|
fn store(slot: Source, value: usize) -> RegInstr {
|
||||||
}
|
let (op, imm) = match slot {
|
||||||
|
Source::Arg(k) => (OpCode::PutArg, ImmOperand::Arg(k)),
|
||||||
/// What the run has to be handed, in first-use order. A frame slot appears
|
Source::Loc(k) => (OpCode::PutLoc, ImmOperand::Loc(k)),
|
||||||
/// once however many times it is loaded, which folds a mutation's repeated
|
Source::VarRef(k) => (OpCode::PutVarRef, ImmOperand::VarRef(k)),
|
||||||
/// 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 {
|
RegInstr::new(op, imm, vec![], vec![value])
|
||||||
return Err(ins);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// define_field hands the object back as a NEW value each time, so the run
|
fn lifted_name(code: &CodeKind) -> String {
|
||||||
// holds together as a chain: follow it from one write to the next
|
match code {
|
||||||
let mut obj = first.dsts[0];
|
CodeKind::DefineField(a) => format!("<define {a:?}>"),
|
||||||
let mut end = 1;
|
CodeKind::PutField(a) => format!("<put {a:?}>"),
|
||||||
let mut writes = 0;
|
CodeKind::Code(_) => unreachable!("a lifted function has no body"),
|
||||||
|
}
|
||||||
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 {
|
// ── operand helpers ────────────────────────────────────────────────────────
|
||||||
return Err(ins); // A bare `object` with no writes is not a run
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(&ins[end..])
|
/// The frame slot an instruction reads. Only plain one-value loads count;
|
||||||
}
|
/// `get_loc0_loc1` pushes two.
|
||||||
|
|
||||||
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> {
|
fn frame_load(i: &RegInstr) -> Option<Source> {
|
||||||
let slot = |imm| match imm {
|
let slot = |imm| match imm {
|
||||||
ImmOperand::Loc(k) | ImmOperand::Arg(k) | ImmOperand::VarRef(k) => k,
|
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::GetLoc1 => Some(Source::Loc(1)),
|
||||||
OpCode::GetLoc2 => Some(Source::Loc(2)),
|
OpCode::GetLoc2 => Some(Source::Loc(2)),
|
||||||
OpCode::GetLoc3 => Some(Source::Loc(3)),
|
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::GetVarRef0 => Some(Source::VarRef(0)),
|
||||||
OpCode::GetVarRef1 => Some(Source::VarRef(1)),
|
OpCode::GetVarRef1 => Some(Source::VarRef(1)),
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
use crate::{
|
||||||
|
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||||
|
regify::RegInstr,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Type {
|
||||||
|
Unknown,
|
||||||
|
Undefined,
|
||||||
|
Bool,
|
||||||
|
Int,
|
||||||
|
Float,
|
||||||
|
Empty,
|
||||||
|
/// A named function, by flat fn id.
|
||||||
|
Fn(usize),
|
||||||
|
ReturnOf((usize, Vec<Type>)),
|
||||||
|
Any,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CodeKind {
|
||||||
|
Code(Vec<RegInstr>),
|
||||||
|
DefineField(AtomRef),
|
||||||
|
PutField(AtomRef),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Signature {
|
||||||
|
pub args: Vec<Type>,
|
||||||
|
pub ret: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InferFn {
|
||||||
|
pub name: String,
|
||||||
|
pub cpool: Vec<JsValue>,
|
||||||
|
pub code: CodeKind,
|
||||||
|
pub sigs: Vec<Signature>,
|
||||||
|
/// One entry per value id, so a value's type is `types[v]`. regify keeps
|
||||||
|
/// the first `var_count` ids for the locals, so a local's type lives there
|
||||||
|
/// too and `put_loc` / `get_loc` just copy between the two.
|
||||||
|
pub types: Vec<Type>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The lattice, joining what a value already holds with what it is being
|
||||||
|
/// given. Unknown is the bottom and gives way to anything; Any is the top and
|
||||||
|
/// swallows everything; two types that disagree can only be described by Any.
|
||||||
|
pub fn join(a: &Type, b: &Type) -> Type {
|
||||||
|
match (a, b) {
|
||||||
|
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
|
||||||
|
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
||||||
|
(a, b) if a == b => a.clone(),
|
||||||
|
_ => Type::Any,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk a function's code once, giving every value a type, and hand back what
|
||||||
|
/// the function returns. A call is followed down into its callee.
|
||||||
|
pub fn infer(fn_id: usize, fns: &mut Vec<InferFn>) -> Type {
|
||||||
|
// The caller left this function's argument types here before recursing.
|
||||||
|
let args = match fns[fn_id].sigs.first() {
|
||||||
|
Some(sig) => sig.args.clone(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two field writers have no body: their result IS the shape term that
|
||||||
|
// names them, built from the types they were handed.
|
||||||
|
let code = match &fns[fn_id].code {
|
||||||
|
CodeKind::DefineField(_) | CodeKind::PutField(_) => return Type::ReturnOf((fn_id, args)),
|
||||||
|
|
||||||
|
// An empty body means this function's code is out on the stack, so we
|
||||||
|
// have come back round a cycle. Unknown is the bottom, and a later
|
||||||
|
// round raises it.
|
||||||
|
CodeKind::Code(c) if c.is_empty() => return Type::Unknown,
|
||||||
|
|
||||||
|
CodeKind::Code(_) => {
|
||||||
|
let taken = std::mem::replace(&mut fns[fn_id].code, CodeKind::Code(vec![]));
|
||||||
|
let CodeKind::Code(code) = taken else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
code
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if fns[fn_id].sigs.is_empty() {
|
||||||
|
fns[fn_id].sigs.push(Signature {
|
||||||
|
args: vec![],
|
||||||
|
ret: Type::Unknown,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The types move out too, so a callee can be inferred through `fns` while
|
||||||
|
// this function is still being walked. Both go back at the end.
|
||||||
|
let mut types = std::mem::take(&mut fns[fn_id].types);
|
||||||
|
let mut ret = Type::Unknown;
|
||||||
|
|
||||||
|
for instr in &code {
|
||||||
|
// regify does not rename locals, so a load just names the local's value
|
||||||
|
// again. It must not write to it, or it would wipe what is stored there.
|
||||||
|
let load = matches!(
|
||||||
|
instr.op,
|
||||||
|
OpCode::GetLoc0
|
||||||
|
| OpCode::GetLoc1
|
||||||
|
| OpCode::GetLoc2
|
||||||
|
| OpCode::GetLoc3
|
||||||
|
| OpCode::GetLoc0Loc1
|
||||||
|
| OpCode::GetLoc
|
||||||
|
| OpCode::GetLoc8
|
||||||
|
| OpCode::GetLocCheck
|
||||||
|
);
|
||||||
|
|
||||||
|
let ty = match instr.op {
|
||||||
|
// ── literals ────────────────────────────────────────────────
|
||||||
|
OpCode::PushMinus1
|
||||||
|
| OpCode::Push0
|
||||||
|
| OpCode::Push1
|
||||||
|
| OpCode::Push2
|
||||||
|
| OpCode::Push3
|
||||||
|
| OpCode::Push4
|
||||||
|
| OpCode::Push5
|
||||||
|
| OpCode::Push6
|
||||||
|
| OpCode::Push7
|
||||||
|
| OpCode::PushI8
|
||||||
|
| OpCode::PushI16
|
||||||
|
| OpCode::PushI32 => Type::Int,
|
||||||
|
|
||||||
|
OpCode::PushTrue | OpCode::PushFalse => Type::Bool,
|
||||||
|
OpCode::Undefined => Type::Undefined,
|
||||||
|
OpCode::Object => Type::Empty,
|
||||||
|
|
||||||
|
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8 => {
|
||||||
|
match instr.imm {
|
||||||
|
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
|
||||||
|
Some(JsValue::Int(_)) => Type::Int,
|
||||||
|
Some(JsValue::Float(_)) => Type::Float,
|
||||||
|
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
|
||||||
|
Some(JsValue::Undefined) => Type::Undefined,
|
||||||
|
Some(&JsValue::FnById(id)) => Type::Fn(id),
|
||||||
|
_ => Type::Any,
|
||||||
|
},
|
||||||
|
_ => Type::Any,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── field reads ─────────────────────────────────────────────
|
||||||
|
// Walk the shape term from the outside in: the newest write of the
|
||||||
|
// field wins, which is what makes a later write shadow an earlier.
|
||||||
|
OpCode::GetField => {
|
||||||
|
let field = match instr.imm {
|
||||||
|
ImmOperand::Atom(a) => a,
|
||||||
|
_ => AtomRef::TaggedInt(u32::MAX),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ty = types[instr.args[0]].clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (id, args) = match ty {
|
||||||
|
Type::ReturnOf((id, args)) if args.len() == 2 => (id, args),
|
||||||
|
Type::Empty => break Type::Undefined, // No fields at all
|
||||||
|
_ => break Type::Any,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &fns[id].code {
|
||||||
|
CodeKind::DefineField(a) | CodeKind::PutField(a) if *a == field => {
|
||||||
|
break args[1].clone();
|
||||||
|
}
|
||||||
|
CodeKind::DefineField(_) | CodeKind::PutField(_) => ty = args[0].clone(),
|
||||||
|
CodeKind::Code(_) => break Type::Any, // Not a shape we built
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── arithmetic ──────────────────────────────────────────────
|
||||||
|
// A float anywhere makes the result a float; anything else can be
|
||||||
|
// a string concatenation or an object coercion, so it is Any.
|
||||||
|
OpCode::Add | OpCode::Sub | OpCode::Mul | OpCode::Div | OpCode::Mod | OpCode::Pow => {
|
||||||
|
match (&types[instr.args[0]], &types[instr.args[1]]) {
|
||||||
|
(Type::Int, Type::Int) => Type::Int,
|
||||||
|
(Type::Float, Type::Float)
|
||||||
|
| (Type::Int, Type::Float)
|
||||||
|
| (Type::Float, Type::Int) => Type::Float,
|
||||||
|
_ => Type::Any,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => match types[instr.args[0]] {
|
||||||
|
Type::Int => Type::Int,
|
||||||
|
Type::Float => Type::Float,
|
||||||
|
_ => Type::Any,
|
||||||
|
},
|
||||||
|
|
||||||
|
OpCode::Lt
|
||||||
|
| OpCode::Lte
|
||||||
|
| OpCode::Gt
|
||||||
|
| OpCode::Gte
|
||||||
|
| OpCode::Eq
|
||||||
|
| OpCode::Neq
|
||||||
|
| OpCode::StrictEq
|
||||||
|
| OpCode::StrictNeq
|
||||||
|
| OpCode::Not => Type::Bool,
|
||||||
|
|
||||||
|
// ── calls ───────────────────────────────────────────────────
|
||||||
|
// demutate names a lifted callee in the immediate. Anything else
|
||||||
|
// is a callee off the stack, which this does not resolve yet.
|
||||||
|
OpCode::Call | OpCode::Call0 | OpCode::Call1 | OpCode::Call2 | OpCode::Call3 => {
|
||||||
|
// demutate names its callee in the immediate; every other call
|
||||||
|
// takes it off the stack, where it is deepest and so comes last.
|
||||||
|
// Reversing the rest puts them back in written order.
|
||||||
|
let (callee, args) = match instr.imm {
|
||||||
|
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
|
||||||
|
Some(&JsValue::FnById(id)) => (Some(id), &instr.args[..]),
|
||||||
|
_ => (None, &instr.args[..]),
|
||||||
|
},
|
||||||
|
_ => match instr.args.last().map(|a| &types[*a]) {
|
||||||
|
Some(&Type::Fn(id)) => (Some(id), &instr.args[..instr.args.len() - 1]),
|
||||||
|
_ => (None, &instr.args[..]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match callee {
|
||||||
|
Some(callee) => {
|
||||||
|
let args = args.iter().rev().map(|a| types[*a].clone()).collect();
|
||||||
|
fns[callee].sigs = vec![Signature {
|
||||||
|
args,
|
||||||
|
ret: Type::Unknown,
|
||||||
|
}];
|
||||||
|
infer(callee, fns)
|
||||||
|
}
|
||||||
|
None => Type::Any,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── arguments ───────────────────────────────────────────────
|
||||||
|
OpCode::GetArg0 => args.first().cloned().unwrap_or(Type::Undefined),
|
||||||
|
OpCode::GetArg1 => args.get(1).cloned().unwrap_or(Type::Undefined),
|
||||||
|
OpCode::GetArg2 => args.get(2).cloned().unwrap_or(Type::Undefined),
|
||||||
|
OpCode::GetArg3 => args.get(3).cloned().unwrap_or(Type::Undefined),
|
||||||
|
OpCode::GetArg => match instr.imm {
|
||||||
|
ImmOperand::Arg(k) => args.get(k as usize).cloned().unwrap_or(Type::Undefined),
|
||||||
|
_ => Type::Any,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── value movers ────────────────────────────────────────────
|
||||||
|
// These hand the value straight back, so the type goes through.
|
||||||
|
// dup pushes it twice, which the assignment below spreads over
|
||||||
|
// both destinations.
|
||||||
|
// A store into a local is one of these too: the local IS the
|
||||||
|
// destination value, so the type simply moves across.
|
||||||
|
OpCode::SetName
|
||||||
|
| OpCode::SetNameComputed
|
||||||
|
| OpCode::SetHomeObject
|
||||||
|
| OpCode::Dup
|
||||||
|
| OpCode::PutLoc0
|
||||||
|
| OpCode::PutLoc1
|
||||||
|
| OpCode::PutLoc2
|
||||||
|
| OpCode::PutLoc3
|
||||||
|
| OpCode::PutLoc
|
||||||
|
| OpCode::PutLoc8
|
||||||
|
| OpCode::PutLocCheck
|
||||||
|
| OpCode::PutLocCheckInit
|
||||||
|
| OpCode::SetLoc0
|
||||||
|
| OpCode::SetLoc1
|
||||||
|
| OpCode::SetLoc2
|
||||||
|
| OpCode::SetLoc3
|
||||||
|
| OpCode::SetLoc
|
||||||
|
| OpCode::SetLoc8 => types[instr.args[0]].clone(),
|
||||||
|
|
||||||
|
OpCode::SetLocUninitialized => Type::Unknown,
|
||||||
|
|
||||||
|
// ── returns ─────────────────────────────────────────────────
|
||||||
|
OpCode::Return => {
|
||||||
|
ret = join(&ret, &types[instr.args[0]]);
|
||||||
|
Type::Unknown
|
||||||
|
}
|
||||||
|
OpCode::ReturnUndef => {
|
||||||
|
ret = join(&ret, &Type::Undefined);
|
||||||
|
Type::Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── everything else ─────────────────────────────────────────
|
||||||
|
_ => Type::Any,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assigning follows the algebra rather than overwriting. A local keeps
|
||||||
|
// one value id, so both arms of a branch write it, and the join is what
|
||||||
|
// stops the second simply winning.
|
||||||
|
//
|
||||||
|
// IMPRECISE, and it needs a CFG to fix: joining EVERY write treats a
|
||||||
|
// plain reassignment like a branch, so `let x = 1; x = 2.5` gives Any
|
||||||
|
// where Float is right. Only a merge point should join; an
|
||||||
|
// unconditional store should replace. A linear walk cannot tell the two
|
||||||
|
// apart. It also walks a loop body once, with the types from before the
|
||||||
|
// loop, instead of iterating it.
|
||||||
|
//
|
||||||
|
// Both need the jump targets, and `ImmOperand::Label` still holds a
|
||||||
|
// BYTE offset (parse.rs resolves it against pc), which means nothing
|
||||||
|
// once the code is a list of instructions.
|
||||||
|
if !load {
|
||||||
|
for &dst in &instr.dsts {
|
||||||
|
types[dst] = join(&types[dst], &ty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fns[fn_id].types = types;
|
||||||
|
fns[fn_id].code = CodeKind::Code(code);
|
||||||
|
fns[fn_id].sigs[0].ret = ret.clone();
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
+16
-5
@@ -2,6 +2,7 @@ mod declass;
|
|||||||
mod declosure;
|
mod declosure;
|
||||||
mod demutate;
|
mod demutate;
|
||||||
mod flatten;
|
mod flatten;
|
||||||
|
mod infer;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod regify;
|
mod regify;
|
||||||
|
|
||||||
@@ -9,7 +10,9 @@ use std::process::Command;
|
|||||||
|
|
||||||
use declass::declass;
|
use declass::declass;
|
||||||
use declosure::declosure;
|
use declosure::declosure;
|
||||||
|
use demutate::demutate;
|
||||||
use flatten::flatten;
|
use flatten::flatten;
|
||||||
|
use infer::infer;
|
||||||
use parse::QuickJSBinary;
|
use parse::QuickJSBinary;
|
||||||
use regify::regify;
|
use regify::regify;
|
||||||
|
|
||||||
@@ -44,15 +47,23 @@ 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:#?}");
|
||||||
|
|
||||||
|
println!("=== after demutate ===");
|
||||||
|
let mut infer_fns = demutate(declosured);
|
||||||
|
println!("{infer_fns:#?}");
|
||||||
|
|
||||||
|
println!("=== after infer ===");
|
||||||
|
if let Some(main_id) = infer_fns.iter().position(|f| f.name == "main") {
|
||||||
|
let ret = infer(main_id, &mut infer_fns);
|
||||||
|
println!("main returns {ret:?}");
|
||||||
|
println!("{infer_fns:#?}");
|
||||||
|
} else {
|
||||||
|
println!("no main");
|
||||||
|
}
|
||||||
|
|
||||||
println!("{atoms:#?}");
|
println!("{atoms:#?}");
|
||||||
|
|
||||||
/*println!("=== after deprototype ===");
|
/*println!("=== after deprototype ===");
|
||||||
|
|||||||
@@ -85,11 +85,58 @@ impl RegFn {
|
|||||||
reg_instr.args.push(v);
|
reg_instr.args.push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Locals are NOT renamed: local k is value k for the whole function,
|
||||||
|
// the id reserved above. A load pushes it, a store defines it again,
|
||||||
|
// and neither makes a new value -- so a local keeps one identity
|
||||||
|
// however many times it is read and written.
|
||||||
|
let local = match instr.op {
|
||||||
|
OpCode::GetLoc0 | OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0),
|
||||||
|
OpCode::GetLoc1 | OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1),
|
||||||
|
OpCode::GetLoc2 | OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2),
|
||||||
|
OpCode::GetLoc3 | OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3),
|
||||||
|
|
||||||
|
OpCode::GetLoc
|
||||||
|
| OpCode::GetLoc8
|
||||||
|
| OpCode::GetLocCheck
|
||||||
|
| OpCode::PutLoc
|
||||||
|
| OpCode::PutLoc8
|
||||||
|
| OpCode::PutLocCheck
|
||||||
|
| OpCode::PutLocCheckInit
|
||||||
|
| OpCode::SetLoc
|
||||||
|
| OpCode::SetLoc8
|
||||||
|
| OpCode::SetLocUninitialized => match reg_instr.imm {
|
||||||
|
ImmOperand::Loc(k) => Some(k as usize),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match local {
|
||||||
|
Some(k) => {
|
||||||
|
reg_instr.dsts.push(k);
|
||||||
|
|
||||||
|
if n_push == 1 {
|
||||||
|
stack.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one op that reads two locals at once
|
||||||
|
None if instr.op == OpCode::GetLoc0Loc1 => {
|
||||||
|
for k in [0, 1] {
|
||||||
|
stack.push(k);
|
||||||
|
reg_instr.dsts.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
for _ in 0..n_push {
|
for _ in 0..n_push {
|
||||||
let v = fresh_val();
|
let v = fresh_val();
|
||||||
stack.push(v);
|
stack.push(v);
|
||||||
reg_instr.dsts.push(v);
|
reg_instr.dsts.push(v);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output.push(reg_instr);
|
output.push(reg_instr);
|
||||||
}
|
}
|
||||||
@@ -109,6 +156,10 @@ impl RegFn {
|
|||||||
self.next_val += 1;
|
self.next_val += 1;
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn num_vals(&self) -> usize {
|
||||||
|
self.next_val
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
||||||
|
|||||||
+76
-22
@@ -34,16 +34,23 @@ DefineField both become DefineField on the fresh copy the function owns.
|
|||||||
PARAMETERS -- the rule that keeps this general
|
PARAMETERS -- the rule that keeps this general
|
||||||
====================================================================
|
====================================================================
|
||||||
|
|
||||||
A value the run USES but does not itself PRODUCE becomes a parameter, in
|
A run is handed anything it does not make for itself. Everything else moves
|
||||||
first-use order. Everything produced inside the run (Object, constants,
|
into the function unchanged. There are two kinds of parameter:
|
||||||
arithmetic that is wholly inside) moves into the function unchanged.
|
|
||||||
|
|
||||||
the object being mutated is used-but-not-produced -> it is a parameter.
|
FRAME. GetArg, GetLoc, GetVarRef and their numbered forms are frame
|
||||||
a constant field value (Push1) is produced inside -> it moves in.
|
reads. Each becomes a parameter, and the load stays at the call site.
|
||||||
a field value from the enclosing frame (GetArg p) -> a parameter.
|
RUN. A nested build inside this run. That run is lifted first, and its
|
||||||
|
call result is passed in. See NESTED BUILDS.
|
||||||
|
|
||||||
So a pure literal of constants lifts to a 0-parameter function; a mutation of
|
Everything else -- constants, arithmetic, field reads off a parameter -- is
|
||||||
`a` lifts to a 1-parameter function; `a.x = p + 1` lifts to a 2-parameter one.
|
made by the run and moves in.
|
||||||
|
|
||||||
|
Parameters are taken in first-use order. A frame slot appears once however
|
||||||
|
many times it is loaded, so a mutation's repeated loads of its object fold
|
||||||
|
into the one parameter, and the object is always parameter 0.
|
||||||
|
|
||||||
|
So a literal of constants lifts to a 0-parameter function; a mutation of `a`
|
||||||
|
lifts to a 1-parameter function; `a.x = p + 1` lifts to a 2-parameter one.
|
||||||
|
|
||||||
|
|
||||||
====================================================================
|
====================================================================
|
||||||
@@ -83,17 +90,32 @@ loads it with Fclosure8 Const(K).
|
|||||||
DETECTION (one linear walk)
|
DETECTION (one linear walk)
|
||||||
====================================================================
|
====================================================================
|
||||||
|
|
||||||
start a run at an Object, or at the first static-key write to an object.
|
A run cannot be matched opcode by opcode, the way a class build can,
|
||||||
extend it while the next instruction is one of:
|
because <value> is any expression at all. FOLLOW THE OBJECT instead:
|
||||||
Object (a nested object is its own run)
|
|
||||||
a value computation feeding a write in this run
|
start a run at an Object (creation), or at a load of a slot that a
|
||||||
DefineField, imm == Atom(F) (same object)
|
static-key write then writes to (mutation).
|
||||||
PutField, imm == Atom(F) (same object)
|
skip forward to the next instruction that USES the object.
|
||||||
a (re)load of the same object
|
carry on only while that use is a static-key write to it.
|
||||||
|
everything passed over on the way is the value computation, and it
|
||||||
|
joins the run.
|
||||||
|
|
||||||
|
A creation and a mutation differ only in how the object carries from one
|
||||||
|
write to the next:
|
||||||
|
|
||||||
|
DefineField hands the object back as a NEW value, so a creation run
|
||||||
|
is one value chain.
|
||||||
|
PutField hands nothing back, so a mutation re-loads the object before
|
||||||
|
every write.
|
||||||
|
|
||||||
END the run at any of:
|
END the run at any of:
|
||||||
the object is READ for anything but a field write -- passed to a call,
|
the object is READ for anything but a field write -- passed to a call,
|
||||||
used in an expression, branched on.
|
used in an expression, branched on.
|
||||||
a static-key write to a DIFFERENT object.
|
a static-key write to a DIFFERENT object.
|
||||||
|
a jump, or an instruction some jump lands on. A run is ONE straight
|
||||||
|
line of code, so both sides of a branch are cut off. Without this
|
||||||
|
an `if` in the middle of a sequence of writes joins the two sides
|
||||||
|
into one run.
|
||||||
|
|
||||||
"Same object": a mutation re-loads the object before each write (each
|
"Same object": a mutation re-loads the object before each write (each
|
||||||
GetArg0 / GetLocCheck is a fresh value), so sameness is judged by the load
|
GetArg0 / GetLocCheck is a fresh value), so sameness is judged by the load
|
||||||
@@ -103,6 +125,27 @@ DETECTION (one linear walk)
|
|||||||
|
|
||||||
Ignore computed-key writes (DefineArrayEl / PutArrayEl) -- see extensions.
|
Ignore computed-key writes (DefineArrayEl / PutArrayEl) -- see extensions.
|
||||||
|
|
||||||
|
NESTED BUILDS: a run's own instructions are walked AGAIN, to find the
|
||||||
|
object builds inside it. Every Object in the program ends up in exactly
|
||||||
|
one lifted function, so one function describes one object shape.
|
||||||
|
|
||||||
|
{ x: { y: 1 }, z: { w: 2 } } lifts to THREE functions, not one.
|
||||||
|
|
||||||
|
An inner run is held INSIDE its parent's parameter list, as the Run
|
||||||
|
parameter it becomes. So lifting is depth-first: lift a run's Run
|
||||||
|
parameters, then the run itself, which needs their results.
|
||||||
|
|
||||||
|
The inner walk looks for BUILDS ONLY. A mutation writes its object over
|
||||||
|
and over, so a walk that looked for anything would match the run's own
|
||||||
|
second write as a run of its own:
|
||||||
|
|
||||||
|
a.x = 1 ; a.y = 2 one mutation run, NOT a run with one nested
|
||||||
|
|
||||||
|
ORDER IN THE PASS: demutate runs BEFORE declosure. declosure builds its
|
||||||
|
closure objects out of Object and DefineField Atom(int(k)), which a run
|
||||||
|
cannot tell apart from a user object. Run it after, and every closure gets
|
||||||
|
lifted as if it were an object literal.
|
||||||
|
|
||||||
|
|
||||||
====================================================================
|
====================================================================
|
||||||
WORKED EXAMPLE (test), real instructions
|
WORKED EXAMPLE (test), real instructions
|
||||||
@@ -176,13 +219,24 @@ new FnById(Nm), name "<mutate>", arg_count 1:
|
|||||||
FIELD VALUE FORMS -- all handled the same way
|
FIELD VALUE FORMS -- all handled the same way
|
||||||
====================================================================
|
====================================================================
|
||||||
|
|
||||||
constant a.x = 1 Push1 moves into the function
|
One rule covers them all: a frame read (GetArg, GetLoc, GetVarRef) becomes a
|
||||||
argument { x: p } GetArg becomes a parameter
|
parameter and its load stays at the call site; everything else moves in.
|
||||||
computed a.x = p + 1 p is a parameter; the Add moves in
|
|
||||||
field read a.x = b.y b is a parameter; the GetField moves in
|
a.x = 1 constant
|
||||||
nested object { x: { y: 1 } } the inner Object is its own run, lifted
|
Push1 is produced inside the run, so it moves in.
|
||||||
first; its result is a value the outer
|
|
||||||
run uses -> a parameter of the outer
|
{ x: p } argument
|
||||||
|
GetArg is a frame read, so p becomes a parameter.
|
||||||
|
|
||||||
|
a.x = p + 1 computed
|
||||||
|
p is a parameter; the Add is produced inside, so it moves in.
|
||||||
|
|
||||||
|
a.x = b.y field read
|
||||||
|
b is a parameter; the GetField is produced inside, so it moves in.
|
||||||
|
|
||||||
|
{ x: { y: 1 } } nested object
|
||||||
|
The inner Object is a run of its OWN. It is lifted first, and the
|
||||||
|
outer run takes its result as a parameter. See NESTED BUILDS.
|
||||||
|
|
||||||
|
|
||||||
====================================================================
|
====================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user