Big Rewrite

This commit is contained in:
2026-09-11 13:36:57 +00:00
parent f8b1ef045e
commit 465d9095bb
16 changed files with 773 additions and 1902 deletions
+135 -191
View File
@@ -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()