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
-30
View File
@@ -1,30 +0,0 @@
//! declass: ES classes become prototype form, one algo step per file.
//!
//! The steps follow declass_algo.txt:
//!
//! step_1 rewrite how a class is built (per function).
//! step_2 splice the field initialiser into the constructor.
//! step_3 fix the constructor header: rename, drop the capture (to come).
mod step_1;
mod step_2;
use crate::{declass::step_1::ClassSequence, regify::RegFn};
use step_1::{find_class_seqs, rewrite_class_to_proto};
use step_2::splice_ctors;
pub fn declass(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
let class_seqs = reg_fns
.iter()
.map(find_class_seqs)
.collect::<Vec<Vec<ClassSequence>>>();
reg_fns
.iter_mut()
.zip(&class_seqs)
.for_each(|(f, s)| rewrite_class_to_proto(f, s));
splice_ctors(&mut reg_fns, &class_seqs);
reg_fns
}
-283
View File
@@ -1,283 +0,0 @@
use std::range::Range;
use crate::{
parse::{AtomRef, ImmOperand, JsValue, atom_names::PREDEFINED_ATOMS, opcodes::OpCode},
regify::{RegFn, RegInstr},
};
pub fn find_class_seqs(reg_fn: &RegFn) -> Vec<ClassSequence> {
let mut ins = reg_fn.code.as_slice();
let mut seqs = vec![];
let mut offset = 0;
while !ins.is_empty() {
let seq = match_class_seq(ins); // Try to match a class decl sequence
match seq {
// Found class sequence, store it and advance `ins`
Ok((rest, num_methods)) => {
let consumed = ins.len() - rest.len(); // How many instrs were consumed
let seq_instr = &ins[..consumed]; // The class seq's instrs
seqs.push(ClassSequence {
range: Range {
start: offset,
end: offset + consumed,
},
num_methods,
ctor_fn: fn_id(&reg_fn.cpool, as_const(seq_instr[0].imm)),
field_init_fn: find_field_init_fn(seq_instr, num_methods, &reg_fn.cpool),
});
offset += consumed;
ins = rest;
}
// Did not find sequence, advance `ins` by 1 and try again
Err(rest) => {
let consumed = ins.len() - rest.len(); // Handle partial consumption
let consumed = consumed.max(1);
offset += consumed;
ins = &ins[consumed..];
}
}
}
seqs
}
pub fn rewrite_class_to_proto(reg_fn: &mut RegFn, class_seqs: &[ClassSequence]) {
// The prototype form is never longer than the class build it replaces, so
// it drops straight over it and every instr outside a class seq stays put.
// The code moves out only so fresh_val() can borrow reg_fn beside it.
let mut code = std::mem::take(&mut reg_fn.code);
let mut buf = vec![]; // Reused across seqs, so it allocates once
for seq in class_seqs {
seq.emit_prototype(&code[seq.range], &mut || reg_fn.fresh_val(), &mut buf);
buf[0].block_start = code[seq.range.start].block_start;
code[seq.range].swap_with_slice(&mut buf);
}
reg_fn.code = code;
}
pub struct ClassSequence {
pub range: Range<usize>,
pub num_methods: usize,
pub ctor_fn: usize, // Fn id of the class ctor
pub field_init_fn: Option<usize>, // Fn id of the field init, if any
}
/// The field initialiser's fn id, if the class has a field-initialiser tail.
fn find_field_init_fn(
matched: &[RegInstr],
num_methods: usize,
cpool: &[JsValue],
) -> Option<usize> {
let tail = 2 + 2 * num_methods; // The ctor and its methods, before the tail
if matched.len() == tail {
return None; // No field-initialiser tail
}
Some(fn_id(cpool, as_const(matched[tail].imm)))
}
impl ClassSequence {
/// Emit the prototype form into the scratchpad `buf`, reading operands
/// straight out of `matched` -- this class sequence's own instructions.
/// `buf` comes back padded with nops to exactly the length of the class
/// build, so the caller can drop it over the original in place.
fn emit_prototype<F>(&self, matched: &[RegInstr], fresh_val: &mut F, buf: &mut Vec<RegInstr>)
where
F: FnMut() -> usize,
{
buf.clear();
// push_const8 #ctor ; define_class -> fclosure8 #ctor (constructor kept)
let (ctor_const, ctor_val) = self.ctor(matched);
buf.push(fclosure8(ctor_const, ctor_val));
// A class with no methods has no prototype to read
if self.num_methods > 0 {
// The prototype cannot change between the put_fields below, so read
// it once for the whole class instead of once per method
let proto = fresh_val();
buf.push(get_field(proto_atom(), proto, ctor_val));
// each ( fclosure8 #m ; define_method @meth ) -> fclosure8 + put_field
for k in 0..self.num_methods {
let (method_const, method_val, meth) = self.method(matched, k);
buf.push(fclosure8(method_const, method_val));
buf.push(put_field(ImmOperand::Atom(meth), method_val, proto));
}
}
// the fclosure8 #fields ; set_home_object ; put_loc ; drop tail is dropped.
// 2 + 2n emitted against the 2 + 2n matched, less the 4-instr field
// initialiser tail, so the emission always fits. Nop pads the slack; it
// pops and pushes nothing, so it cannot disturb the value flow.
let len = self.range.end - self.range.start;
debug_assert!(buf.len() <= len, "class rewrite outgrew its range");
buf.resize_with(len, nop);
}
/// The constructor's constant and register ID it was written to
/// push_const8 #ctor ; define_class.
fn ctor(&self, matched: &[RegInstr]) -> (u32, usize) {
(as_const(matched[0].imm), matched[1].dsts[0])
}
/// The k-th method's constant, closure value, and name; each method pair
/// sits after the two constructor instructions.
fn method(&self, matched: &[RegInstr], k: usize) -> (u32, usize, AtomRef) {
let fclosure = &matched[2 + 2 * k];
let define_method = &matched[3 + 2 * k];
(
as_const(fclosure.imm),
fclosure.dsts[0],
atom_of(define_method.imm),
)
}
}
// ─────────────────────────── Class Sequence Grammar ─────────────────────────────────────
// push_const8 #ctor ; define_class @Name <-- Class decl header
// ( fclosure8 #method ; define_method @meth )* <-- Methods
// ( fclosure8 #fields ; set_home_object ; put_loc ; drop )? <-- Field initializer stuff
//
// match fns return Ok(advanced_instrs) or Err(original_instrs)
fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> {
let ins = match_one(ins, |i| i.op == OpCode::PushConst8)?;
if let Some(i) = ins.first() {
let is_derived_class = matches!(i.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1);
if i.op == OpCode::DefineClass && is_derived_class {
panic!("declass: derived classes (extends) are not supported");
}
}
let ins = match_one(ins, |i| i.op == OpCode::DefineClass)?;
let (ins, num_methods) = match_many(ins, match_method_seq);
let ins = match_opt(ins, match_field_init_seq);
Ok((ins, num_methods))
}
// TODO: accessors are not handled
fn match_method_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> {
let ins = match_one(ins, |i| i.op == OpCode::Fclosure8)?;
let ins = match_one(ins, |i| i.op == OpCode::DefineMethod)?;
Ok(ins)
}
fn match_field_init_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> {
let is_put_loc = |op| {
matches!(
op,
OpCode::PutLoc
| OpCode::PutLoc8
| OpCode::PutLoc0
| OpCode::PutLoc1
| OpCode::PutLoc2
| OpCode::PutLoc3
)
};
let ins = match_one(ins, |i| i.op == OpCode::Fclosure8)?;
let ins = match_one(ins, |i| i.op == OpCode::SetHomeObject)?;
let ins = match_one(ins, |i| is_put_loc(i.op))?;
let ins = match_one(ins, |i| i.op == OpCode::Drop)?;
Ok(ins)
}
// ───────────────────────────── Matcher Primitives ───────────────────────────────────────
fn match_one(i: &[RegInstr], f: impl Fn(&RegInstr) -> bool) -> Result<&[RegInstr], &[RegInstr]> {
match i.first() {
Some(instr) if f(instr) => Ok(&i[1..]),
_ => Err(i),
}
}
fn match_many(
mut i: &[RegInstr],
f: impl Fn(&[RegInstr]) -> Result<&[RegInstr], &[RegInstr]>,
) -> (&[RegInstr], usize) {
let mut n = 0;
while let Ok(rest) = f(i) {
n += 1;
i = rest;
}
(i, n)
}
fn match_opt<'a>(
i: &'a [RegInstr],
f: impl Fn(&'a [RegInstr]) -> Result<&'a [RegInstr], &'a [RegInstr]>,
) -> &'a [RegInstr] {
match f(i) {
Ok(rest) => rest,
Err(_) => i,
}
}
// ── emitted instructions ───────────────────────────────────────────────────
fn nop() -> RegInstr {
RegInstr::new(OpCode::Nop, ImmOperand::None, vec![], vec![])
}
fn fclosure8(const_index: u32, dst: usize) -> RegInstr {
RegInstr::new(
OpCode::Fclosure8,
ImmOperand::Const(const_index),
vec![dst],
vec![],
)
}
fn get_field(field: ImmOperand, dst: usize, obj: usize) -> RegInstr {
RegInstr::new(OpCode::GetField, field, vec![dst], vec![obj])
}
fn put_field(field: ImmOperand, value: usize, obj: usize) -> RegInstr {
RegInstr::new(OpCode::PutField, field, vec![], vec![value, obj])
}
// ── operand helpers ────────────────────────────────────────────────────────
fn atom_of(imm: ImmOperand) -> AtomRef {
match imm {
ImmOperand::AtomU8 { atom, .. } => atom,
_ => unreachable!("expected an atom operand"),
}
}
fn as_const(imm: ImmOperand) -> u32 {
match imm {
ImmOperand::Const(c) => c,
_ => unreachable!("expected a constant operand"),
}
}
/// The function id a constant-pool entry points at.
fn fn_id(cpool: &[JsValue], const_index: u32) -> usize {
match cpool[const_index as usize] {
JsValue::FnById(id) => id,
_ => unreachable!("class constant is not a function"),
}
}
/// The `"prototype"` field name as a predefined-atom operand.
fn proto_atom() -> ImmOperand {
let i = PREDEFINED_ATOMS
.iter()
.position(|&n| n == "prototype")
.unwrap();
ImmOperand::Atom(AtomRef::Predefined(i as u32))
}
-126
View File
@@ -1,126 +0,0 @@
//! Step 2 of declass: splice the field initialiser into the constructor.
//!
//! A class constructor does not set its own fields; quickjs hoisted them into a
//! separate initialiser function that the constructor reaches through the
//! captured <class_fields_init> and runs with a call (declass_algo.txt, step 2):
//!
//! get_var_ref_check <class_fields_init> ; dup ; if_false
//! ; get_loc ; swap ; call_method
//!
//! This step takes the initialiser's field writes -- its body minus the
//! this/home-object prologue and the trailing return -- and splices them into
//! the constructor in place of that call, renumbered onto fresh value ids. The
//! initialiser is left with an empty body. It cannot be dropped from the
//! function list, because a function id is an index into that list.
//!
//! Deficiency, left for step 3: a class with NO instance fields compiles
//! `<class_fields_init>` to `Undefined` rather than a closure, so there is no
//! initialiser to splice and `field_init_links` records no link. Its
//! constructor keeps the call block above, which the `if_false` skips at run
//! time -- a harmless no-op. Step 3 must strip that block when it drops the
//! `<class_fields_init>` capture, or the `get_var_ref_check` would dangle.
use super::step_1::ClassSequence;
use crate::{
parse::{ImmOperand, opcodes::OpCode},
regify::{RegFn, RegInstr},
};
/// The name quickjs gives the hoisted field initialiser.
const FIELDS_INIT: &str = "<class_fields_init>";
pub fn splice_ctors(fns: &mut [RegFn], class_seqs: &[Vec<ClassSequence>]) {
// Only a class with instance fields has an initialiser to splice.
for seq in class_seqs.iter().flatten() {
if let Some(field_init_fn) = seq.field_init_fn {
splice_one(fns, seq.ctor_fn, field_init_fn);
}
}
}
fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) {
// Find the call block first. If the constructor does not have one there is
// nothing to splice into, and the initialiser has to keep working.
let Some((start, end)) = call_span(&fns[ctor]) else {
return;
};
// Take the field writes out of the initialiser (a separate function), then
// hand them to the constructor.
let writes = take_field_writes(&mut fns[fields]);
let ctor_fn = &mut fns[ctor];
let writes = renumber(writes, ctor_fn);
ctor_fn.code.splice(start..=end, writes);
}
/// The initialiser's field writes: its body minus the this/home-object prologue
/// and the trailing return. They reach `this` through get_loc0, which is `this`
/// in the constructor too, so they carry over unchanged.
///
/// This empties the initialiser, which is dead once its writes move into the
/// constructor. The function itself has to stay: a function id is an index into
/// the function list, so removing one would renumber every function after it.
fn take_field_writes(init: &mut RegFn) -> Vec<RegInstr> {
init.code.retain(|i| !is_prologue(i.op));
std::mem::take(&mut init.code)
}
/// Renumber the writes' value ids onto fresh ids in the constructor. Each write
/// only uses values it or an earlier write produced, so a flat map suffices.
fn renumber(mut writes: Vec<RegInstr>, ctor_fn: &mut RegFn) -> Vec<RegInstr> {
let hi = writes
.iter()
.flat_map(|i| i.dsts.iter().chain(&i.args))
.copied()
.max()
.unwrap_or(0);
let mut map = vec![usize::MAX; hi + 1];
for instr in &mut writes {
for arg in &mut instr.args {
*arg = map[*arg];
}
for dst in &mut instr.dsts {
let fresh = ctor_fn.fresh_val();
map[*dst] = fresh;
*dst = fresh;
}
}
writes
}
/// The call block `get_var_ref_check <class_fields_init> .. call_method`, as an
/// inclusive `start..=end` into the constructor's code.
fn call_span(ctor: &RegFn) -> Option<(usize, usize)> {
let start = ctor.code.iter().position(|i| reads_fields_init(i, ctor))?;
let end = ctor.code[start..]
.iter()
.position(|i| matches!(i.op, OpCode::CallMethod | OpCode::TailCallMethod))?;
Some((start, start + end))
}
/// True for `get_var_ref_check` on the `<class_fields_init>` capture.
fn reads_fields_init(i: &RegInstr, ctor: &RegFn) -> bool {
let ImmOperand::VarRef(k) = i.imm else {
return false;
};
i.op == OpCode::GetVarRefCheck && ctor.captures[k as usize].name == FIELDS_INIT
}
/// The initialiser prologue and terminator, dropped when splicing.
fn is_prologue(op: OpCode) -> bool {
matches!(
op,
OpCode::SpecialObject
| OpCode::PushThis
| OpCode::PutLoc0
| OpCode::PutLoc1
| OpCode::PutLoc2
| OpCode::PutLoc3
| OpCode::Goto8
| OpCode::Goto
| OpCode::ReturnUndef
)
}
-326
View File
@@ -1,326 +0,0 @@
//! declosure: closures become plain objects.
//!
//! A closure becomes an object holding its function plus its captured
//! variables (docs/declosure_algo.txt). Field 0 is the function; fields 1..
//! are the captures, in capture order. The object doubles as the environment,
//! so there is no second env value to track. Field names are plain integers,
//! so a field operand reads `Atom(int(k))`.
//!
//! Three steps, one property each:
//!
//! 1. `take_env_param` -- every function takes the env as its
//! LAST parameter and reads its captures out of it: `get_var_ref` /
//! `put_var_ref` become field access on the env.
//! 2. `build_closure_objects` -- every `fclosure` builds the object: the
//! function in field 0, then one field per capture, loaded from the
//! parent frame.
//! 3. `route_calls_through_object` -- every plain call reads field 0 and
//! passes the object itself as the trailing env argument.
//!
//! Every function takes an env whether it captures anything or not, so step 3
//! can append the object at a uniform call site.
//!
//! Two facts about the register form this leans on:
//!
//! * A function is a plain constant -- an index into the cpool. `fclosure`
//! becomes `push_const`, so it loads like any other const, and storing it
//! in a field is no different from storing a number.
//! * The callee of a call is its LAST argument (the deepest stack slot,
//! popped last), so step 3 reads `args.last()`.
//!
//! Known gaps, in rough order of how likely they are to bite:
//!
//! * A captured variable the parent later writes. The write produces a new
//! value instead of storing through the alias, so the closure never sees
//! it. A capture aliases the value a slot held at capture time.
//! * Non-local capture sources. Step 2 can materialise a capture that comes
//! from a parent local or arg. A capture that itself comes from the
//! parent's captures, a module binding, or a global is left as an
//! `undefined` placeholder -- it needs the parent to forward its own env.
//! * Foreign callees. Step 3 rewrites every plain call into a field-0 read.
//! That is only valid for functions we produced; a host builtin such as
//! `Math.sqrt` has no such field.
//! * Entry points. Step 1 gives every function an env parameter, but the
//! module root and any host-invoked function have no caller of ours to
//! supply one.
//! * `call_method` and `call_constructor` carry a receiver, so they belong
//! to the class passes; step 3 leaves them untouched. Same for `apply`.
use crate::{
flatten::{Capture, CaptureFrom},
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
regify::{RegFn, RegInstr},
};
pub fn declosure(mut reg_fns: Vec<RegFn>) -> Vec<RegFn> {
// Every function's captures, per fn id. Step 2 runs on a parent but needs
// its CHILD's captures, and `declosure_one` only ever holds one function,
// so the cross-function view has to be gathered here. Taking the lists
// moves them -- no per-function allocation -- and leaves every function
// with the empty capture list declosure ends with anyway.
let captures: Vec<Vec<Capture>> = reg_fns
.iter_mut()
.map(|f| std::mem::take(&mut f.captures))
.collect();
reg_fns
.into_iter()
.map(|f| declosure_one(f, &captures))
.collect()
}
fn declosure_one(reg_fn: RegFn, captures: &[Vec<Capture>]) -> RegFn {
let reg_fn = take_env_param(reg_fn);
let reg_fn = build_closure_objects(reg_fn, captures);
route_calls_through_object(reg_fn)
}
// ── 1. every function takes an env ─────────────────────────────────────────
/// Append `env` as the last parameter -- existing arguments keep their numbers
/// -- and turn every capture access into a field access on it.
fn take_env_param(mut reg_fn: RegFn) -> RegFn {
let env_arg = reg_fn.arg_count;
reg_fn.arg_count += 1;
let code = std::mem::take(&mut reg_fn.code);
// Nothing reads a capture, so there is nothing to rewrite. The parameter
// is still added, to keep step 3's call sites uniform.
if !code.iter().any(|i| is_capture_access(i).is_some()) {
reg_fn.code = code;
return reg_fn;
}
// Load the env once at entry.
let mut out = Vec::with_capacity(code.len() + 1);
let env = reg_fn.fresh_val();
out.push(get_arg(env, env_arg));
for instr in code {
match is_capture_access(&instr) {
// A read keeps its destination, so downstream uses do not change.
Some(CaptureAccess::Read(k)) => out
.push(get_field(int_field(k + 1), instr.dsts[0], env).starting(instr.block_start)),
Some(CaptureAccess::Write(k)) => out
.push(put_field(int_field(k + 1), instr.args[0], env).starting(instr.block_start)),
None => out.push(instr),
}
}
reg_fn.code = out;
reg_fn
}
/// Which capture an instruction reads or writes, if any.
enum CaptureAccess {
Read(u32),
Write(u32),
}
fn is_capture_access(i: &RegInstr) -> Option<CaptureAccess> {
// The numbered forms fold the slot into the opcode; the rest carry it.
let slot = || match i.imm {
ImmOperand::VarRef(k) => k as u32,
_ => unreachable!("a var_ref opcode without a var_ref operand"),
};
Some(match i.op {
OpCode::GetVarRef | OpCode::GetVarRefCheck => CaptureAccess::Read(slot()),
OpCode::GetVarRef0 => CaptureAccess::Read(0),
OpCode::GetVarRef1 => CaptureAccess::Read(1),
OpCode::GetVarRef2 => CaptureAccess::Read(2),
OpCode::GetVarRef3 => CaptureAccess::Read(3),
OpCode::PutVarRef | OpCode::PutVarRefCheck | OpCode::PutVarRefCheckInit => {
CaptureAccess::Write(slot())
}
OpCode::PutVarRef0 => CaptureAccess::Write(0),
OpCode::PutVarRef1 => CaptureAccess::Write(1),
OpCode::PutVarRef2 => CaptureAccess::Write(2),
OpCode::PutVarRef3 => CaptureAccess::Write(3),
_ => return None,
})
}
// ── 2. every fclosure builds the object ────────────────────────────────────
/// Replace each `fclosure` with the object that stands for it: the function in
/// field 0, then one field per capture, each loaded from the parent frame.
fn build_closure_objects(mut reg_fn: RegFn, captures: &[Vec<Capture>]) -> RegFn {
let code = std::mem::take(&mut reg_fn.code);
let mut out = Vec::with_capacity(code.len());
for instr in code {
let Some(child) = closure_child(&instr, &reg_fn.cpool) else {
out.push(instr);
continue;
};
// The value the rest of the code already uses for this closure.
let closure = instr.dsts[0];
let child_captures = &captures[child];
// The function is a plain constant, so it loads with `push_const`.
let load = match instr.op {
OpCode::Fclosure8 => OpCode::PushConst8,
_ => OpCode::PushConst,
};
let func = reg_fn.fresh_val();
out.push(RegInstr::new(load, instr.imm, vec![func], vec![]).starting(instr.block_start));
let mut obj = reg_fn.fresh_val();
out.push(object(obj));
// Field 0 is the function, fields 1.. are the captures. Each
// define_field returns the object, so the stores chain -- and the LAST
// must land in `closure`.
for k in 0..=child_captures.len() {
let value = if k == 0 {
func
} else {
let value = reg_fn.fresh_val();
out.push(load_capture(child_captures[k - 1].from, value));
value
};
let dst = if k == child_captures.len() {
closure
} else {
reg_fn.fresh_val()
};
out.push(define_field(int_field(k as u32), dst, value, obj));
obj = dst;
}
}
reg_fn.code = out;
reg_fn
}
/// The nested function an `fclosure` instantiates, as a fn id.
fn closure_child(i: &RegInstr, cpool: &[JsValue]) -> Option<usize> {
if !matches!(i.op, OpCode::Fclosure | OpCode::Fclosure8) {
return None;
}
let ImmOperand::Const(index) = i.imm else {
return None;
};
match cpool.get(index as usize) {
Some(JsValue::FnById(id)) => Some(*id),
_ => None,
}
}
/// Load one captured value out of the parent frame into `into`. Only parent
/// locals and args are supported; any other source is a preserved gap and
/// loads `undefined`.
fn load_capture(source: CaptureFrom, into: usize) -> RegInstr {
match source {
CaptureFrom::ParentLocal(slot) => get_loc(into, slot),
CaptureFrom::ParentArg(slot) => get_arg(into, slot),
_ => RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![into], vec![]),
}
}
// ── 3. every call goes through the object ──────────────────────────────────
/// A call names a closure object now, not a function. Read the function out of
/// field 0, and pass the object itself as the trailing env argument.
fn route_calls_through_object(mut reg_fn: RegFn) -> RegFn {
let code = std::mem::take(&mut reg_fn.code);
let mut out = Vec::with_capacity(code.len());
for instr in code {
let Some((op, argc)) = plain_call(&instr) else {
out.push(instr);
continue;
};
// The callee is the deepest stack slot, so it is popped last.
let closure = *instr.args.last().unwrap();
let func = reg_fn.fresh_val();
out.push(get_field(int_field(0), func, closure).starting(instr.block_start));
// Args become [ env, <original args>, func ]: the env is pushed last so
// it pops first, and the callee stays last.
let mut args = Vec::with_capacity(instr.args.len() + 1);
args.push(closure);
args.extend_from_slice(&instr.args[..instr.args.len() - 1]);
args.push(func);
out.push(RegInstr::new(
op,
ImmOperand::Npop(argc + 1),
instr.dsts,
args,
));
}
reg_fn.code = out;
reg_fn
}
/// The opcode to emit and the argument count, for a plain call. `None` if this
/// is not one: the receiver-carrying calls (`call_method`, `call_constructor`,
/// `apply`) are deliberately excluded.
///
/// A tail call stays a tail call -- it pushes nothing and returns, so turning
/// it into an ordinary `call` would leave a value behind and drop the return.
fn plain_call(i: &RegInstr) -> Option<(OpCode, u16)> {
if i.args.is_empty() {
return None;
}
let npop = || match i.imm {
ImmOperand::Npop(n) => n,
_ => unreachable!("a call opcode without an npop operand"),
};
Some(match i.op {
OpCode::Call0 => (OpCode::Call, 0),
OpCode::Call1 => (OpCode::Call, 1),
OpCode::Call2 => (OpCode::Call, 2),
OpCode::Call3 => (OpCode::Call, 3),
OpCode::Call => (OpCode::Call, npop()),
OpCode::TailCall => (OpCode::TailCall, npop()),
_ => return None,
})
}
// ── emitted instructions ───────────────────────────────────────────────────
fn get_arg(dst: usize, arg: usize) -> RegInstr {
RegInstr::new(
OpCode::GetArg,
ImmOperand::Arg(arg as u16),
vec![dst],
vec![],
)
}
fn get_loc(dst: usize, loc: usize) -> RegInstr {
RegInstr::new(
OpCode::GetLoc,
ImmOperand::Loc(loc as u16),
vec![dst],
vec![],
)
}
fn object(dst: usize) -> RegInstr {
RegInstr::new(OpCode::Object, ImmOperand::None, vec![dst], vec![])
}
fn get_field(field: ImmOperand, dst: usize, obj: usize) -> RegInstr {
RegInstr::new(OpCode::GetField, field, vec![dst], vec![obj])
}
fn put_field(field: ImmOperand, value: usize, obj: usize) -> RegInstr {
RegInstr::new(OpCode::PutField, field, vec![], vec![value, obj])
}
fn define_field(field: ImmOperand, dst: usize, value: usize, obj: usize) -> RegInstr {
RegInstr::new(OpCode::DefineField, field, vec![dst], vec![value, obj])
}
/// A plain integer field name, as an operand.
fn int_field(n: u32) -> ImmOperand {
ImmOperand::Atom(AtomRef::TaggedInt(n))
}
-235
View File
@@ -1,235 +0,0 @@
//! demutate: every static-key field write becomes a call to a function that
//! RETURNS a fresh object.
//!
//! There is no grouping and no analysis of what surrounds a write. Each one
//! stands alone, because its two arguments are the call's two arguments and
//! the enclosing code already holds both:
//!
//! define_field @F [v, o] -> [n] n = <define F>(o, v)
//! put_field @F [v, o] n = <put F>(o, v) ; store n back
//!
//! `object` is left as it is. It already makes a fresh object, so it needs no
//! function; infer reads it as `Type::Empty`, the base every shape builds on.
//!
//! The lifted functions have no body. `CodeKind::DefineField` / `PutField`
//! name the operation and the field, and infer gives each a type rule, so ONE
//! function serves every write of that field in the program. Their ids run on
//! after the real functions.
use crate::{
infer::{CodeKind, InferFn, Type},
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
regify::{RegFn, RegInstr},
};
pub fn demutate(reg_fns: Vec<RegFn>) -> Vec<InferFn> {
// The lifted functions take the ids after the real ones, so a write can
// name the one it needs before the list of them is finished
let first_lifted = reg_fns.len();
let mut lifted = vec![];
let mut fns = reg_fns
.into_iter()
.map(|f| rewrite_fn(f, first_lifted, &mut lifted))
.collect::<Vec<InferFn>>();
fns.extend(lifted.into_iter().map(|code| InferFn {
name: lifted_name(&code),
cpool: vec![],
code,
sigs: vec![],
types: vec![],
blocks: vec![],
}));
fns
}
/// One scan of the code. Everything that is not a static-key write is moved
/// across untouched; a write becomes a call, and a `put_field` a call and a
/// store.
fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec<CodeKind>) -> InferFn {
let code = std::mem::take(&mut reg_fn.code);
let mut new = Vec::with_capacity(code.len());
for instr in code {
let Some(field) = static_key(&instr) else {
new.push(instr); // Not a field write, so it passes through
continue;
};
let define = instr.op == OpCode::DefineField;
// Where the object lives says where its replacement goes. The
// instruction that made it is already in `new`.
let object = instr.args[1];
let slot = (!define).then(|| frame_slot(&new, object)).flatten();
let callee = intern_const(
&mut reg_fn.cpool,
first_lifted + intern_fn(lifted, instr.op, field),
);
let dst = match slot {
// define_field hands the object back, so the call keeps its dst
_ if define => instr.dsts[0],
// regify does not rename a local, so the local IS the object's
// value: defining it again is the store, and costs no instruction
Some(Source::Loc(_)) => object,
// Anything else needs a value of its own to be written back from,
// and with no slot at all the write is lost -- `f().x = 1` does that
_ => reg_fn.fresh_val(),
};
new.push(call(callee, dst, instr.args).starting(instr.block_start));
if let Some(slot) = slot.filter(|s| !matches!(s, Source::Loc(_))) {
new.push(store(slot, dst));
}
}
InferFn {
types: vec![Type::Unknown; reg_fn.num_vals()],
blocks: reg_fn.blocks,
name: reg_fn.name,
cpool: reg_fn.cpool,
code: CodeKind::Code(new),
sigs: vec![],
}
}
/// The field a write names, when the name is fixed at compile time. A computed
/// key is `define_array_el` / `put_array_el`, and is left alone.
fn static_key(i: &RegInstr) -> Option<AtomRef> {
let is_write = matches!(i.op, OpCode::DefineField | OpCode::PutField) && i.args.len() == 2;
match i.imm {
ImmOperand::Atom(a) if is_write => Some(a),
_ => None,
}
}
/// The index of the function for this write, adding it if it is the first of
/// its kind. One function serves every write of one field by one opcode.
fn intern_fn(lifted: &mut Vec<CodeKind>, op: OpCode, field: AtomRef) -> usize {
let wanted = match op {
OpCode::DefineField => CodeKind::DefineField(field),
_ => CodeKind::PutField(field),
};
let same = |c: &CodeKind| match (c, &wanted) {
(CodeKind::DefineField(a), CodeKind::DefineField(b)) => a == b,
(CodeKind::PutField(a), CodeKind::PutField(b)) => a == b,
_ => false,
};
lifted.iter().position(same).unwrap_or_else(|| {
lifted.push(wanted);
lifted.len() - 1
})
}
/// The cpool slot holding function `id`, adding it if it is not there yet. A
/// call names its function the way every other reference to one does.
fn intern_const(cpool: &mut Vec<JsValue>, id: usize) -> u32 {
let at = cpool
.iter()
.position(|c| matches!(c, JsValue::FnById(f) if *f == id))
.unwrap_or_else(|| {
cpool.push(JsValue::FnById(id));
cpool.len() - 1
});
at as u32
}
/// The frame slot an object lives in, from whichever instruction defined it.
fn frame_slot(code: &[RegInstr], object: usize) -> Option<Source> {
code.iter()
.find(|i| i.dsts.contains(&object))
.and_then(frame_load)
}
/// A frame slot, and the two instructions that read and write it.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Source {
Arg(u16),
Loc(u16),
VarRef(u16),
}
// ── emitted instructions ───────────────────────────────────────────────────
/// A call to a function fixed at rewrite time. The callee is named by the
/// immediate rather than pushed, so the call fits where the write stood.
fn call(callee: u32, dst: usize, args: Vec<usize>) -> RegInstr {
RegInstr::new(OpCode::Call, ImmOperand::Const(callee), vec![dst], args)
}
fn store(slot: Source, value: usize) -> RegInstr {
let (op, imm) = match slot {
Source::Arg(k) => (OpCode::PutArg, ImmOperand::Arg(k)),
Source::Loc(k) => (OpCode::PutLoc, ImmOperand::Loc(k)),
Source::VarRef(k) => (OpCode::PutVarRef, ImmOperand::VarRef(k)),
};
RegInstr::new(op, imm, vec![], vec![value])
}
fn lifted_name(code: &CodeKind) -> String {
match code {
CodeKind::DefineField(a) => format!("<define {a:?}>"),
CodeKind::PutField(a) => format!("<put {a:?}>"),
CodeKind::Code(_) => unreachable!("a lifted function has no body"),
}
}
// ── operand helpers ────────────────────────────────────────────────────────
/// The frame slot an instruction reads. Only plain one-value loads count;
/// `get_loc0_loc1` pushes two.
fn frame_load(i: &RegInstr) -> Option<Source> {
let slot = |imm| match imm {
ImmOperand::Loc(k) | ImmOperand::Arg(k) | ImmOperand::VarRef(k) => k,
_ => unreachable!("a frame load without a slot operand"),
};
match i.op {
OpCode::GetArg0 => Some(Source::Arg(0)),
OpCode::GetArg1 => Some(Source::Arg(1)),
OpCode::GetArg2 => Some(Source::Arg(2)),
OpCode::GetArg3 => Some(Source::Arg(3)),
OpCode::GetArg => Some(Source::Arg(slot(i.imm))),
OpCode::GetLoc0 => Some(Source::Loc(0)),
OpCode::GetLoc1 => Some(Source::Loc(1)),
OpCode::GetLoc2 => Some(Source::Loc(2)),
OpCode::GetLoc3 => Some(Source::Loc(3)),
// Every local op names the same value, so any of them identifies it
OpCode::GetLoc
| OpCode::GetLoc8
| OpCode::GetLocCheck
| OpCode::PutLoc
| OpCode::PutLoc8
| OpCode::PutLocCheck
| OpCode::PutLocCheckInit
| OpCode::SetLoc
| OpCode::SetLoc8
| OpCode::SetLocUninitialized => Some(Source::Loc(slot(i.imm))),
OpCode::PutLoc0 | OpCode::SetLoc0 => Some(Source::Loc(0)),
OpCode::PutLoc1 | OpCode::SetLoc1 => Some(Source::Loc(1)),
OpCode::PutLoc2 | OpCode::SetLoc2 => Some(Source::Loc(2)),
OpCode::PutLoc3 | OpCode::SetLoc3 => Some(Source::Loc(3)),
OpCode::GetVarRef0 => Some(Source::VarRef(0)),
OpCode::GetVarRef1 => Some(Source::VarRef(1)),
OpCode::GetVarRef2 => Some(Source::VarRef(2)),
OpCode::GetVarRef3 => Some(Source::VarRef(3)),
OpCode::GetVarRef | OpCode::GetVarRefCheck => Some(Source::VarRef(slot(i.imm))),
_ => None,
}
}
+17 -106
View File
@@ -1,131 +1,42 @@
//! This module takes the nested QuickJS binary structure and separates the functions into a flat indexed namespace
use crate::parse::{
Atom, AtomRef, ClosureKind, Function, Instr, JsString, JsValue, QuickJSBinary, atom_names,
};
#[derive(Debug, Clone, Copy)]
pub enum CaptureFrom {
/// `parent.var_defs[i]`
ParentArg(usize),
/// `parent.var_defs[parent.arg_count + i]`
ParentLocal(usize),
/// `parent.captures[i]`, itself resolved against the grandparent.
ParentCapture(usize),
/// A module-level binding of the module body.
ModuleBinding(usize),
/// Global or module import; not frame-relative.
NotFrameRelative(&'static str),
}
#[derive(Debug)]
pub struct Capture {
pub name: String,
/// The name as it was on the wire. A field cannot be given a name that is
/// not already an atom, so the operand has to be kept, not rebuilt.
pub atom: AtomRef,
pub from: CaptureFrom,
}
use crate::libqjs::parse::{Atom, AtomRef, ClosureVar, Function, Instr, JsValue, QuickJSBinary};
#[derive(Debug)]
pub struct FlatFn {
pub name: String,
pub name: AtomRef,
pub arg_count: usize,
pub var_count: usize, // num locals
pub value_names: Vec<AtomRef>, // adds names to args and locals, one for each arg, then one for each local, in one vec
pub captures: Vec<Capture>,
pub var_count: usize,
pub cpool: Vec<JsValue>,
pub captures: Vec<ClosureVar>,
pub code: Vec<Instr>,
}
pub fn flatten(bin: QuickJSBinary) -> (Vec<FlatFn>, Vec<Atom>) {
let mut fns = vec![];
walk(&mut fns, bin.root_mod.root_fn, &bin.atoms);
walk(&mut fns, bin.root_mod.root_fn);
(fns, bin.atoms)
}
fn walk(flat_fns: &mut Vec<FlatFn>, func: Function, atoms: &[Atom]) -> usize {
fn walk(flat_fns: &mut Vec<FlatFn>, func: Function) -> usize {
let id = flat_fns.len();
let name = atom_str(&func.name, atoms);
// Find vars func captures from its environment
let captures = func
.closure_vars
.iter()
.map(|cv| Capture {
name: atom_str(&cv.name, atoms),
atom: cv.name,
from: match cv.flags.closure_kind {
ClosureKind::Arg => CaptureFrom::ParentArg(cv.var_idx),
ClosureKind::Local => CaptureFrom::ParentLocal(cv.var_idx),
ClosureKind::Ref => CaptureFrom::ParentCapture(cv.var_idx),
ClosureKind::ModuleDecl => CaptureFrom::ModuleBinding(cv.var_idx),
ClosureKind::ModuleImport => todo!(), // CaptureFrom::NotFrameRelative("module_import"),
ClosureKind::Global | ClosureKind::GlobalDecl | ClosureKind::GlobalRef => todo!(), // CaptureFrom::NotFrameRelative("global")
},
})
.collect();
// Reserve this function's slot before recursing into nested functions,
// so it keeps `id` while its children take the ids after it
flat_fns.push(FlatFn {
name,
name: func.name,
arg_count: func.arg_count,
var_count: func.var_count,
value_names: func.var_defs.iter().map(|v| v.name).collect(),
captures,
cpool: Vec::new(),
cpool: vec![],
captures: func.closure_vars,
code: func.byte_code,
});
// Nested functions live in the cpool; replace each with its flat id.
let mut cpool = Vec::with_capacity(func.cpool.len());
for const_entry in func.cpool {
let entry = match const_entry {
JsValue::Function(child) => JsValue::FnById(walk(flat_fns, child, atoms)),
let cpool = func
.cpool
.into_iter()
.map(|c| match c {
JsValue::Function(child) => JsValue::FnById(walk(flat_fns, child)),
other => other,
};
cpool.push(entry);
}
flat_fns[id].cpool = cpool;
})
.collect();
flat_fns[id].cpool = cpool;
id
}
// LLM GENERATED
// ─── atom rendering ──────────────────────────────────────────────────────
pub fn atom_str(a: &AtomRef, atoms: &[Atom]) -> String {
match a {
AtomRef::TaggedInt(n) => n.to_string(),
AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS
.get(*i as usize)
.copied()
.unwrap_or("<predef?>")
.to_string(),
AtomRef::FileLocal(i) => atoms
.get(*i)
.map(atom_text)
.unwrap_or_else(|| format!("<file_atom {i}>")),
}
}
fn atom_text(a: &Atom) -> String {
match a {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s),
Atom::Predefined(n) => format!("<predef {n}>"),
}
}
fn js_string(s: &JsString) -> String {
match s {
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
JsString::UTF16(b) => {
let u: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u)
}
}
}
+176 -346
View File
@@ -1,52 +1,37 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use crate::{
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
regify::{Block, RegInstr},
libqjs::parse::{
Atom, AtomRef, ClosureVar, ImmOperand, JsString, JsValue, atom_names, opcodes::OpCode,
},
regify::{BasicBlock, RegFn, RegInstr},
};
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
Unknown,
Undefined,
Bool,
Int,
Float,
Empty,
/// A named function, by flat fn id.
Fn(usize),
ReturnOf((usize, Vec<Type>)),
Any,
}
#[derive(Debug)]
pub enum CodeKind {
Code(Vec<RegInstr>),
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FnKind {
Id(usize),
DefineField(AtomRef),
PutField(AtomRef),
}
#[derive(Debug)]
pub struct Signature {
pub args: Vec<Type>,
pub ret: Type,
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Type {
Unknown,
Bool,
Int,
Float,
Fn(FnKind),
Any,
}
#[derive(Debug)]
pub struct InferFn {
pub name: String,
pub cpool: Vec<JsValue>,
pub code: CodeKind,
pub sigs: Vec<Signature>,
/// One entry per value id, so a value's type is `types[v]`. regify keeps
/// the first `var_count` ids for the locals, so a local's type lives there
/// too and `put_loc` / `get_loc` just copy between the two.
pub types: Vec<Type>,
pub blocks: Vec<Block>,
}
/// The lattice, joining what a value already holds with what it is being
/// given. Unknown is the bottom and gives way to anything; Any is the top and
/// swallows everything; two types that disagree can only be described by Any.
pub fn join(a: &Type, b: &Type) -> Type {
/// Type lattice join rules:
/// Unknown * T = T
/// Any * T = Any
/// T * T = T
/// T * U = Any
fn join_ty(a: &Type, b: &Type) -> Type {
match (a, b) {
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
(Type::Any, _) | (_, Type::Any) => Type::Any,
@@ -55,95 +40,137 @@ pub fn join(a: &Type, b: &Type) -> Type {
}
}
/// Walk a function's code once, giving every value a type, and hand back what
/// the function returns. A call is followed down into its callee.
pub fn infer(fn_id: usize, fns: &mut Vec<InferFn>) -> Type {
// The caller left this function's argument types here before recursing.
let args = match fns[fn_id].sigs.first() {
Some(sig) => sig.args.clone(),
None => vec![],
};
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct InferRequest {
fn_id: usize,
arg_types: Vec<Type>,
}
// The two field writers have no body: their result IS the shape term that
// names them, built from the types they were handed.
let code = match &fns[fn_id].code {
CodeKind::DefineField(_) | CodeKind::PutField(_) => return Type::ReturnOf((fn_id, args)),
#[derive(Debug)]
pub struct InferResult {
val_types: Vec<Type>,
ret_type: Type,
}
// An empty body means this function's code is out on the stack, so we
// have come back round a cycle. Unknown is the bottom, and a later
// round raises it.
CodeKind::Code(c) if c.is_empty() => return Type::Unknown,
pub type TypeCache = HashMap<InferRequest, InferResult>;
CodeKind::Code(_) => {
let taken = std::mem::replace(&mut fns[fn_id].code, CodeKind::Code(vec![]));
let CodeKind::Code(code) = taken else {
unreachable!()
};
code
}
};
pub fn infer_types(reg_fns: &[RegFn], atoms: &[Atom]) -> TypeCache {
// Find main fn
let main_fn_id = reg_fns
.iter()
.position(|f| atom_to_str(&f.name, atoms) == "main")
.unwrap();
if fns[fn_id].sigs.is_empty() {
fns[fn_id].sigs.push(Signature {
args: vec![],
ret: Type::Unknown,
let mut cache = TypeCache::with_capacity(reg_fns.len());
let mut worklist = vec![];
worklist.push(InferRequest {
fn_id: main_fn_id,
arg_types: vec![],
});
while let Some(req) = worklist.pop() {
match walk_fn(&req, reg_fns, &mut cache) {
Ok(res) => {
cache.insert(req, res);
}
Err(new_req) => {
worklist.push(new_req);
worklist.push(req);
}
}
}
// The types move out too, so a callee can be inferred through `fns` while
// this function is still being walked. Both go back at the end.
let mut types = std::mem::take(&mut fns[fn_id].types);
let blocks = std::mem::take(&mut fns[fn_id].blocks);
cache
}
// The blocks, read back off the instructions. Passes have moved them, but
// none of them adds or removes a jump, so this is the partition regify
// found and `succ` still lines up with it. Anything inserted above the
// first boundary belongs to the first block.
let mut starts = (code.iter().enumerate())
.filter(|(_, i)| i.block_start)
.map(|(at, _)| at)
.collect::<Vec<usize>>();
fn walk_fn(
req: &InferRequest,
reg_fns: &[RegFn],
cache: &mut TypeCache,
) -> Result<InferResult, InferRequest> {
let f = &reg_fns[req.fn_id];
let mut val_types = vec![Type::Unknown; f.val_count];
let mut ret_type = Type::Unknown;
if let Some(first) = starts.first_mut() {
*first = 0;
let mut worklist = vec![0];
while let Some(block_id) = worklist.pop() {
let block = &f.blocks[block_id];
walk_block(&block, &req.arg_types, f, &mut val_types, &mut ret_type)?;
worklist.extend(block.succ.iter());
}
// One flag in, one block out. A pass may move a boundary or insert around
// it, but it must carry the flag when it REPLACES the instruction holding
// one -- that is what `RegInstr::starting` is for. Miss it and the blocks
// silently stop lining up with the edges regify recorded.
debug_assert_eq!(
starts.len(),
blocks.len(),
"{}: block starts lost or gained since regify",
fns[fn_id].name
);
let mut ret = Type::Unknown;
Ok(InferResult {
val_types,
ret_type,
})
}
// What each block is entered with, joined from the blocks that reach it.
let mut entry = vec![vec![Type::Unknown; types.len()]; starts.len()];
fn walk_block(
block: &BasicBlock,
arg_types: &[Type],
f: &RegFn,
val_types: &mut [Type],
ret_type: &mut Type,
) -> Result<(), InferRequest> {
use OpCode::*;
for (b, &start) in starts.iter().enumerate() {
let end = starts.get(b + 1).copied().unwrap_or(code.len());
for instr in &f.code[block.start..block.end] {
let ty = match instr.op {
// ──────────────────────────────────── Literals ────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
| PushI16 | PushI32 => Type::Int,
// Fold in what the blocks reaching this one left behind. Two paths
// meeting is the ONLY place the algebra applies -- everything below
// runs in a straight line.
for (t, e) in types.iter_mut().zip(&entry[b]) {
*t = join(t, e);
PushTrue | PushFalse => Type::Bool,
PushConst | PushConst8 | Fclosure | Fclosure8 => match instr.imm {
ImmOperand::Const(k) => match f.cpool.get(k as usize) {
Some(JsValue::Int(_)) => Type::Int,
Some(JsValue::Float(_)) => Type::Float,
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
Some(&JsValue::FnById(id)) => Type::Fn(FnKind::Id(id)),
_ => panic!(),
},
_ => panic!(),
},
// ─────────────────────────────────── Arithmetic ───────────────────────────────────
Add | Sub | Mul | Div | Mod | Pow => {
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
(Type::Int, Type::Int) => Type::Int,
(Type::Float, Type::Float)
| (Type::Int, Type::Float)
| (Type::Float, Type::Int) => Type::Float,
_ => Type::Any,
}
}
// A slot at a merge keeps the ids the first path in left, so every
// other path's value has to be folded into them. Without this the
// merge reads one arm and believes it.
for &(slot, incoming) in &blocks[b].phi {
types[slot] = join(&types[slot], &types[incoming]);
Neg | Plus | Inc | Dec => match val_types[instr.args[0]] {
Type::Int => Type::Int,
Type::Float => Type::Float,
_ => Type::Any,
},
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
// ────────────────────────────────── Value Movers ──────────────────────────────────
Dup | PutLoc0 | PutLoc1 | PutLoc2 | PutLoc3 | PutLoc | PutLoc8 | PutLocCheck
| PutLocCheckInit | SetLoc0 | SetLoc1 | SetLoc2 | SetLoc3 | SetLoc | SetLoc8 => {
val_types[instr.args[0]].clone()
}
for instr in &code[start..end] {
// regify does not rename locals, so a load just names the local's value
// again. It must not write to it, or it would wipe what is stored there.
let load = matches!(
SetLocUninitialized => Type::Unknown,
// ────────────────────────────────────── Returns ───────────────────────────────────
Return | ReturnAsync | ReturnUndef => val_types[instr.args[0]].clone(),
_ => Type::Unknown,
};
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
let is_loc_read = matches!(
instr.op,
OpCode::GetLoc0
| OpCode::GetLoc1
@@ -155,247 +182,50 @@ pub fn infer(fn_id: usize, fns: &mut Vec<InferFn>) -> Type {
| OpCode::GetLocCheck
);
let ty = match instr.op {
// ── literals ────────────────────────────────────────────────
OpCode::PushMinus1
| OpCode::Push0
| OpCode::Push1
| OpCode::Push2
| OpCode::Push3
| OpCode::Push4
| OpCode::Push5
| OpCode::Push6
| OpCode::Push7
| OpCode::PushI8
| OpCode::PushI16
| OpCode::PushI32 => Type::Int,
OpCode::PushTrue | OpCode::PushFalse => Type::Bool,
OpCode::Undefined => Type::Undefined,
OpCode::Object => Type::Empty,
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8 => {
match instr.imm {
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
Some(JsValue::Int(_)) => Type::Int,
Some(JsValue::Float(_)) => Type::Float,
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
Some(JsValue::Undefined) => Type::Undefined,
Some(&JsValue::FnById(id)) => Type::Fn(id),
_ => Type::Any,
},
_ => Type::Any,
}
}
// ── field reads ─────────────────────────────────────────────
// Walk the shape term from the outside in: the newest write of the
// field wins, which is what makes a later write shadow an earlier.
OpCode::GetField => {
let field = match instr.imm {
ImmOperand::Atom(a) => a,
_ => AtomRef::TaggedInt(u32::MAX),
};
let mut ty = types[instr.args[0]].clone();
loop {
let (id, args) = match ty {
Type::ReturnOf((id, args)) if args.len() == 2 => (id, args),
Type::Empty => break Type::Undefined, // No fields at all
_ => break Type::Any,
};
match &fns[id].code {
CodeKind::DefineField(a) | CodeKind::PutField(a) if *a == field => {
break args[1].clone();
}
CodeKind::DefineField(_) | CodeKind::PutField(_) => {
ty = args[0].clone()
}
CodeKind::Code(_) => break Type::Any, // Not a shape we built
}
}
}
// ── arithmetic ──────────────────────────────────────────────
// A float anywhere makes the result a float; anything else can be
// a string concatenation or an object coercion, so it is Any.
OpCode::Add
| OpCode::Sub
| OpCode::Mul
| OpCode::Div
| OpCode::Mod
| OpCode::Pow => match (&types[instr.args[0]], &types[instr.args[1]]) {
(Type::Int, Type::Int) => Type::Int,
(Type::Float, Type::Float)
| (Type::Int, Type::Float)
| (Type::Float, Type::Int) => Type::Float,
_ => Type::Any,
},
OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => {
match types[instr.args[0]] {
Type::Int => Type::Int,
Type::Float => Type::Float,
_ => Type::Any,
}
}
OpCode::Lt
| OpCode::Lte
| OpCode::Gt
| OpCode::Gte
| OpCode::Eq
| OpCode::Neq
| OpCode::StrictEq
| OpCode::StrictNeq
| OpCode::Not => Type::Bool,
// ── calls ───────────────────────────────────────────────────
// demutate names a lifted callee in the immediate. Anything else
// is a callee off the stack, which this does not resolve yet.
OpCode::Call
| OpCode::Call0
| OpCode::Call1
| OpCode::Call2
| OpCode::Call3
| OpCode::TailCall => {
// demutate names its callee in the immediate; every other call
// takes it off the stack, where it is deepest and so comes last.
// Reversing the rest puts them back in written order.
let (callee, args) = match instr.imm {
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
Some(&JsValue::FnById(id)) => (Some(id), &instr.args[..]),
_ => (None, &instr.args[..]),
},
_ => match instr.args.last().map(|a| &types[*a]) {
Some(&Type::Fn(id)) => (Some(id), &instr.args[..instr.args.len() - 1]),
_ => (None, &instr.args[..]),
},
};
let result = match callee {
Some(callee) => {
let args = args.iter().rev().map(|a| types[*a].clone()).collect();
fns[callee].sigs = vec![Signature {
args,
ret: Type::Unknown,
}];
infer(callee, fns)
}
None => Type::Any,
};
// A tail call hands its result straight back, so it ends
// the function as much as a `return` does.
if instr.op == OpCode::TailCall {
ret = join(&ret, &result);
}
result
}
// A method call keeps its receiver under the callee, so the
// callee is not the last argument and the lookup above does not
// find it. Resolving that is not done yet.
OpCode::CallMethod | OpCode::TailCallMethod => {
if instr.op == OpCode::TailCallMethod {
ret = join(&ret, &Type::Any);
}
Type::Any
}
// ── globals ─────────────────────────────────────────────────
// A free name is a property of the global object, which nothing
// here models -- every built-in reaches this way too. Writing
// one (`put_var`) records nothing, because a global is not a
// value and has no id to record it against.
OpCode::GetVar | OpCode::GetVarUndef => Type::Any,
// ── arguments ───────────────────────────────────────────────
OpCode::GetArg0 => args.first().cloned().unwrap_or(Type::Undefined),
OpCode::GetArg1 => args.get(1).cloned().unwrap_or(Type::Undefined),
OpCode::GetArg2 => args.get(2).cloned().unwrap_or(Type::Undefined),
OpCode::GetArg3 => args.get(3).cloned().unwrap_or(Type::Undefined),
OpCode::GetArg => match instr.imm {
ImmOperand::Arg(k) => args.get(k as usize).cloned().unwrap_or(Type::Undefined),
_ => Type::Any,
},
// ── value movers ────────────────────────────────────────────
// These hand the value straight back, so the type goes through.
// dup pushes it twice, which the assignment below spreads over
// both destinations.
// A store into a local is one of these too: the local IS the
// destination value, so the type simply moves across.
OpCode::SetName
| OpCode::SetNameComputed
| OpCode::SetHomeObject
| OpCode::Dup
| OpCode::PutLoc0
| OpCode::PutLoc1
| OpCode::PutLoc2
| OpCode::PutLoc3
| OpCode::PutLoc
| OpCode::PutLoc8
| OpCode::PutLocCheck
| OpCode::PutLocCheckInit
| OpCode::SetLoc0
| OpCode::SetLoc1
| OpCode::SetLoc2
| OpCode::SetLoc3
| OpCode::SetLoc
| OpCode::SetLoc8 => types[instr.args[0]].clone(),
OpCode::SetLocUninitialized => Type::Unknown,
// ── returns ─────────────────────────────────────────────────
// Every way out has to feed `ret`. Missing one leaves it at
// Unknown, the BOTTOM, which a caller folds away and believes
// -- where Any would have been distrusted.
OpCode::Return | OpCode::ReturnAsync => {
ret = join(&ret, &types[instr.args[0]]);
Type::Unknown
}
OpCode::ReturnUndef => {
ret = join(&ret, &Type::Undefined);
Type::Unknown
}
// ── everything else ─────────────────────────────────────────
_ => Type::Any,
};
// A write joins with what the value already holds, so
// reassigning a local a different type widens it to Any. Almost
// every reassignment keeps the type, and join(T, T) = T, so the
// common path costs nothing.
//
// Nothing branches inside a block, so plain `types[dst] = ty`
// would be sound here and would keep `let x = 1; x = 2.5` as
// Float. That is a one-line swap; the blocks do their own job
// either way, which is to join where paths MEET.
if !load {
// TODO: is_loc_read logic is ungraceful
if is_ret {
*ret_type = join_ty(ret_type, &ty);
} else if !is_loc_read {
for &dst in &instr.dsts {
types[dst] = join(&types[dst], &ty);
val_types[dst] = join_ty(&val_types[dst], &ty);
}
}
}
// Hand the state on to every block this one reaches.
for &s in &blocks[b].succ {
for (e, t) in entry[s].iter_mut().zip(&types) {
*e = join(e, t);
Ok(())
}
pub fn atom_to_str(a: &AtomRef, atoms: &[Atom]) -> String {
match a {
AtomRef::TaggedInt(n) => n.to_string(),
AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS
.get(*i as usize)
.copied()
.unwrap_or("<predef?>")
.to_string(),
AtomRef::FileLocal(i) => atoms
.get(*i)
.map(atom_text)
.unwrap_or_else(|| format!("<file_atom {i}>")),
}
}
fn atom_text(a: &Atom) -> String {
match a {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s),
Atom::Predefined(n) => format!("<predef {n}>"),
}
}
fn js_string(s: &JsString) -> String {
match s {
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
JsString::UTF16(b) => {
let u: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u)
}
}
fns[fn_id].types = types;
fns[fn_id].blocks = blocks;
fns[fn_id].code = CodeKind::Code(code);
fns[fn_id].sigs[0].ret = ret.clone();
ret
}
-141
View File
@@ -1,141 +0,0 @@
use std::ffi::{CStr, CString, c_char, c_int, c_void};
/// Compile `source` to quickjs bytecode, as `qjsc -b` does.
///
/// `name` is the module name quickjs records and reports errors against; it
/// does not have to be a path that exists.
pub fn compile(source: &str, name: &str) -> Result<Vec<u8>, String> {
// A runtime and a context per call: compiling holds no state worth reusing,
// and this way nothing outlives the returned bytes.
unsafe {
let rt = JS_NewRuntime();
let ctx = JS_NewContext(rt);
let result = compile_in(ctx, source, name);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
result
}
}
/// The body of `compile`, so the runtime is freed on either outcome.
unsafe fn compile_in(ctx: *mut JSContext, source: &str, name: &str) -> Result<Vec<u8>, String> {
// JS_Eval requires `input[input_len] == 0`, which a &str does not promise:
// the byte past the end is whatever the allocator left there. Read as a
// token, that byte decides whether the source parses.
let source = CString::new(source).map_err(|_| "source has a nul byte".to_string())?;
let name = CString::new(name).map_err(|_| "module name has a nul byte".to_string())?;
unsafe {
// COMPILE_ONLY stops at the bytecode, so nothing in the source runs.
let obj = JS_Eval(
ctx,
source.as_ptr(),
source.as_bytes().len(),
name.as_ptr(),
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY,
);
if obj.tag == JS_TAG_EXCEPTION {
return Err(exception(ctx));
}
let mut len = 0;
let buf = JS_WriteObject(ctx, &mut len, obj, JS_WRITE_OBJ_BYTECODE);
JS_FreeValue(ctx, obj);
if buf.is_null() {
return Err(exception(ctx));
}
let bytes = std::slice::from_raw_parts(buf, len).to_vec();
js_free(ctx, buf.cast::<c_void>());
Ok(bytes)
}
}
/// The pending exception, as text. Taking it also clears it.
unsafe fn exception(ctx: *mut JSContext) -> String {
unsafe {
let err = JS_GetException(ctx);
let text = JS_ToCStringLen2(ctx, std::ptr::null_mut(), err, false);
let message = if text.is_null() {
"quickjs raised an exception it could not describe".to_string()
} else {
CStr::from_ptr(text).to_string_lossy().into_owned()
};
if !text.is_null() {
JS_FreeCString(ctx, text);
}
JS_FreeValue(ctx, err);
message
}
}
// ── the C side ─────────────────────────────────────────────────────────────
enum JSRuntime {}
enum JSContext {}
/// 16 bytes passed and returned BY VALUE. quickjs only nan-boxes on 32-bit
/// targets (`JS_NAN_BOXING`, quickjs.h), where this layout would be wrong.
#[repr(C)]
#[derive(Clone, Copy)]
struct JSValue {
u: JSValueUnion,
tag: i64,
}
#[repr(C)]
#[derive(Clone, Copy)]
union JSValueUnion {
int32: i32,
float64: f64,
ptr: *mut c_void,
}
// `JS_IsException` and `JS_ToCString` are `static inline` in the header, so
// they are not symbols to link against. These are the constants they read.
const JS_TAG_EXCEPTION: i64 = 6;
const JS_EVAL_TYPE_MODULE: c_int = 1 << 0;
const JS_EVAL_FLAG_COMPILE_ONLY: c_int = 1 << 5;
const JS_WRITE_OBJ_BYTECODE: c_int = 1 << 0;
unsafe extern "C" {
fn JS_NewRuntime() -> *mut JSRuntime;
fn JS_FreeRuntime(rt: *mut JSRuntime);
fn JS_NewContext(rt: *mut JSRuntime) -> *mut JSContext;
fn JS_FreeContext(ctx: *mut JSContext);
fn JS_Eval(
ctx: *mut JSContext,
input: *const c_char,
input_len: usize,
filename: *const c_char,
eval_flags: c_int,
) -> JSValue;
fn JS_WriteObject(
ctx: *mut JSContext,
psize: *mut usize,
obj: JSValue,
flags: c_int,
) -> *mut u8;
fn JS_GetException(ctx: *mut JSContext) -> JSValue;
fn JS_ToCStringLen2(
ctx: *mut JSContext,
plen: *mut usize,
val: JSValue,
cesu8: bool,
) -> *const c_char;
fn JS_FreeCString(ctx: *mut JSContext, ptr: *const c_char);
fn JS_FreeValue(ctx: *mut JSContext, v: JSValue);
fn js_free(ctx: *mut JSContext, ptr: *mut c_void);
}
+57
View File
@@ -0,0 +1,57 @@
use std::ffi::{c_int, c_void};
pub const JS_TAG_EXCEPTION: i64 = 6;
pub const JS_EVAL_TYPE_MODULE: c_int = 1 << 0;
pub const JS_EVAL_FLAG_COMPILE_ONLY: c_int = 1 << 5;
pub const JS_WRITE_OBJ_BYTECODE: c_int = 1 << 0;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct JSValue {
u: JSValueUnion,
pub tag: i64,
}
#[repr(C)]
#[derive(Clone, Copy)]
union JSValueUnion {
int32: i32,
float64: f64,
ptr: *mut c_void,
}
pub type JSRuntime = c_void;
pub type JSContext = c_void;
unsafe extern "C" {
pub fn JS_NewRuntime() -> *mut JSRuntime;
pub fn JS_FreeRuntime(rt: *mut JSRuntime);
pub fn JS_NewContext(rt: *mut JSRuntime) -> *mut JSContext;
pub fn JS_FreeContext(ctx: *mut JSContext);
pub fn JS_Eval(
ctx: *mut JSContext,
input: *const u8,
input_len: usize,
filename: *const u8,
eval_flags: c_int,
) -> JSValue;
pub fn JS_FreeValue(ctx: *mut JSContext, v: JSValue);
pub fn JS_WriteObject(
ctx: *mut JSContext,
psize: *mut usize,
obj: JSValue,
flags: c_int,
) -> *mut u8;
pub fn JS_GetException(ctx: *mut JSContext) -> JSValue;
pub fn JS_ToCStringLen2(
ctx: *mut JSContext,
plen: *mut usize,
val: JSValue,
cesu8: bool,
) -> *const i8;
}
+53
View File
@@ -0,0 +1,53 @@
mod ffi;
pub mod parse;
use std::ffi::{CStr, CString};
use parse::QuickJSBinary;
pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary {
unsafe {
let filename = CString::new(filename).unwrap();
let rt = ffi::JS_NewRuntime();
let ctx = ffi::JS_NewContext(rt);
let obj = ffi::JS_Eval(
ctx,
src.as_ptr(),
src.len(),
filename.as_ptr().cast(),
ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE,
);
assert!(obj.tag != ffi::JS_TAG_EXCEPTION, "{}", get_exception(ctx));
let mut len = 0;
let buf = ffi::JS_WriteObject(ctx, &mut len, obj, ffi::JS_WRITE_OBJ_BYTECODE);
ffi::JS_FreeValue(ctx, obj);
assert!(!buf.is_null());
let bytes = std::slice::from_raw_parts(buf, len).to_vec();
// TODO: Are we sure other cleanup is not necessary?
ffi::JS_FreeContext(ctx);
ffi::JS_FreeRuntime(rt);
QuickJSBinary::parse(&bytes).unwrap().1
}
}
fn get_exception(ctx: *mut ffi::JSContext) -> String {
unsafe {
let err = ffi::JS_GetException(ctx);
let text = ffi::JS_ToCStringLen2(ctx, std::ptr::null_mut(), err, false);
let message = if text.is_null() {
"quickjs raised an exception it could not describe".to_string()
} else {
CStr::from_ptr(text).to_string_lossy().into_owned()
};
message
}
}
@@ -1,5 +1,3 @@
//! This module takes the QuickJS produced binary and parses it into an identical structured represention
use std::fmt::Debug;
use std::fmt::Write;
@@ -14,11 +12,11 @@ use nom::{
use num_enum::TryFromPrimitive;
pub mod atom_names {
include!("../../c_parse/atom_names.rs");
include!("../../../c_parse/atom_names.rs");
}
pub mod opcodes {
include!("../../c_parse/opcodes.rs");
include!("../../../c_parse/opcodes.rs");
}
#[derive(Debug)]
@@ -652,7 +650,6 @@ pub struct Function {
pub flags: FunctionFlags,
pub is_strict_mode: bool,
pub name: AtomRef,
/// Regular declared params
pub arg_count: usize,
/// Num locals
pub var_count: usize,
+18 -39
View File
@@ -1,58 +1,37 @@
mod declass;
mod declosure;
mod demutate;
mod flatten;
mod infer;
mod libqjs;
mod parse;
mod regify;
use declass::declass;
use declosure::declosure;
use demutate::demutate;
use std::fs::File;
use std::io::Read;
use flatten::flatten;
use infer::infer;
use parse::QuickJSBinary;
use infer::infer_types;
use regify::regify;
fn main() {
// Load source file
let args = std::env::args().collect::<Vec<String>>();
let filename = args.get(1).unwrap();
// Compile source file
let source = std::fs::read_to_string(filename).unwrap();
let bin = libqjs::compile(&source, filename).unwrap();
let mut file = File::open(filename).unwrap();
let metadata = std::fs::metadata(filename).unwrap();
assert!(metadata.is_file());
// Parse, then flatten
let (_, parsed) = QuickJSBinary::parse(&bin).unwrap();
let (flat_fns, atoms) = flatten(parsed);
let mut src = Vec::with_capacity(metadata.len() as usize);
file.read_to_end(&mut src).unwrap();
println!("{flat_fns:#?}");
// Compile to bytecode
let binary = libqjs::compile(&src, filename);
println!("=== after regify ===");
// Process it
let (flat_fns, atoms) = flatten(binary);
let reg_fns = regify(flat_fns);
println!("{reg_fns:#?}");
println!("=== after declass ===");
let declassed = declass(reg_fns);
println!("{declassed:#?}");
println!("=== after declosure ===");
let declosured = declosure(declassed);
println!("{declosured:#?}");
println!("=== after demutate ===");
let mut infer_fns = demutate(declosured);
println!("{infer_fns:#?}");
println!("=== after infer ===");
if let Some(main_id) = infer_fns.iter().position(|f| f.name == "main") {
let ret = infer(main_id, &mut infer_fns);
println!("main returns {ret:?}");
println!("{infer_fns:#?}");
} else {
println!("no main");
}
println!("{atoms:#?}");
let type_cache = infer_types(&reg_fns, &atoms);
println!("{type_cache:#?}");
}
+89 -74
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];
for (at, instr) in code.iter().enumerate() {
if let Some(target) = jump_target(instr.imm_operands, pcs) {
starts.push(target);
}
if ends_block(instr, pcs) && at + 1 < code.len() {
starts.push(at + 1);
impl BasicBlock {
fn new(start: usize) -> Self {
Self {
start,
end: 0,
succ: vec![],
}
}
starts.sort_unstable();
starts.dedup();
let ends = |i: usize| starts.get(i + 1).copied().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![];
if let Some(t) = jump_target(last.imm_operands, pcs) {
succ.extend(starts.iter().position(|&s| s == t));
}
if falls_through(last.op) && i + 1 < starts.len() {
succ.push(i + 1);
}
Block { succ, phi: vec![] }
})
.collect();
(starts, 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()
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 terminal, next instr (if exists) is a start
if is_terminal_instr(instr) {
if i + 1 < code.len() {
blocks.push(BasicBlock::new(i + 1));
}
}
}
blocks.sort_unstable_by_key(|b| b.start);
blocks.dedup_by_key(|b| b.start);
// 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());
}
// Find block successors
for i in 0..blocks.len() {
let end = blocks[i].end;
let last_instr = &code[end - 1];
// 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);
}
// Fall through blocks are a successor
if falls_through(last_instr) && i + 1 < blocks.len() {
blocks[i].succ.push(i + 1);
}
}
blocks
}
/// 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 falls_through(instr: &Instr) -> bool {
use OpCode::*;
let uncond_jump = matches!(instr.op, Goto | Goto8 | Goto16);
!uncond_jump && !is_terminal_instr(instr)
}
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
)
}
+75 -131
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,103 +17,105 @@ 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();
walk_block(
&blocks[block_idx],
&flat_fn.code,
&mut stack,
&mut next_val,
&mut out,
);
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);
}
}
}
Self {
name: flat_fn.name,
arg_count: flat_fn.arg_count,
val_count: next_val,
cpool: flat_fn.cpool,
captures: flat_fn.captures,
code: out,
blocks,
}
}
}
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;
let v = *next_val;
*next_val += 1;
v
};
let end = starts.get(b + 1).copied().unwrap_or(flat_fn.code.len());
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
for at in starts[b]..end {
let instr = &flat_fn.code[at];
let mut reg_instr = RegInstr {
block_start: false,
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,
@@ -136,16 +132,14 @@ impl RegFn {
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();
reg_instr.args.push(v);
out_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 {
// 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),
@@ -160,7 +154,7 @@ impl RegFn {
| OpCode::PutLocCheckInit
| OpCode::SetLoc
| OpCode::SetLoc8
| OpCode::SetLocUninitialized => match reg_instr.imm {
| OpCode::SetLocUninitialized => match instr.imm_operands {
ImmOperand::Loc(k) => Some(k as usize),
_ => None,
},
@@ -168,12 +162,13 @@ impl RegFn {
_ => None,
};
match local {
Some(k) => {
reg_instr.dsts.push(k);
// 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(k);
stack.push(loc);
}
}
@@ -181,7 +176,7 @@ impl RegFn {
None if instr.op == OpCode::GetLoc0Loc1 => {
for k in [0, 1] {
stack.push(k);
reg_instr.dsts.push(k);
out_instr.dsts.push(k);
}
}
@@ -189,66 +184,15 @@ impl RegFn {
for _ in 0..n_push {
let v = fresh_val();
stack.push(v);
reg_instr.dsts.push(v);
out_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);
}
}
}
// 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,
cpool: flat_fn.cpool,
code: output,
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
out[instr_idx] = out_instr;
}
}
/// 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
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
flat_fns.into_iter().map(RegFn::from_flat).collect()
}
+139
View File
@@ -0,0 +1,139 @@
=== after regify ===
[
RegFn {
name: "test",
arg_count: 0,
var_count: 4,
value_names: [
file_atom[2],
atom("<class_fields_init>"),
file_atom[2],
file_atom[3],
],
captures: [],
cpool: [
FnById(
2,
),
FnById(
3,
),
FnById(
4,
),
],
code: [
[] = SetLocUninitialized Loc(3) [],
[] = SetLocUninitialized Loc(2) [],
[] = SetLocUninitialized Loc(0) [],
[4] = Undefined None [],
[] = SetLocUninitialized Loc(1) [],
// This is the pattern to be replaced by prototype form
[5] = PushConst8 Const(1) [],
[6, 7] = DefineClass AtomU8 { atom: file_atom[2], arg: 0 } [5, 4],
[8] = Fclosure8 Const(0) [],
[9] = DefineMethod AtomU8 { atom: file_atom[5], arg: 0 } [8, 7],
[10] = Fclosure8 Const(2) [],
[11, 12] = SetHomeObject None [10, 9],
[] = PutLoc1 None [12],
[] = Drop None [11],
[13] = SetLoc0 None [6],
[] = CloseLoc Loc(1) [],
[] = PutLoc2 None [13],
[14] = GetLocCheck Loc(2) [],
[15, 16] = Dup None [14],
[17] = CallConstructor Npop(0) [16, 15],
[] = PutLoc3 None [17],
[] = ReturnUndef None [],
],
next_val: 18,
},
RegFn {
name: "<null>", // method, unchanged
arg_count: 0,
var_count: 0,
value_names: [],
captures: [],
cpool: [],
code: [
[] = ReturnUndef None [],
],
next_val: 0,
},
RegFn {
name: "<null>", // constructor, needs to be renamed to "Counter"
arg_count: 0,
var_count: 1,
value_names: [
atom("this"),
],
captures: [
Capture {
name: "<class_fields_init>",
atom: atom("<class_fields_init>"),
from: ParentLocal(
1,
),
},
],
cpool: [],
code: [
[1] = PushThis None [],
[] = PutLoc0 None [1],
[] = CheckCtor None [],
[2] = GetVarRefCheck VarRef(0) [],
[3, 4] = Dup None [2],
[] = IfFalse8 Label { target: 14 } [4],
[5] = GetLoc0 None [],
[6, 7] = Swap None [5, 3],
[8] = CallMethod Npop(0) [7, 6],
[] = ReturnUndef None [],
],
next_val: 9,
},
RegFn {
name: "<null>", // instance fields initializer, needs thinking
arg_count: 0,
var_count: 2,
value_names: [
atom("this"),
atom("<home_object>"),
],
captures: [],
cpool: [],
code: [
[2] = SpecialObject U8(4) [],
[] = PutLoc1 None [2],
[3] = PushThis None [],
[] = PutLoc0 None [3],
[] = Goto8 Label { target: 7 } [],
[4] = GetLoc0 None [],
[5] = Push1 None [],
[6] = DefineField Atom(file_atom[4]) [5, 4],
[] = ReturnUndef None [],
],
next_val: 7,
},
]
[
String(
"../test.js",
),
String(
"test",
),
String(
"Counter",
),
String(
"c",
),
String(
"x",
),
String(
"m1",
),
]
+87
View File
@@ -0,0 +1,87 @@
=== after regify ===
[
RegFn {
name: "test",
arg_count: 0,
var_count: 2,
value_names: [
file_atom[2],
file_atom[3],
],
captures: [],
cpool: [
FnById(
2,
),
FnById(
3,
),
],
code: [
[2] = Fclosure8 Const(0) [],
[] = PutLoc0 None [2],
[] = SetLocUninitialized Loc(1) [],
[3] = GetLoc0 None [],
[4] = GetField Atom(atom("prototype")) [3],
[5] = Fclosure8 Const(1) [],
[] = PutField Atom(file_atom[5]) [5, 4],
[6] = GetLoc0 None [],
[7, 8] = Dup None [6],
[9] = CallConstructor Npop(0) [8, 7],
[] = PutLoc1 None [9],
[] = ReturnUndef None [],
],
next_val: 10,
},
RegFn {
name: "Counter",
arg_count: 0,
var_count: 1,
value_names: [
atom("this"),
],
captures: [],
cpool: [],
code: [
[1] = PushThis None [],
[] = PutLoc0 None [1],
[2] = GetLoc0 None [],
[3] = Push1 None [],
[] = PutField Atom(file_atom[4]) [3, 2],
[] = ReturnUndef None [],
],
next_val: 4,
},
RegFn {
name: "<null>",
arg_count: 0,
var_count: 0,
value_names: [],
captures: [],
cpool: [],
code: [
[] = ReturnUndef None [],
],
next_val: 0,
},
]
[
String(
"../test.js",
),
String(
"test",
),
String(
"Counter",
),
String(
"c",
),
String(
"x",
),
String(
"m1",
),
]