diff --git a/compiler/src/declass/step_1.rs b/compiler/src/declass/step_1.rs index b9ee6d1..074b805 100644 --- a/compiler/src/declass/step_1.rs +++ b/compiler/src/declass/step_1.rs @@ -54,6 +54,7 @@ pub fn rewrite_class_to_proto(reg_fn: &mut RegFn, class_seqs: &[ClassSequence]) for seq in class_seqs { seq.emit_prototype(&code[seq.range], &mut || reg_fn.fresh_val(), &mut buf); + buf[0].block_start = code[seq.range.start].block_start; code[seq.range].swap_with_slice(&mut buf); } diff --git a/compiler/src/declosure.rs b/compiler/src/declosure.rs index f2a8f2b..7384003 100644 --- a/compiler/src/declosure.rs +++ b/compiler/src/declosure.rs @@ -100,12 +100,10 @@ fn take_env_param(mut reg_fn: RegFn) -> RegFn { for instr in code { match is_capture_access(&instr) { // A read keeps its destination, so downstream uses do not change. - Some(CaptureAccess::Read(k)) => { - out.push(get_field(int_field(k + 1), instr.dsts[0], env)) - } - Some(CaptureAccess::Write(k)) => { - out.push(put_field(int_field(k + 1), instr.args[0], env)) - } + Some(CaptureAccess::Read(k)) => out + .push(get_field(int_field(k + 1), instr.dsts[0], env).starting(instr.block_start)), + Some(CaptureAccess::Write(k)) => out + .push(put_field(int_field(k + 1), instr.args[0], env).starting(instr.block_start)), None => out.push(instr), } } @@ -166,7 +164,7 @@ fn build_closure_objects(mut reg_fn: RegFn, captures: &[Vec]) -> RegFn _ => OpCode::PushConst, }; let func = reg_fn.fresh_val(); - out.push(RegInstr::new(load, instr.imm, vec![func], vec![])); + out.push(RegInstr::new(load, instr.imm, vec![func], vec![]).starting(instr.block_start)); let mut obj = reg_fn.fresh_val(); out.push(object(obj)); @@ -240,7 +238,7 @@ fn route_calls_through_object(mut reg_fn: RegFn) -> RegFn { // The callee is the deepest stack slot, so it is popped last. let closure = *instr.args.last().unwrap(); let func = reg_fn.fresh_val(); - out.push(get_field(int_field(0), func, closure)); + out.push(get_field(int_field(0), func, closure).starting(instr.block_start)); // Args become [ env, , func ]: the env is pushed last so // it pops first, and the callee stays last. diff --git a/compiler/src/demutate.rs b/compiler/src/demutate.rs index d4afef6..7c0283e 100644 --- a/compiler/src/demutate.rs +++ b/compiler/src/demutate.rs @@ -39,6 +39,7 @@ pub fn demutate(reg_fns: Vec) -> Vec { code, sigs: vec![], types: vec![], + blocks: vec![], })); fns @@ -64,8 +65,10 @@ fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec let object = instr.args[1]; 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 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 @@ -80,7 +83,7 @@ fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec _ => reg_fn.fresh_val(), }; - new.push(call(callee, dst, instr.args)); + new.push(call(callee, dst, instr.args).starting(instr.block_start)); if let Some(slot) = slot.filter(|s| !matches!(s, Source::Loc(_))) { new.push(store(slot, dst)); @@ -89,6 +92,7 @@ fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec InferFn { types: vec![Type::Unknown; reg_fn.num_vals()], + blocks: reg_fn.blocks, name: reg_fn.name, cpool: reg_fn.cpool, code: CodeKind::Code(new), diff --git a/compiler/src/infer.rs b/compiler/src/infer.rs index d496234..c9cb8d1 100644 --- a/compiler/src/infer.rs +++ b/compiler/src/infer.rs @@ -1,6 +1,6 @@ use crate::{ parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode}, - regify::RegInstr, + regify::{Block, RegInstr}, }; #[derive(Debug, Clone, PartialEq)] @@ -40,6 +40,7 @@ pub struct InferFn { /// 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, + pub blocks: Vec, } /// The lattice, joining what a value already holds with what it is being @@ -92,217 +93,307 @@ pub fn infer(fn_id: usize, fns: &mut Vec) -> Type { // 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 blocks = std::mem::take(&mut fns[fn_id].blocks); + + // The blocks, read back off the instructions. Passes have moved them, but + // none of them adds or removes a jump, so this is the partition regify + // found and `succ` still lines up with it. Anything inserted above the + // first boundary belongs to the first block. + let mut starts = (code.iter().enumerate()) + .filter(|(_, i)| i.block_start) + .map(|(at, _)| at) + .collect::>(); + + if let Some(first) = starts.first_mut() { + *first = 0; + } + + // One flag in, one block out. A pass may move a boundary or insert around + // it, but it must carry the flag when it REPLACES the instruction holding + // one -- that is what `RegInstr::starting` is for. Miss it and the blocks + // silently stop lining up with the edges regify recorded. + debug_assert_eq!( + starts.len(), + blocks.len(), + "{}: block starts lost or gained since regify", + fns[fn_id].name + ); 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 - ); + // What each block is entered with, joined from the blocks that reach it. + let mut entry = vec![vec![Type::Unknown; types.len()]; starts.len()]; - 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, + for (b, &start) in starts.iter().enumerate() { + let end = starts.get(b + 1).copied().unwrap_or(code.len()); - OpCode::PushTrue | OpCode::PushFalse => Type::Bool, - OpCode::Undefined => Type::Undefined, - OpCode::Object => Type::Empty, + // Fold in what the blocks reaching this one left behind. Two paths + // meeting is the ONLY place the algebra applies -- everything below + // runs in a straight line. + for (t, e) in types.iter_mut().zip(&entry[b]) { + *t = join(t, e); + } - 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), + // A slot at a merge keeps the ids the first path in left, so every + // other path's value has to be folded into them. Without this the + // merge reads one arm and believes it. + for &(slot, incoming) in &blocks[b].phi { + types[slot] = join(&types[slot], &types[incoming]); + } + + for instr in &code[start..end] { + // 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, - }, - _ => 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]]) { + // ── 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) + OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => { + match types[instr.args[0]] { + Type::Int => Type::Int, + Type::Float => Type::Float, + _ => Type::Any, } - None => 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 + | OpCode::TailCall => { + // 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[..]), + }, + }; + + let result = 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, + }; + + // A tail call hands its result straight back, so it ends + // the function as much as a `return` does. + if instr.op == OpCode::TailCall { + ret = join(&ret, &result); + } + + result + } + + // A method call keeps its receiver under the callee, so the + // callee is not the last argument and the lookup above does not + // find it. Resolving that is not done yet. + OpCode::CallMethod | OpCode::TailCallMethod => { + if instr.op == OpCode::TailCallMethod { + ret = join(&ret, &Type::Any); + } + + Type::Any + } + + // ── globals ───────────────────────────────────────────────── + // A free name is a property of the global object, which nothing + // here models -- every built-in reaches this way too. Writing + // one (`put_var`) records nothing, because a global is not a + // value and has no id to record it against. + OpCode::GetVar | OpCode::GetVarUndef => 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 ───────────────────────────────────────────────── + // Every way out has to feed `ret`. Missing one leaves it at + // Unknown, the BOTTOM, which a caller folds away and believes + // -- where Any would have been distrusted. + OpCode::Return | OpCode::ReturnAsync => { + ret = join(&ret, &types[instr.args[0]]); + Type::Unknown + } + OpCode::ReturnUndef => { + ret = join(&ret, &Type::Undefined); + Type::Unknown + } + + // ── everything else ───────────────────────────────────────── + _ => Type::Any, + }; + + // A write joins with what the value already holds, so + // reassigning a local a different type widens it to Any. Almost + // every reassignment keeps the type, and join(T, T) = T, so the + // common path costs nothing. + // + // Nothing branches inside a block, so plain `types[dst] = ty` + // would be sound here and would keep `let x = 1; x = 2.5` as + // Float. That is a one-line swap; the blocks do their own job + // either way, which is to join where paths MEET. + if !load { + for &dst in &instr.dsts { + types[dst] = join(&types[dst], &ty); } } + } - // ── 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); + // Hand the state on to every block this one reaches. + for &s in &blocks[b].succ { + for (e, t) in entry[s].iter_mut().zip(&types) { + *e = join(e, t); } } } fns[fn_id].types = types; + fns[fn_id].blocks = blocks; fns[fn_id].code = CodeKind::Code(code); fns[fn_id].sigs[0].ret = ret.clone(); diff --git a/compiler/src/regify.rs b/compiler/src/regify.rs index c27b3ed..2dc76d2 100644 --- a/compiler/src/regify.rs +++ b/compiler/src/regify.rs @@ -2,11 +2,15 @@ use std::fmt::Debug; use crate::{ flatten::{Capture, FlatFn}, - parse::{ImmOperand, JsValue, opcodes::OpCode}, + parse::{ImmOperand, Instr, JsValue, opcodes::OpCode}, }; #[derive(Clone)] pub struct RegInstr { + /// True on the first instruction of a basic block. It rides with the + /// instruction, so a pass that inserts around it does not disturb it, and + /// anything inserted is correctly NOT a boundary. + pub block_start: bool, pub dsts: Vec, pub op: OpCode, pub imm: ImmOperand, @@ -16,24 +20,76 @@ pub struct RegInstr { impl RegInstr { pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec, args: Vec) -> Self { Self { + block_start: false, dsts, op, imm, args, } } + + /// Carry a boundary onto a replacement. A rewrite may turn one instruction + /// into several, but it never moves where a block begins, so the flag goes + /// on the first one out. + pub fn starting(mut self, block_start: bool) -> Self { + self.block_start = block_start; + self + } } impl Debug for RegInstr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{:?} = {:?} {:?} {:?}", - self.dsts, self.op, self.imm, self.args + "{} {:?} = {:?} {:?} {:?}", + if self.block_start { "|" } else { " " }, + self.dsts, + self.op, + self.imm, + self.args ) } } +// ─────────────────────────────── Basic Blocks ─────────────────────────────── +// A block is a run of instructions control enters at the top and leaves at the +// bottom. Blocks are all regify needs: they say what stack a block is entered +// with, and where two paths meet -- the only thing a linear walk gets wrong. + +#[derive(Debug)] +pub struct Block { + /// The blocks control can reach from this one. + pub succ: Vec, + + /// The value ids this block's stack slots hold when it is entered. The + /// first path in fixes them. None until one arrives. + entry: Option>, + + /// `(slot value, incoming value)` for every path in AFTER the first. The + /// slot keeps the id the first path gave it, so a later path's value has to + /// be joined into it. That is the phi, and it stays data because nothing + /// here has to run it -- infer folds the types together instead. + pub phi: Vec<(usize, usize)>, +} + +impl Block { + /// Arrive along one edge, with `stack` in hand. The first arrival names the + /// slots; every later one is reconciled against those names. + fn arrive(&mut self, stack: &[usize]) { + match &self.entry { + None => self.entry = Some(stack.to_vec()), + Some(slots) => self + .phi + .extend(slots.iter().copied().zip(stack.iter().copied())), + } + } + + /// The stack this block starts with. Empty when nothing reaches it. + fn entry_stack(&self) -> Vec { + self.entry.clone().unwrap_or_default() + } +} + #[derive(Debug)] pub struct RegFn { pub name: String, @@ -41,104 +97,132 @@ pub struct RegFn { pub captures: Vec, pub cpool: Vec, pub code: Vec, + /// The basic blocks, in listing order. No pass adds or removes a jump, so + /// these outlive every rewrite. Where a block STARTS is not here: that is + /// an instruction index and it moves, so `RegInstr::block_start` holds it. + pub blocks: Vec, next_val: usize, } impl RegFn { fn from_flat(flat_fn: FlatFn) -> Self { - let mut stack = vec![]; + let pcs = flat_fn.code.iter().map(|i| i.pc).collect::>(); + let (starts, mut blocks) = find_basic_blocks(&flat_fn.code, &pcs); + let mut output = Vec::with_capacity(flat_fn.code.len()); let mut next_val = flat_fn.var_count; // Reserve values for locals - let mut fresh_val = || { - let v = next_val; - next_val += 1; - v - }; + for b in 0..blocks.len() { + // A block starts with the stack it is ENTERED with, not with + // whatever the block above it in the listing left behind. This one + // line is what stops the walk running off a `return` into a + // jump-only path, or through a `goto` into the arm it jumps over. + let mut stack = blocks[b].entry_stack(); + let mut first = true; - for instr in flat_fn.code { - let mut reg_instr = RegInstr { - dsts: vec![], - op: instr.op, - imm: instr.imm_operands, - args: vec![], + let mut fresh_val = || { + let v = next_val; + next_val += 1; + v }; - let extra_pop = match instr.op { - OpCode::Call0 => 0, - OpCode::Call1 => 1, - OpCode::Call2 => 2, - OpCode::Call3 => 3, - _ => match instr.imm_operands { - ImmOperand::Npop(npop) => npop as usize, - ImmOperand::NpopU16 { npop, .. } => npop as usize, - _ => 0, - }, - }; + let end = starts.get(b + 1).copied().unwrap_or(flat_fn.code.len()); - let info = instr.op.info(); - let n_pop = info.n_pop as usize + extra_pop; - let n_push = info.n_push as usize; + for instr in &flat_fn.code[starts[b]..end] { + let mut reg_instr = RegInstr { + block_start: false, + dsts: vec![], + op: instr.op, + imm: instr.imm_operands, + args: vec![], + }; - for _ in 0..n_pop { - let v = stack.pop().unwrap(); - reg_instr.args.push(v); - } + let extra_pop = match instr.op { + OpCode::Call0 => 0, + OpCode::Call1 => 1, + OpCode::Call2 => 2, + OpCode::Call3 => 3, + _ => match instr.imm_operands { + ImmOperand::Npop(npop) => npop as usize, + ImmOperand::NpopU16 { npop, .. } => npop as usize, + _ => 0, + }, + }; - // 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), + let info = instr.op.info(); + let n_pop = info.n_pop as usize + extra_pop; + let n_push = info.n_push as usize; + + for _ in 0..n_pop { + let v = stack.pop().unwrap(); + 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, + }, - 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); + 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 { + let v = fresh_val(); + stack.push(v); + reg_instr.dsts.push(v); + } } } - None => { - for _ in 0..n_push { - let v = fresh_val(); - stack.push(v); - reg_instr.dsts.push(v); - } - } + reg_instr.block_start = std::mem::take(&mut first); + output.push(reg_instr); } - output.push(reg_instr); + // Every edge out of here arrives at its target with this stack. + // One pass in listing order reaches every block, because a block is + // always entered from earlier in the listing before any back edge + // returns to it. + for i in 0..blocks[b].succ.len() { + let to = blocks[b].succ[i]; + blocks[to].arrive(&stack); + } } Self { @@ -147,6 +231,7 @@ impl RegFn { captures: flat_fn.captures, cpool: flat_fn.cpool, code: output, + blocks, next_val, } } @@ -162,6 +247,89 @@ impl RegFn { } } +/// Cut the code into blocks. A block begins at the first instruction, at every +/// jump target, and after anything a block cannot continue past. +/// +/// Two things come back: where each block starts, which is only good while the +/// code stands still, and the block itself, which outlives every later pass. +fn find_basic_blocks(code: &[Instr], pcs: &[usize]) -> (Vec, Vec) { + let mut starts = vec![0]; + + for (at, instr) in code.iter().enumerate() { + if let Some(target) = jump_target(instr.imm_operands, pcs) { + starts.push(target); + } + + if ends_block(instr, pcs) && at + 1 < code.len() { + starts.push(at + 1); + } + } + + starts.sort_unstable(); + starts.dedup(); + + let ends = |i: usize| starts.get(i + 1).copied().unwrap_or(code.len()); + + // A block reaches wherever its last instruction goes: the jump's target, + // and the block below whenever control can run off the end. + let blocks = (0..starts.len()) + .map(|i| { + let last = &code[ends(i) - 1]; + let mut succ = vec![]; + + if let Some(t) = jump_target(last.imm_operands, pcs) { + succ.extend(starts.iter().position(|&s| s == t)); + } + + if falls_through(last.op) && i + 1 < starts.len() { + succ.push(i + 1); + } + + Block { + succ, + entry: None, + phi: vec![], + } + }) + .collect(); + + (starts, blocks) +} + +/// True when a block cannot carry on past this instruction, because it jumps +/// or because it leaves the function. +fn ends_block(instr: &Instr, pcs: &[usize]) -> bool { + !falls_through(instr.op) || jump_target(instr.imm_operands, pcs).is_some() +} + +/// The instruction a jump lands on. A target arrives as a byte offset, so it is +/// looked up in the pcs; one past the end lands nowhere and is dropped. +fn jump_target(imm: ImmOperand, pcs: &[usize]) -> Option { + let target = match imm { + ImmOperand::Label { target } + | ImmOperand::LabelU16 { target, .. } + | ImmOperand::AtomLabelU8 { target, .. } + | ImmOperand::AtomLabelU16 { target, .. } => target, + _ => return None, + }; + + pcs.iter().position(|&pc| pc == target) +} + +/// True when control can leave a block by running off the end of it. +fn falls_through(op: OpCode) -> bool { + !matches!( + op, + OpCode::Goto + | OpCode::Goto8 + | OpCode::Goto16 + | OpCode::Return + | OpCode::ReturnUndef + | OpCode::ReturnAsync + | OpCode::Throw + ) +} + pub fn regify(flat_fns: Vec) -> Vec { flat_fns.into_iter().map(RegFn::from_flat).collect() }