Big Rewrite
This commit is contained in:
+135
-191
@@ -1,21 +1,15 @@
|
||||
mod basic_block;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
flatten::{Capture, FlatFn},
|
||||
parse::{ImmOperand, JsValue, opcodes::OpCode},
|
||||
flatten::FlatFn,
|
||||
libqjs::parse::{AtomRef, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode},
|
||||
};
|
||||
|
||||
pub use basic_block::Block;
|
||||
pub use basic_block::BasicBlock;
|
||||
use basic_block::find_basic_blocks;
|
||||
|
||||
#[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<usize>,
|
||||
pub op: OpCode,
|
||||
pub imm: ImmOperand,
|
||||
@@ -23,231 +17,181 @@ pub struct RegInstr {
|
||||
}
|
||||
|
||||
impl RegInstr {
|
||||
pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> Self {
|
||||
fn nop() -> Self {
|
||||
Self {
|
||||
block_start: false,
|
||||
dsts,
|
||||
op,
|
||||
imm,
|
||||
args,
|
||||
dsts: vec![],
|
||||
op: OpCode::Nop,
|
||||
imm: ImmOperand::None,
|
||||
args: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 std::fmt::Debug for RegInstr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {:?} = {:?} {:?} {:?}",
|
||||
if self.block_start { "|" } else { " " },
|
||||
self.dsts,
|
||||
self.op,
|
||||
self.imm,
|
||||
self.args
|
||||
"{:?} = {:?} {:?} {:?}",
|
||||
self.dsts, self.op, self.imm, self.args
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RegFn {
|
||||
pub name: String,
|
||||
pub name: AtomRef,
|
||||
pub arg_count: usize,
|
||||
pub captures: Vec<Capture>,
|
||||
pub val_count: usize,
|
||||
pub cpool: Vec<JsValue>,
|
||||
pub captures: Vec<ClosureVar>,
|
||||
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,
|
||||
pub blocks: Vec<BasicBlock>,
|
||||
}
|
||||
|
||||
impl RegFn {
|
||||
fn from_flat(flat_fn: FlatFn) -> Self {
|
||||
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 blocks = find_basic_blocks(&flat_fn.code);
|
||||
let mut out = vec![RegInstr::nop(); flat_fn.code.len()];
|
||||
|
||||
let mut next_val = flat_fn.var_count; // Reserve values for locals
|
||||
|
||||
// One instruction in, one out, so a block's output lands exactly where
|
||||
// its input was and blocks can be walked in any order. A block nothing
|
||||
// reaches keeps its nops, which is what unreachable code should leave.
|
||||
let mut output =
|
||||
vec![RegInstr::new(OpCode::Nop, ImmOperand::None, vec![], vec![]); flat_fn.code.len()];
|
||||
|
||||
// A worklist, because listing order does not reach every block: a
|
||||
// for-of puts its body BEFORE the test, so the body is entered only by
|
||||
// the back edge, from a block further down. A block joins the list the
|
||||
// once, when the first path arrives with a stack for it. The function
|
||||
// itself is entered at block 0, holding nothing.
|
||||
let mut entry_stack = vec![None; blocks.len()];
|
||||
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
|
||||
let mut worklist = vec![];
|
||||
|
||||
entry_stack[0] = Some(vec![]);
|
||||
entry_stacks[0] = Some(vec![]);
|
||||
worklist.push(0);
|
||||
|
||||
while let Some(b) = worklist.pop() {
|
||||
// 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 = entry_stack[b].clone().unwrap_or_default();
|
||||
while let Some(block_idx) = worklist.pop() {
|
||||
let mut stack: Vec<usize> = entry_stacks[block_idx].clone().unwrap_or_default();
|
||||
|
||||
let mut fresh_val = || {
|
||||
let v = next_val;
|
||||
next_val += 1;
|
||||
v
|
||||
};
|
||||
walk_block(
|
||||
&blocks[block_idx],
|
||||
&flat_fn.code,
|
||||
&mut stack,
|
||||
&mut next_val,
|
||||
&mut out,
|
||||
);
|
||||
|
||||
let end = starts.get(b + 1).copied().unwrap_or(flat_fn.code.len());
|
||||
|
||||
for at in starts[b]..end {
|
||||
let instr = &flat_fn.code[at];
|
||||
|
||||
let mut reg_instr = RegInstr {
|
||||
block_start: false,
|
||||
dsts: vec![],
|
||||
op: instr.op,
|
||||
imm: instr.imm_operands,
|
||||
args: vec![],
|
||||
};
|
||||
|
||||
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 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,
|
||||
},
|
||||
|
||||
_ => 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 {
|
||||
let v = fresh_val();
|
||||
stack.push(v);
|
||||
reg_instr.dsts.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output[at] = reg_instr;
|
||||
}
|
||||
|
||||
// Every edge out of here arrives at its target with this stack.
|
||||
for i in 0..blocks[b].succ.len() {
|
||||
let to = blocks[b].succ[i];
|
||||
|
||||
// A later path is reconciled against the names the first one
|
||||
// left, and that pairing is the phi.
|
||||
if let Some(slots) = &entry_stack[to] {
|
||||
blocks[to]
|
||||
.phi
|
||||
.extend(slots.iter().copied().zip(stack.iter().copied()));
|
||||
}
|
||||
// The first path names the block's slots, and is the one that
|
||||
// puts it on the worklist.
|
||||
else {
|
||||
entry_stack[to] = Some(stack.clone());
|
||||
worklist.push(to);
|
||||
for &succ_idx in &blocks[block_idx].succ {
|
||||
if entry_stacks[succ_idx].is_none() {
|
||||
entry_stacks[succ_idx] = Some(stack.clone());
|
||||
worklist.push(succ_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every block begins where its first instruction sits, walked or not.
|
||||
for &start in &starts {
|
||||
output[start].block_start = true;
|
||||
}
|
||||
|
||||
Self {
|
||||
name: flat_fn.name,
|
||||
arg_count: flat_fn.arg_count,
|
||||
captures: flat_fn.captures,
|
||||
val_count: next_val,
|
||||
cpool: flat_fn.cpool,
|
||||
code: output,
|
||||
captures: flat_fn.captures,
|
||||
code: out,
|
||||
blocks,
|
||||
next_val,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fresh_val(&mut self) -> usize {
|
||||
let v = self.next_val;
|
||||
self.next_val += 1;
|
||||
v
|
||||
}
|
||||
|
||||
pub fn num_vals(&self) -> usize {
|
||||
self.next_val
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn walk_block(
|
||||
block: &BasicBlock,
|
||||
code: &[Instr],
|
||||
stack: &mut Vec<usize>,
|
||||
next_val: &mut usize,
|
||||
out: &mut [RegInstr],
|
||||
) {
|
||||
let mut fresh_val = || {
|
||||
let v = *next_val;
|
||||
*next_val += 1;
|
||||
v
|
||||
};
|
||||
|
||||
for instr_idx in block.start..block.end {
|
||||
let instr = &code[instr_idx];
|
||||
|
||||
let mut out_instr = RegInstr {
|
||||
dsts: vec![],
|
||||
op: instr.op,
|
||||
imm: instr.imm_operands,
|
||||
args: vec![],
|
||||
};
|
||||
|
||||
// Some instrs have extra pops not reflected in info.n_pop
|
||||
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 info = instr.op.info();
|
||||
let n_pop = info.n_pop as usize + extra_pop;
|
||||
let n_push = info.n_push as usize;
|
||||
|
||||
// Handle this instr's pops
|
||||
for _ in 0..n_pop {
|
||||
let v = stack.pop().unwrap();
|
||||
out_instr.args.push(v);
|
||||
}
|
||||
|
||||
// Locals are not renamed, needs special handling
|
||||
let local_access = 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 instr.imm_operands {
|
||||
ImmOperand::Loc(k) => Some(k as usize),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Handle this instr's pushes, handling local accesses separately
|
||||
match local_access {
|
||||
Some(loc) => {
|
||||
out_instr.dsts.push(loc);
|
||||
|
||||
if n_push == 1 {
|
||||
stack.push(loc);
|
||||
}
|
||||
}
|
||||
|
||||
// The one op that reads two locals at once
|
||||
None if instr.op == OpCode::GetLoc0Loc1 => {
|
||||
for k in [0, 1] {
|
||||
stack.push(k);
|
||||
out_instr.dsts.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
for _ in 0..n_push {
|
||||
let v = fresh_val();
|
||||
stack.push(v);
|
||||
out_instr.dsts.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out[instr_idx] = out_instr;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
||||
flat_fns.into_iter().map(RegFn::from_flat).collect()
|
||||
|
||||
Reference in New Issue
Block a user