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()
}
+259
View File
@@ -0,0 +1,259 @@
//! This module converts one basic block's instrs from stack to register form using a simulated stack
//!
//! The output reg form instrs are structured as:
//! `dsts = op(imm, args)`
//!
//! Every instr's pushes allocates new register operands which are pushed on the stack and form the
//! output instr's `dsts`, the instr's pops pop operands from the stack and form the output instr's
//! `args`
//!
//! Each instr's `op` and `imm` is preserved except for some special cases
//!
//! `set_loc` is henceforth used to move data from one operand (local, arg, reg) to another
#![allow(non_snake_case)]
use super::{BasicBlock, Operand, RegInstr};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// Walk a basic block's instructions, converting each instr to register form
pub fn walk_block(
block: &BasicBlock,
code: &[Instr],
stack: &mut Vec<Operand>,
next_reg: &mut usize,
out: &mut [RegInstr],
) {
let mut fresh_reg = || {
let r = *next_reg;
*next_reg += 1;
Operand::Reg(r)
};
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
let out_instr = &mut out[instr_idx];
if try_handle_named_read(instr, stack, &mut fresh_reg, out_instr) {
continue;
}
if try_handle_named_write(instr, stack, out_instr) {
continue;
}
if try_handle_misc(instr, stack, out_instr) {
continue;
}
handle_normal_case(instr, stack, &mut fresh_reg, out_instr);
}
}
/// Handles a "named read" i.e. a read of a local or arg (and not a stack value)
///
/// We simulate pushing the operand to stack, and replace the instr with `SetLoc`, which
/// is our catch-all for a data-move instr
fn try_handle_named_read(
instr: &Instr,
stack: &mut Vec<Operand>,
mut fresh_reg: impl FnMut() -> Operand,
out: &mut RegInstr,
) -> bool {
use OpCode::*;
let arg = match instr.op {
GetLoc0 => Operand::Local(0),
GetLoc1 => Operand::Local(1),
GetLoc2 => Operand::Local(2),
GetLoc3 => Operand::Local(3),
GetLoc | GetLoc8 | GetLocCheck => match instr.imm {
ImmOperand::Loc(k) => Operand::Local(k as usize),
_ => unreachable!(),
},
GetArg0 => Operand::Arg(0),
GetArg1 => Operand::Arg(1),
GetArg2 => Operand::Arg(2),
GetArg3 => Operand::Arg(3),
GetArg => match instr.imm {
ImmOperand::Arg(k) => Operand::Arg(k as usize),
_ => unreachable!(),
},
_ => return false,
};
let dst = fresh_reg();
// Converted SetLoc, reading from arg, writing to dst
*out = RegInstr {
dsts: vec![dst],
op: SetLoc,
imm: ImmOperand::None,
args: vec![arg],
};
// Simulate stack push
stack.push(dst);
true
}
/// Handles a "named write" i.e. a write of a local or arg (and not a stack value)
///
/// We simulate popping the operand off stack, and replace the instr with `SetLoc`, which
/// is our catch-all for a data-move instr
///
/// A distinction needs to be made between `Set*` and `Put*` since `Set*` instrs leave
/// the value-to-write on the stack and `Put*` pops it off
fn try_handle_named_write(instr: &Instr, stack: &mut Vec<Operand>, out: &mut RegInstr) -> bool {
use OpCode::*;
let (dst, preserved) = match instr.op {
PutLoc0 => (Operand::Local(0), false),
PutLoc1 => (Operand::Local(1), false),
PutLoc2 => (Operand::Local(2), false),
PutLoc3 => (Operand::Local(3), false),
PutLoc | PutLoc8 | PutLocCheck | PutLocCheckInit => match instr.imm {
ImmOperand::Loc(k) => (Operand::Local(k as usize), false),
_ => unreachable!(),
},
PutArg0 => (Operand::Arg(0), false),
PutArg1 => (Operand::Arg(1), false),
PutArg2 => (Operand::Arg(2), false),
PutArg3 => (Operand::Arg(3), false),
SetLoc0 => (Operand::Local(0), true),
SetLoc1 => (Operand::Local(1), true),
SetLoc2 => (Operand::Local(2), true),
SetLoc3 => (Operand::Local(3), true),
SetLoc | SetLoc8 => match instr.imm {
ImmOperand::Loc(k) => (Operand::Local(k as usize), true),
_ => unreachable!(),
},
SetArg0 => (Operand::Arg(0), true),
SetArg1 => (Operand::Arg(1), true),
SetArg2 => (Operand::Arg(2), true),
SetArg3 => (Operand::Arg(3), true),
_ => return false,
};
// Simulate stack pop or peek, depending on instr
let arg = if preserved {
stack.last().unwrap().clone()
} else {
stack.pop().unwrap()
};
// Converted SetLoc, reading from arg, writing to dst
*out = RegInstr {
dsts: vec![dst],
op: SetLoc,
imm: ImmOperand::None,
args: vec![arg],
};
true
}
fn try_handle_misc(instr: &Instr, stack: &mut Vec<Operand>, out_instr: &mut RegInstr) -> bool {
use OpCode::*;
*out_instr = match instr.op {
// Duplicate stack top, out_instr becomes `nop`
Dup => {
let top = stack[stack.len() - 1].clone();
stack.push(top);
RegInstr {
dsts: vec![],
op: Nop,
imm: ImmOperand::None,
args: vec![],
}
}
// Duplicate the top 2 elems in stack, out_instr becomes `nop`
Dup2 => {
let a = stack[stack.len() - 1].clone();
let b = stack[stack.len() - 2].clone();
stack.push(b);
stack.push(a);
RegInstr {
dsts: vec![],
op: Nop,
imm: ImmOperand::None,
args: vec![],
}
}
// Swap top 2 elems in stack, out_instr becomes `nop`
Swap => {
let a = stack.pop().unwrap();
let b = stack.pop().unwrap();
stack.push(a);
stack.push(b);
RegInstr {
dsts: vec![],
op: Nop,
imm: ImmOperand::None,
args: vec![],
}
}
_ => return false,
};
true
}
/// Handle a normal instruction with simulated stack pushes/pops
fn handle_normal_case(
instr: &Instr,
stack: &mut Vec<Operand>,
mut fresh_reg: impl FnMut() -> Operand,
out_instr: &mut RegInstr,
) {
// op and imm carried as-is
*out_instr = RegInstr {
dsts: vec![],
op: instr.op,
imm: instr.imm,
args: vec![],
};
// Some instrs have extra pops not reflected in info.n_pop
let extra_pops = match instr.imm {
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_pops;
let n_push = info.n_push as usize;
// Simulate this instr's pops
for _ in 0..n_pop {
let x = stack.pop().unwrap();
out_instr.args.push(x);
}
// Simulate this instr's pushes
for _ in 0..n_push {
let x = fresh_reg();
stack.push(x);
out_instr.dsts.push(x);
}
}