Mid rewrite

This commit is contained in:
UditDey
2026-10-13 11:52:02 +00:00
parent 2f21347088
commit d0c2f291e9
24 changed files with 752 additions and 3527 deletions
+21 -354
View File
@@ -1,25 +1,34 @@
mod basic_block;
mod walk;
use crate::{
flatten::FlatFn,
libqjs::parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
};
pub use basic_block::BasicBlock;
use basic_block::find_basic_blocks;
use walk::walk_block;
#[derive(Debug, Clone, Copy)]
pub enum Operand {
Local(usize),
Arg(usize),
Reg(usize),
}
#[derive(Debug, Clone)]
pub struct RegInstr {
pub dsts: Vec<usize>,
pub dsts: Vec<Operand>,
pub op: OpCode,
pub imm: ImmOperand,
pub args: Vec<usize>,
pub args: Vec<Operand>,
}
impl RegInstr {
fn nop() -> Self {
Self {
dsts: vec![], // TODO: do we ever have more than one dst?
dsts: vec![],
op: OpCode::Nop,
imm: ImmOperand::None,
args: vec![],
@@ -30,10 +39,8 @@ impl RegInstr {
#[derive(Debug)]
pub struct RegFn {
pub arg_count: usize,
pub var_count: usize,
/// Locals and args are not renamed: local `k` is value id `k`, and arg `k`
/// is value id `var_count + k`.
pub val_count: usize,
pub local_count: usize,
pub reg_count: usize,
pub cpool: Vec<JsValue>,
pub code: Vec<RegInstr>,
pub blocks: Vec<BasicBlock>,
@@ -41,18 +48,12 @@ pub struct RegFn {
impl RegFn {
fn from_flat(f: FlatFn) -> Self {
let mut blocks = find_basic_blocks(&f.code);
let blocks = find_basic_blocks(&f.code);
let mut out = vec![RegInstr::nop(); f.code.len()];
// Reserve values for locals, then args
let mut next_val = f.var_count + f.arg_count;
// The stack each block recieves at entry from its predecessors
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
// How many blocks lead to each block
let mut n_preds = vec![0; blocks.len()];
blocks.iter().flat_map(|b| &b.succ).for_each(|&s| n_preds[s] += 1);
let mut entry_stacks: Vec<Option<Vec<Operand>>> = vec![None; blocks.len()];
let mut next_reg = 0;
// Walk blocks in CFG order, starting from block 0
let mut worklist = vec![];
@@ -65,348 +66,14 @@ impl RegFn {
walk_block(
&blocks[block_idx],
&f.code,
f.var_count,
&mut stack,
&mut next_val,
&mut next_reg,
&mut out,
);
for i in 0..blocks[block_idx].succ.len() {
let succ_idx = blocks[block_idx].succ[i];
// A join's stack slots are new values, which every block that
// leads to it copies its own values into. Naming a slot after
// one predecessor's value would give that value a second
// meaning (this does the job of phi nodes in SSA)
let succ_stack = match &entry_stacks[succ_idx] {
Some(succ_stack) => succ_stack.clone(),
None => {
let succ_stack: Vec<usize> = match n_preds[succ_idx] {
1 => stack.clone(),
_ => stack.iter().map(|_| fresh_val(&mut next_val)).collect(),
};
entry_stacks[succ_idx] = Some(succ_stack.clone());
worklist.push(succ_idx);
succ_stack
}
};
let copies = edge_copies(&stack, &succ_stack);
if copies.is_empty() {
continue;
}
// The copies belong to this one edge. A block that can go
// elsewhere too gets them on a block of their own on this
// edge, so the other edge does not run them
if blocks[block_idx].succ.len() == 1 {
blocks[block_idx].postfix.extend(copies);
} else {
let at = blocks[succ_idx].start;
blocks.push(BasicBlock {
start: at,
end: at,
succ: vec![succ_idx],
postfix: copies,
});
blocks[block_idx].succ[i] = blocks.len() - 1;
}
}
// TODO: handle sucessors
}
// A define_field into an object that no op of this fn made (a class
// field on `this`, a static field on the class) is a write like any
// other: a put_field. So a define_field is always a write into an
// object before it goes anywhere, and its field is never absent (see
// `Cell::write()` in infer)
let mut created = vec![false; next_val];
for instr in &out {
if matches!(instr.op, OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8) {
created[instr.dsts[0]] = true;
}
}
for instr in &mut out {
if instr.op == OpCode::DefineField && !created[instr.args[1]] {
instr.op = OpCode::PutField;
}
}
Self {
arg_count: f.arg_count,
var_count: f.var_count,
val_count: next_val,
cpool: f.cpool,
code: out,
blocks,
}
todo!()
}
}
/// Walk a basic block's instructions, converting each instr to register form
fn walk_block(
block: &BasicBlock,
code: &[Instr],
var_count: usize,
stack: &mut Vec<usize>,
next_val: &mut usize,
out: &mut [RegInstr],
) {
let mut fresh_val = || fresh_val(next_val);
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,
args: vec![],
};
let info = instr.op.info();
let n_pop = info.n_pop as usize + extra_pops(instr);
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);
}
if matches!(
instr.op,
OpCode::Call | OpCode::CallMethod | OpCode::CallConstructor
) {
out_instr.args = call_operands(instr.op, out_instr.args);
}
// Locals and args are not renamed, so their read/writes need special handling
// Handle read of a local or arg
if let Some(var) = is_var_read(instr, var_count) {
let v = fresh_val();
stack.push(v);
out_instr.op = OpCode::SetLoc;
out_instr.imm = ImmOperand::None;
out_instr.dsts.push(v);
out_instr.args.push(var);
out[instr_idx] = out_instr;
continue;
}
// Handle write of a local or arg value
if let Some(var) = is_var_write(instr, var_count) {
out_instr.op = OpCode::SetLoc;
out_instr.imm = ImmOperand::None;
out_instr.dsts.push(var);
// set_loc also leaves the value it stored on the stack.
if n_push == 1 {
stack.push(out_instr.args[0]); // args set by previos pops loop
}
out[instr_idx] = out_instr;
continue;
}
// Some more special cases:
// set_home_object binds [[HomeObject]], which only `super` reads,
// `super` is unsupported currently, It returns both its operands unchanged
if instr.op == OpCode::SetHomeObject {
stack.push(out_instr.args[1]);
stack.push(out_instr.args[0]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
// Handle `Dup`, `Dup2`, `Swap` and `ToPropkey2`, which just push values
if instr.op == OpCode::Dup {
stack.push(out_instr.args[0]);
stack.push(out_instr.args[0]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
if instr.op == OpCode::Dup2 {
let (a, b) = (out_instr.args[1], out_instr.args[0]);
stack.extend([a, b, a, b]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
// to_propkey2 makes the key on top a property key, and keeps the
// object under it. A key is a number, which it keeps as it is (see
// infer), so it only gives both back
if instr.op == OpCode::ToPropkey2 {
stack.push(out_instr.args[1]);
stack.push(out_instr.args[0]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
if instr.op == OpCode::Swap {
stack.push(out_instr.args[0]);
stack.push(out_instr.args[1]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
// define_field leaves the object it wrote on the stack: the same value,
// not a new one
if instr.op == OpCode::DefineField {
stack.push(out_instr.args[1]);
out[instr_idx] = out_instr;
continue;
}
// Normal case: handle this instr's pushes
for _ in 0..n_push {
let v = fresh_val();
stack.push(v);
out_instr.dsts.push(v);
}
out[instr_idx] = out_instr;
}
}
/// Some instrs have extra pops not reflected by instr.op.info().n_pop
fn extra_pops(instr: &Instr) -> usize {
match instr.imm {
ImmOperand::Npop(npop) => npop as usize,
ImmOperand::NpopU16 { npop, .. } => npop as usize,
_ => 0,
}
}
/// Put a call's popped operands in the order of the callee's params: the
/// callee (which is its env), `this`, then the args in source order
///
/// `call_method` has `this` at the bottom of its frame, below the callee.
/// `call_constructor` has the callee at the bottom and `this` above it (see
/// transform). `call` has `this` on top, where transform pushed it
fn call_operands(op: OpCode, mut popped: Vec<usize>) -> Vec<usize> {
let (callee, this) = match op {
OpCode::CallMethod => {
let this = popped.pop().unwrap();
(popped.pop().unwrap(), this)
}
OpCode::CallConstructor => {
let callee = popped.pop().unwrap();
(callee, popped.pop().unwrap())
}
_ => {
let callee = popped.pop().unwrap();
(callee, popped.remove(0))
}
};
// What remains are the args, last arg first
popped.reverse();
[callee, this].into_iter().chain(popped).collect()
}
/// The value id of the local or arg that `instr` reads, if any.
pub fn is_var_read(instr: &Instr, var_count: usize) -> Option<usize> {
let arg = |k: usize| Some(var_count + k);
match instr.op {
OpCode::GetLoc0 => Some(0),
OpCode::GetLoc1 => Some(1),
OpCode::GetLoc2 => Some(2),
OpCode::GetLoc3 => Some(3),
OpCode::GetLoc | OpCode::GetLoc8 | OpCode::GetLocCheck => match instr.imm {
ImmOperand::Loc(k) => Some(k as usize),
_ => unreachable!(),
},
OpCode::GetArg0 => arg(0),
OpCode::GetArg1 => arg(1),
OpCode::GetArg2 => arg(2),
OpCode::GetArg3 => arg(3),
OpCode::GetArg => match instr.imm {
ImmOperand::Arg(k) => arg(k as usize),
_ => unreachable!(),
},
_ => None,
}
}
/// The value id of the local or arg that `instr` writes, if any.
pub fn is_var_write(instr: &Instr, var_count: usize) -> Option<usize> {
let arg = |k: usize| Some(var_count + k);
match instr.op {
OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0),
OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1),
OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2),
OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3),
OpCode::PutLoc
| OpCode::PutLoc8
| OpCode::PutLocCheck
| OpCode::PutLocCheckInit
| OpCode::SetLoc
| OpCode::SetLoc8 => match instr.imm {
ImmOperand::Loc(k) => Some(k as usize),
_ => unreachable!(),
},
OpCode::PutArg0 | OpCode::SetArg0 => arg(0),
OpCode::PutArg1 | OpCode::SetArg1 => arg(1),
OpCode::PutArg2 | OpCode::SetArg2 => arg(2),
OpCode::PutArg3 | OpCode::SetArg3 => arg(3),
OpCode::PutArg | OpCode::SetArg => match instr.imm {
ImmOperand::Arg(k) => arg(k as usize),
_ => unreachable!(),
},
_ => None,
}
}
/// The copies that carry the values on a block's stack into its successor's
/// entry stack
fn edge_copies(stack: &[usize], succ_stack: &[usize]) -> Vec<RegInstr> {
let overlap = stack.iter().rev().zip(succ_stack.iter().rev());
overlap
.filter(|(val, succ_val)| val != succ_val)
.map(|(&val, &succ_val)| RegInstr {
dsts: vec![succ_val],
op: OpCode::SetLoc,
imm: ImmOperand::None,
args: vec![val],
})
.collect()
}
fn fresh_val(next_val: &mut usize) -> usize {
let v = *next_val;
*next_val += 1;
v
}
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
flat_fns.into_iter().map(RegFn::from_flat).collect()
}