Initial SSA impl + docs

This commit is contained in:
2026-09-02 13:16:05 +00:00
parent a8acb7706c
commit c7d44584d9
6 changed files with 1255 additions and 33 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
//! This module takes the nested QuickJS binary structure and separates the functions into a flat indexed namespace
use crate::parse::{
Atom, AtomRef, ClosureKind, Function, Instruction, JsString, JsValue, QuickJSBinary, atom_names,
Atom, AtomRef, ClosureKind, Function, Instr, JsString, JsValue, QuickJSBinary, atom_names,
};
#[derive(Debug, Clone, Copy)]
@@ -34,7 +34,7 @@ pub struct FlatFn {
pub var_count: usize, // num locals
pub captures: Vec<Capture>,
pub cpool: Vec<JsValue>,
pub code: Vec<Instruction>,
pub code: Vec<Instr>,
}
pub fn flatten(bin: QuickJSBinary) -> (Vec<FlatFn>, Vec<Atom>) {
+5 -1
View File
@@ -6,6 +6,7 @@ use std::process::Command;
use flatten::flatten;
use parse::QuickJSBinary;
use ssa::convert_ssa;
const QJSC: &str = "../third_party/quickjs/build/qjsc";
const BIN_OUT: &str = "/tmp/qjsrt.bin";
@@ -16,7 +17,7 @@ fn main() {
// Compile source file
let output = Command::new(QJSC)
.args(["-b", "-m", "-o", BIN_OUT])
.args(["-b", "-o", BIN_OUT])
.arg(filename)
.output()
.unwrap();
@@ -27,4 +28,7 @@ fn main() {
let bin = std::fs::read(BIN_OUT).unwrap();
let (_, parsed) = QuickJSBinary::parse(&bin).unwrap();
let (flat_fns, atoms) = flatten(parsed);
println!("{atoms:#?}");
convert_ssa(flat_fns);
}
+5 -5
View File
@@ -516,13 +516,13 @@ pub enum ImmOperand {
},
}
pub struct Instruction {
pub struct Instr {
pub pc: usize,
pub op: opcodes::OpCode,
pub imm_operands: ImmOperand,
}
impl Debug for Instruction {
impl Debug for Instr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:04} {}", self.pc, self.op.name())?;
match &self.imm_operands {
@@ -544,7 +544,7 @@ impl Debug for Instruction {
// - Jump operands are offsets from the operand field's own position,
// not from the start or end of the instruction. For the
// `atom_label_*` formats that base sits 4 bytes into the operands.
fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instruction>> {
fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instr>> {
let jump = |base: usize, offset: i64| (base as i64 + offset) as usize;
let mut out = Vec::new();
@@ -635,7 +635,7 @@ fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instruction>> {
},
};
out.push(Instruction {
out.push(Instr {
pc,
op: info.op,
imm_operands: operands,
@@ -665,7 +665,7 @@ pub struct Function {
pub closure_vars: Vec<ClosureVar>,
pub cpool: Vec<JsValue>,
/// LLM GENERATED, TENTATIVE
pub byte_code: Vec<Instruction>,
pub byte_code: Vec<Instr>,
}
impl Function {
+115 -25
View File
@@ -1,45 +1,68 @@
#![allow(non_snake_case)]
use crate::parse::{ImmOperand, Instruction};
use crate::{
flatten::FlatFn,
parse::{ImmOperand, opcodes::OpCode},
};
fn is_jump_op(op: OpCode) -> bool {
use OpCode::*;
matches!(
op,
Goto | Goto8 | Goto16 | IfFalse | IfTrue | IfFalse8 | IfTrue8
)
}
fn is_terminal_op(op: OpCode) -> bool {
use OpCode::*;
matches!(op, Return | ReturnUndef | ReturnAsync | Throw)
}
#[derive(Debug)]
enum Terminator {
UncondJump(usize),
CondJump {
truthy: bool,
value: usize,
true_block: usize,
false_block: usize,
},
Terminal,
}
#[derive(Debug)]
struct BlockSpan {
start: usize,
end: usize,
term: Terminator,
}
fn find_basic_blocks(code: &[Instruction]) -> Vec<BlockSpan> {
fn find_basic_blocks(flat_fn: &FlatFn) -> Vec<BlockSpan> {
// Find block starts: the first instruction, every jump target,
// and whatever follows a jump or a return
let mut starts = vec![0usize];
let code = &flat_fn.code;
let mut starts = vec![0];
for (i, instr) in code.iter().enumerate() {
use crate::parse::opcodes::OpCode::*;
// If cond/uncond jump, jump target is a start, next instr (if exists) is another start
if is_jump_op(instr.op) {
let jump_target = match instr.imm_operands {
ImmOperand::Label { target } => code.iter().position(|x| x.pc == target).unwrap(),
_ => unreachable!(),
};
match instr.op {
// If cond/uncond jump, jump target is a start, next instr (if exists) is another start
Goto | Goto8 | Goto16 | IfFalse | IfTrue | IfFalse8 | IfTrue8 => {
let jump_target = match instr.imm_operands {
ImmOperand::Label { target } => {
code.iter().position(|x| x.pc == target).unwrap()
}
_ => panic!("branch opcode without a label operand"),
};
starts.push(jump_target);
starts.push(jump_target);
if i + 1 < code.len() {
starts.push(i + 1);
}
if i + 1 < code.len() {
starts.push(i + 1);
}
}
// If terminal, next instr if exists is a start
Return | ReturnUndef | ReturnAsync | Throw => {
if i + 1 < code.len() {
starts.push(i + 1);
}
// If terminal, next instr (if exists) is a start
if is_terminal_op(instr.op) {
if i + 1 < code.len() {
starts.push(i + 1);
}
_ => {}
}
}
@@ -53,8 +76,75 @@ fn find_basic_blocks(code: &[Instruction]) -> Vec<BlockSpan> {
.map(|(i, &start)| BlockSpan {
start,
end: starts.get(i + 1).copied().unwrap_or(code.len()),
term: Terminator::Terminal,
})
.collect::<Vec<BlockSpan>>();
blocks
}
#[derive(Debug)]
struct SsaInstr {
dsts: Vec<usize>,
op: OpCode,
imm: ImmOperand,
args: Vec<usize>,
}
fn ssa_walk(flat_fn: &FlatFn) -> Vec<SsaInstr> {
//let mut locals = vec![];
let mut stack = vec![];
let mut output = vec![];
let mut next_val = 0;
let mut fresh_val = || {
let v = next_val;
next_val += 1;
v
};
for instr in &flat_fn.code {
let mut ssa_instr = SsaInstr {
dsts: vec![],
op: instr.op,
imm: instr.imm_operands,
args: vec![],
};
let info = instr.op.info();
let n_pop = info.n_pop;
let n_push = info.n_push;
for _ in 0..n_pop {
let v = stack.pop().unwrap();
ssa_instr.args.push(v);
}
for _ in 0..n_push {
let v = fresh_val();
stack.push(v);
ssa_instr.dsts.push(v);
}
output.push(ssa_instr);
}
output
}
fn compute_terminators(ssa: &[SsaInstr], blocks: &mut [BlockSpan]) {
for instr in ssa {
if is_jump_op(instr.op) {}
}
}
pub fn convert_ssa(flat_fns: Vec<FlatFn>) {
for flat_fn in flat_fns {
let blocks = find_basic_blocks(&flat_fn);
let ssa = ssa_walk(&flat_fn);
println!("{flat_fn:#?}");
println!("{blocks:#?}");
println!("{ssa:#?}");
}
}