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
+80 -65
View File
@@ -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
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()