Big Rewrite
This commit is contained in:
@@ -1,91 +1,106 @@
|
||||
//! 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.
|
||||
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
|
||||
|
||||
use crate::parse::{ImmOperand, Instr, opcodes::OpCode};
|
||||
type BlockId = usize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Block {
|
||||
/// The blocks control can reach from this one.
|
||||
pub succ: 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)>,
|
||||
pub struct BasicBlock {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub succ: Vec<BlockId>,
|
||||
}
|
||||
|
||||
pub fn find_basic_blocks(code: &[Instr], pcs: &[usize]) -> (Vec<usize>, Vec<Block>) {
|
||||
let mut starts = vec![0];
|
||||
impl BasicBlock {
|
||||
fn new(start: usize) -> Self {
|
||||
Self {
|
||||
start,
|
||||
end: 0,
|
||||
succ: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (at, instr) in code.iter().enumerate() {
|
||||
if let Some(target) = jump_target(instr.imm_operands, pcs) {
|
||||
starts.push(target);
|
||||
pub fn find_basic_blocks(code: &[Instr]) -> Vec<BasicBlock> {
|
||||
// Find block starts: the first instruction, every jump target,
|
||||
// and whatever follows a jump or a return
|
||||
let mut blocks = vec![BasicBlock::new(0)];
|
||||
|
||||
for (i, instr) in code.iter().enumerate() {
|
||||
// If cond/uncond jump, jump target is a start, next instr (if exists) is another start
|
||||
if let Some(target) = is_jump_instr(instr, code) {
|
||||
blocks.push(BasicBlock::new(target));
|
||||
|
||||
if i + 1 < code.len() {
|
||||
blocks.push(BasicBlock::new(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
if ends_block(instr, pcs) && at + 1 < code.len() {
|
||||
starts.push(at + 1);
|
||||
// If terminal, next instr (if exists) is a start
|
||||
if is_terminal_instr(instr) {
|
||||
if i + 1 < code.len() {
|
||||
blocks.push(BasicBlock::new(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
starts.sort_unstable();
|
||||
starts.dedup();
|
||||
blocks.sort_unstable_by_key(|b| b.start);
|
||||
blocks.dedup_by_key(|b| b.start);
|
||||
|
||||
let ends = |i: usize| starts.get(i + 1).copied().unwrap_or(code.len());
|
||||
// Populate block end indices
|
||||
for i in 0..blocks.len() {
|
||||
blocks[i].end = blocks.get(i + 1).map(|b| b.start).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![];
|
||||
// Find block successors
|
||||
for i in 0..blocks.len() {
|
||||
let end = blocks[i].end;
|
||||
let last_instr = &code[end - 1];
|
||||
|
||||
if let Some(t) = jump_target(last.imm_operands, pcs) {
|
||||
succ.extend(starts.iter().position(|&s| s == t));
|
||||
}
|
||||
// Jump targets are a successor
|
||||
if let Some(target) = is_jump_instr(last_instr, code) {
|
||||
let succ_block = blocks.iter().position(|b| b.start == target).unwrap();
|
||||
blocks[i].succ.push(succ_block);
|
||||
}
|
||||
|
||||
if falls_through(last.op) && i + 1 < starts.len() {
|
||||
succ.push(i + 1);
|
||||
}
|
||||
// Fall through blocks are a successor
|
||||
if falls_through(last_instr) && i + 1 < blocks.len() {
|
||||
blocks[i].succ.push(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Block { succ, phi: vec![] }
|
||||
})
|
||||
.collect();
|
||||
|
||||
(starts, blocks)
|
||||
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()
|
||||
fn falls_through(instr: &Instr) -> bool {
|
||||
use OpCode::*;
|
||||
let uncond_jump = matches!(instr.op, Goto | Goto8 | Goto16);
|
||||
|
||||
!uncond_jump && !is_terminal_instr(instr)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
fn is_jump_instr(instr: &Instr, code: &[Instr]) -> Option<usize> {
|
||||
use OpCode::*;
|
||||
|
||||
let is_jump = matches!(
|
||||
instr.op,
|
||||
Goto | Goto8 | Goto16 | IfFalse | IfTrue | IfFalse8 | IfTrue8
|
||||
);
|
||||
|
||||
if !is_jump {
|
||||
return None;
|
||||
}
|
||||
|
||||
let target = match instr.imm_operands {
|
||||
ImmOperand::Label { target } => code.iter().position(|x| x.pc == target).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
pcs.iter().position(|&pc| pc == target)
|
||||
Some(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
|
||||
fn is_terminal_instr(instr: &Instr) -> bool {
|
||||
use OpCode::*;
|
||||
matches!(
|
||||
instr.op,
|
||||
Return | ReturnUndef | ReturnAsync | Throw | TailCall | TailCallMethod | Ret
|
||||
)
|
||||
}
|
||||
|
||||
+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