Big Rewrite
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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(®_fn.cpool, as_const(seq_instr[0].imm)),
|
||||
field_init_fn: find_field_init_fn(seq_instr, num_methods, ®_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))
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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, ®_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))
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+178
-348
@@ -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![],
|
||||
};
|
||||
|
||||
// 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)),
|
||||
|
||||
// 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,
|
||||
|
||||
CodeKind::Code(_) => {
|
||||
let taken = std::mem::replace(&mut fns[fn_id].code, CodeKind::Code(vec![]));
|
||||
let CodeKind::Code(code) = taken else {
|
||||
unreachable!()
|
||||
};
|
||||
code
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub struct InferRequest {
|
||||
fn_id: usize,
|
||||
arg_types: Vec<Type>,
|
||||
}
|
||||
};
|
||||
|
||||
if fns[fn_id].sigs.is_empty() {
|
||||
fns[fn_id].sigs.push(Signature {
|
||||
args: vec![],
|
||||
ret: Type::Unknown,
|
||||
#[derive(Debug)]
|
||||
pub struct InferResult {
|
||||
val_types: Vec<Type>,
|
||||
ret_type: Type,
|
||||
}
|
||||
|
||||
pub type TypeCache = HashMap<InferRequest, InferResult>;
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
// 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>>();
|
||||
|
||||
if let Some(first) = starts.first_mut() {
|
||||
*first = 0;
|
||||
cache
|
||||
}
|
||||
|
||||
// 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;
|
||||
fn walk_fn(
|
||||
req: &InferRequest,
|
||||
reg_fns: &[RegFn],
|
||||
cache: &mut TypeCache,
|
||||
) -> Result<InferResult, InferRequest> {
|
||||
let f = ®_fns[req.fn_id];
|
||||
let mut val_types = vec![Type::Unknown; f.val_count];
|
||||
let mut ret_type = Type::Unknown;
|
||||
|
||||
// What each block is entered with, joined from the blocks that reach it.
|
||||
let mut entry = vec![vec![Type::Unknown; types.len()]; starts.len()];
|
||||
let mut worklist = vec![0];
|
||||
|
||||
for (b, &start) in starts.iter().enumerate() {
|
||||
let end = starts.get(b + 1).copied().unwrap_or(code.len());
|
||||
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)?;
|
||||
|
||||
// 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);
|
||||
worklist.extend(block.succ.iter());
|
||||
}
|
||||
|
||||
// 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]);
|
||||
Ok(InferResult {
|
||||
val_types,
|
||||
ret_type,
|
||||
})
|
||||
}
|
||||
|
||||
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!(
|
||||
fn walk_block(
|
||||
block: &BasicBlock,
|
||||
arg_types: &[Type],
|
||||
f: &RegFn,
|
||||
val_types: &mut [Type],
|
||||
ret_type: &mut Type,
|
||||
) -> Result<(), InferRequest> {
|
||||
use OpCode::*;
|
||||
|
||||
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,
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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}>")),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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}>"),
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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(®_fns, &atoms);
|
||||
|
||||
println!("{type_cache:#?}");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
impl BasicBlock {
|
||||
fn new(start: usize) -> Self {
|
||||
Self {
|
||||
start,
|
||||
end: 0,
|
||||
succ: vec![],
|
||||
}
|
||||
|
||||
if ends_block(instr, pcs) && at + 1 < code.len() {
|
||||
starts.push(at + 1);
|
||||
}
|
||||
}
|
||||
|
||||
starts.sort_unstable();
|
||||
starts.dedup();
|
||||
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)];
|
||||
|
||||
let ends = |i: usize| starts.get(i + 1).copied().unwrap_or(code.len());
|
||||
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));
|
||||
|
||||
// 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 i + 1 < code.len() {
|
||||
blocks.push(BasicBlock::new(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
if falls_through(last.op) && i + 1 < starts.len() {
|
||||
succ.push(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Block { succ, phi: vec![] }
|
||||
})
|
||||
.collect();
|
||||
blocks.sort_unstable_by_key(|b| b.start);
|
||||
blocks.dedup_by_key(|b| b.start);
|
||||
|
||||
(starts, blocks)
|
||||
// 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());
|
||||
}
|
||||
|
||||
/// 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()
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
// Fall through blocks are a successor
|
||||
if falls_through(last_instr) && i + 1 < blocks.len() {
|
||||
blocks[i].succ.push(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
blocks
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
+76
-132
@@ -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,65 +184,14 @@ 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);
|
||||
out[instr_idx] = out_instr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user