Compute phis
This commit is contained in:
@@ -54,6 +54,7 @@ pub fn rewrite_class_to_proto(reg_fn: &mut RegFn, class_seqs: &[ClassSequence])
|
|||||||
|
|
||||||
for seq in class_seqs {
|
for seq in class_seqs {
|
||||||
seq.emit_prototype(&code[seq.range], &mut || reg_fn.fresh_val(), &mut buf);
|
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);
|
code[seq.range].swap_with_slice(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,12 +100,10 @@ fn take_env_param(mut reg_fn: RegFn) -> RegFn {
|
|||||||
for instr in code {
|
for instr in code {
|
||||||
match is_capture_access(&instr) {
|
match is_capture_access(&instr) {
|
||||||
// A read keeps its destination, so downstream uses do not change.
|
// A read keeps its destination, so downstream uses do not change.
|
||||||
Some(CaptureAccess::Read(k)) => {
|
Some(CaptureAccess::Read(k)) => out
|
||||||
out.push(get_field(int_field(k + 1), instr.dsts[0], env))
|
.push(get_field(int_field(k + 1), instr.dsts[0], env).starting(instr.block_start)),
|
||||||
}
|
Some(CaptureAccess::Write(k)) => out
|
||||||
Some(CaptureAccess::Write(k)) => {
|
.push(put_field(int_field(k + 1), instr.args[0], env).starting(instr.block_start)),
|
||||||
out.push(put_field(int_field(k + 1), instr.args[0], env))
|
|
||||||
}
|
|
||||||
None => out.push(instr),
|
None => out.push(instr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +164,7 @@ fn build_closure_objects(mut reg_fn: RegFn, captures: &[Vec<Capture>]) -> RegFn
|
|||||||
_ => OpCode::PushConst,
|
_ => OpCode::PushConst,
|
||||||
};
|
};
|
||||||
let func = reg_fn.fresh_val();
|
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();
|
let mut obj = reg_fn.fresh_val();
|
||||||
out.push(object(obj));
|
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.
|
// The callee is the deepest stack slot, so it is popped last.
|
||||||
let closure = *instr.args.last().unwrap();
|
let closure = *instr.args.last().unwrap();
|
||||||
let func = reg_fn.fresh_val();
|
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, <original args>, func ]: the env is pushed last so
|
// Args become [ env, <original args>, func ]: the env is pushed last so
|
||||||
// it pops first, and the callee stays last.
|
// it pops first, and the callee stays last.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub fn demutate(reg_fns: Vec<RegFn>) -> Vec<InferFn> {
|
|||||||
code,
|
code,
|
||||||
sigs: vec![],
|
sigs: vec![],
|
||||||
types: vec![],
|
types: vec![],
|
||||||
|
blocks: vec![],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
fns
|
fns
|
||||||
@@ -64,8 +65,10 @@ fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec<CodeKind>
|
|||||||
let object = instr.args[1];
|
let object = instr.args[1];
|
||||||
let slot = (!define).then(|| frame_slot(&new, object)).flatten();
|
let slot = (!define).then(|| frame_slot(&new, object)).flatten();
|
||||||
|
|
||||||
let callee =
|
let callee = intern_const(
|
||||||
intern_const(&mut reg_fn.cpool, first_lifted + intern_fn(lifted, instr.op, field));
|
&mut reg_fn.cpool,
|
||||||
|
first_lifted + intern_fn(lifted, instr.op, field),
|
||||||
|
);
|
||||||
|
|
||||||
let dst = match slot {
|
let dst = match slot {
|
||||||
// define_field hands the object back, so the call keeps its dst
|
// 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<CodeKind>
|
|||||||
_ => reg_fn.fresh_val(),
|
_ => 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(_))) {
|
if let Some(slot) = slot.filter(|s| !matches!(s, Source::Loc(_))) {
|
||||||
new.push(store(slot, dst));
|
new.push(store(slot, dst));
|
||||||
@@ -89,6 +92,7 @@ fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec<CodeKind>
|
|||||||
|
|
||||||
InferFn {
|
InferFn {
|
||||||
types: vec![Type::Unknown; reg_fn.num_vals()],
|
types: vec![Type::Unknown; reg_fn.num_vals()],
|
||||||
|
blocks: reg_fn.blocks,
|
||||||
name: reg_fn.name,
|
name: reg_fn.name,
|
||||||
cpool: reg_fn.cpool,
|
cpool: reg_fn.cpool,
|
||||||
code: CodeKind::Code(new),
|
code: CodeKind::Code(new),
|
||||||
|
|||||||
+279
-188
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||||
regify::RegInstr,
|
regify::{Block, RegInstr},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[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
|
/// 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.
|
/// too and `put_loc` / `get_loc` just copy between the two.
|
||||||
pub types: Vec<Type>,
|
pub types: Vec<Type>,
|
||||||
|
pub blocks: Vec<Block>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The lattice, joining what a value already holds with what it is being
|
/// 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<InferFn>) -> Type {
|
|||||||
// The types move out too, so a callee can be inferred through `fns` while
|
// 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.
|
// 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 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::<Vec<usize>>();
|
||||||
|
|
||||||
|
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;
|
let mut ret = Type::Unknown;
|
||||||
|
|
||||||
for instr in &code {
|
// What each block is entered with, joined from the blocks that reach it.
|
||||||
// regify does not rename locals, so a load just names the local's value
|
let mut entry = vec![vec![Type::Unknown; types.len()]; starts.len()];
|
||||||
// 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 {
|
for (b, &start) in starts.iter().enumerate() {
|
||||||
// ── literals ────────────────────────────────────────────────
|
let end = starts.get(b + 1).copied().unwrap_or(code.len());
|
||||||
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,
|
// Fold in what the blocks reaching this one left behind. Two paths
|
||||||
OpCode::Undefined => Type::Undefined,
|
// meeting is the ONLY place the algebra applies -- everything below
|
||||||
OpCode::Object => Type::Empty,
|
// 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 => {
|
// A slot at a merge keeps the ids the first path in left, so every
|
||||||
match instr.imm {
|
// other path's value has to be folded into them. Without this the
|
||||||
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
|
// merge reads one arm and believes it.
|
||||||
Some(JsValue::Int(_)) => Type::Int,
|
for &(slot, incoming) in &blocks[b].phi {
|
||||||
Some(JsValue::Float(_)) => Type::Float,
|
types[slot] = join(&types[slot], &types[incoming]);
|
||||||
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
|
}
|
||||||
Some(JsValue::Undefined) => Type::Undefined,
|
|
||||||
Some(&JsValue::FnById(id)) => Type::Fn(id),
|
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,
|
||||||
},
|
|
||||||
_ => 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 ──────────────────────────────────────────────
|
// ── field reads ─────────────────────────────────────────────
|
||||||
// A float anywhere makes the result a float; anything else can be
|
// Walk the shape term from the outside in: the newest write of the
|
||||||
// a string concatenation or an object coercion, so it is Any.
|
// field wins, which is what makes a later write shadow an earlier.
|
||||||
OpCode::Add | OpCode::Sub | OpCode::Mul | OpCode::Div | OpCode::Mod | OpCode::Pow => {
|
OpCode::GetField => {
|
||||||
match (&types[instr.args[0]], &types[instr.args[1]]) {
|
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::Int, Type::Int) => Type::Int,
|
||||||
(Type::Float, Type::Float)
|
(Type::Float, Type::Float)
|
||||||
| (Type::Int, Type::Float)
|
| (Type::Int, Type::Float)
|
||||||
| (Type::Float, Type::Int) => Type::Float,
|
| (Type::Float, Type::Int) => Type::Float,
|
||||||
_ => Type::Any,
|
_ => Type::Any,
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => match types[instr.args[0]] {
|
OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => {
|
||||||
Type::Int => Type::Int,
|
match types[instr.args[0]] {
|
||||||
Type::Float => Type::Float,
|
Type::Int => Type::Int,
|
||||||
_ => Type::Any,
|
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,
|
}
|
||||||
|
|
||||||
|
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 ───────────────────────────────────────────────
|
// Hand the state on to every block this one reaches.
|
||||||
OpCode::GetArg0 => args.first().cloned().unwrap_or(Type::Undefined),
|
for &s in &blocks[b].succ {
|
||||||
OpCode::GetArg1 => args.get(1).cloned().unwrap_or(Type::Undefined),
|
for (e, t) in entry[s].iter_mut().zip(&types) {
|
||||||
OpCode::GetArg2 => args.get(2).cloned().unwrap_or(Type::Undefined),
|
*e = join(e, t);
|
||||||
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].types = types;
|
||||||
|
fns[fn_id].blocks = blocks;
|
||||||
fns[fn_id].code = CodeKind::Code(code);
|
fns[fn_id].code = CodeKind::Code(code);
|
||||||
fns[fn_id].sigs[0].ret = ret.clone();
|
fns[fn_id].sigs[0].ret = ret.clone();
|
||||||
|
|
||||||
|
|||||||
+246
-78
@@ -2,11 +2,15 @@ use std::fmt::Debug;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
flatten::{Capture, FlatFn},
|
flatten::{Capture, FlatFn},
|
||||||
parse::{ImmOperand, JsValue, opcodes::OpCode},
|
parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RegInstr {
|
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<usize>,
|
pub dsts: Vec<usize>,
|
||||||
pub op: OpCode,
|
pub op: OpCode,
|
||||||
pub imm: ImmOperand,
|
pub imm: ImmOperand,
|
||||||
@@ -16,24 +20,76 @@ pub struct RegInstr {
|
|||||||
impl RegInstr {
|
impl RegInstr {
|
||||||
pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> Self {
|
pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
block_start: false,
|
||||||
dsts,
|
dsts,
|
||||||
op,
|
op,
|
||||||
imm,
|
imm,
|
||||||
args,
|
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 {
|
impl Debug for RegInstr {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
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<usize>,
|
||||||
|
|
||||||
|
/// 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<Vec<usize>>,
|
||||||
|
|
||||||
|
/// `(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<usize> {
|
||||||
|
self.entry.clone().unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RegFn {
|
pub struct RegFn {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -41,104 +97,132 @@ pub struct RegFn {
|
|||||||
pub captures: Vec<Capture>,
|
pub captures: Vec<Capture>,
|
||||||
pub cpool: Vec<JsValue>,
|
pub cpool: Vec<JsValue>,
|
||||||
pub code: Vec<RegInstr>,
|
pub code: Vec<RegInstr>,
|
||||||
|
/// 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<Block>,
|
||||||
next_val: usize,
|
next_val: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegFn {
|
impl RegFn {
|
||||||
fn from_flat(flat_fn: FlatFn) -> Self {
|
fn from_flat(flat_fn: FlatFn) -> Self {
|
||||||
let mut stack = vec![];
|
let pcs = flat_fn.code.iter().map(|i| i.pc).collect::<Vec<usize>>();
|
||||||
|
let (starts, mut blocks) = find_basic_blocks(&flat_fn.code, &pcs);
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(flat_fn.code.len());
|
let mut output = Vec::with_capacity(flat_fn.code.len());
|
||||||
let mut next_val = flat_fn.var_count; // Reserve values for locals
|
let mut next_val = flat_fn.var_count; // Reserve values for locals
|
||||||
|
|
||||||
let mut fresh_val = || {
|
for b in 0..blocks.len() {
|
||||||
let v = next_val;
|
// A block starts with the stack it is ENTERED with, not with
|
||||||
next_val += 1;
|
// whatever the block above it in the listing left behind. This one
|
||||||
v
|
// 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 fresh_val = || {
|
||||||
let mut reg_instr = RegInstr {
|
let v = next_val;
|
||||||
dsts: vec![],
|
next_val += 1;
|
||||||
op: instr.op,
|
v
|
||||||
imm: instr.imm_operands,
|
|
||||||
args: vec![],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let extra_pop = match instr.op {
|
let end = starts.get(b + 1).copied().unwrap_or(flat_fn.code.len());
|
||||||
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 info = instr.op.info();
|
for instr in &flat_fn.code[starts[b]..end] {
|
||||||
let n_pop = info.n_pop as usize + extra_pop;
|
let mut reg_instr = RegInstr {
|
||||||
let n_push = info.n_push as usize;
|
block_start: false,
|
||||||
|
dsts: vec![],
|
||||||
|
op: instr.op,
|
||||||
|
imm: instr.imm_operands,
|
||||||
|
args: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
for _ in 0..n_pop {
|
let extra_pop = match instr.op {
|
||||||
let v = stack.pop().unwrap();
|
OpCode::Call0 => 0,
|
||||||
reg_instr.args.push(v);
|
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,
|
let info = instr.op.info();
|
||||||
// the id reserved above. A load pushes it, a store defines it again,
|
let n_pop = info.n_pop as usize + extra_pop;
|
||||||
// and neither makes a new value -- so a local keeps one identity
|
let n_push = info.n_push as usize;
|
||||||
// however many times it is read and written.
|
|
||||||
let local = match instr.op {
|
for _ in 0..n_pop {
|
||||||
OpCode::GetLoc0 | OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0),
|
let v = stack.pop().unwrap();
|
||||||
OpCode::GetLoc1 | OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1),
|
reg_instr.args.push(v);
|
||||||
OpCode::GetLoc2 | OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2),
|
}
|
||||||
OpCode::GetLoc3 | OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3),
|
|
||||||
|
// 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,
|
||||||
},
|
};
|
||||||
|
|
||||||
_ => None,
|
match local {
|
||||||
};
|
Some(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);
|
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 => {
|
reg_instr.block_start = std::mem::take(&mut first);
|
||||||
for _ in 0..n_push {
|
output.push(reg_instr);
|
||||||
let v = fresh_val();
|
|
||||||
stack.push(v);
|
|
||||||
reg_instr.dsts.push(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
Self {
|
||||||
@@ -147,6 +231,7 @@ impl RegFn {
|
|||||||
captures: flat_fn.captures,
|
captures: flat_fn.captures,
|
||||||
cpool: flat_fn.cpool,
|
cpool: flat_fn.cpool,
|
||||||
code: output,
|
code: output,
|
||||||
|
blocks,
|
||||||
next_val,
|
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<usize>, Vec<Block>) {
|
||||||
|
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<usize> {
|
||||||
|
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<FlatFn>) -> Vec<RegFn> {
|
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
||||||
flat_fns.into_iter().map(RegFn::from_flat).collect()
|
flat_fns.into_iter().map(RegFn::from_flat).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user