Temp commit
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
//! Transform: ES classes become prototype form.
|
||||
//!
|
||||
//! quickjs compiles the two OOP styles differently. A `class` is *declared*
|
||||
//! with dedicated opcodes; a prototype chain is *computed* with ordinary
|
||||
//! property and call opcodes. Prototype form is the lower of the two, so this
|
||||
//! pass lowers classes into it, leaving one object model for everything
|
||||
//! downstream.
|
||||
//!
|
||||
//! ctor, proto = define_class [template, parent] "Vec" #flags
|
||||
//! proto' = define_method [method, proto] "len" #0
|
||||
//!
|
||||
//! becomes what the same code written with prototypes would produce:
|
||||
//!
|
||||
//! proto = object the prototype
|
||||
//! proto' = define_field [proto, method] "len" an ordinary property
|
||||
//! ctor = define_field [template, proto'] "prototype" hung off the ctor
|
||||
//!
|
||||
//! A function is a plain constant here (see `declosure`), so the constructor
|
||||
//! is just the template constant that `push_const` already produced — no
|
||||
//! separate instantiation.
|
||||
//!
|
||||
//! A derived class starts its prototype as a COPY of the parent's, then
|
||||
//! installs its own methods over it, flattening the hierarchy where it is
|
||||
//! declared so no chain survives to be walked:
|
||||
//!
|
||||
//! raw = object
|
||||
//! pp = get_field parent "prototype"
|
||||
//! proto = copy_data_properties [raw, pp]
|
||||
//!
|
||||
//! Runs BEFORE declosure, so the constructor it leaves behind is an ordinary
|
||||
//! function that declosure then handles like any other.
|
||||
//!
|
||||
//! Known gaps:
|
||||
//!
|
||||
//! * **Instance field initialisers.** A class body may declare fields
|
||||
//! (`x = 1`). quickjs compiles them into a synthesised function the
|
||||
//! constructor captures by reference and calls on `this`. This pass does
|
||||
//! not reconstruct that capture, so a class with instance fields silently
|
||||
//! loses its initialisers. It is the same by-reference-capture problem as
|
||||
//! the parent-writes-a-capture gap in `declosure`.
|
||||
//! * **Getters and setters** (`define_method` with a non-zero kind) are left
|
||||
//! alone; they need property descriptors.
|
||||
//! * **The `_computed` forms** (`define_method_computed`,
|
||||
//! `define_class_computed`) take the name off the stack, not as an atom
|
||||
//! operand, and are left untouched.
|
||||
//! * **Super / parent link.** A derived class records its parent constructor
|
||||
//! in a synthetic `__parent` field (an integer name, so no atom is minted),
|
||||
//! which `deprototype`'s `super` reads. Implicit-constructor synthesis only
|
||||
//! fires when the parent class flows directly from its own `define_class`
|
||||
//! in the same function.
|
||||
//! * **Dead code.** The `push_const` that produced the constructor template
|
||||
//! stays, now doubling as the constructor value. Nothing sweeps it yet.
|
||||
|
||||
use crate::{
|
||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||
regify::{RegFn, RegInstr},
|
||||
};
|
||||
|
||||
/// Synthetic field name recording a derived class's parent constructor, read
|
||||
/// by `deprototype`'s `super` lowering. Integer-named so no atom is minted; the
|
||||
/// high value keeps it clear of the positional fields other passes use.
|
||||
pub const PARENT_LINK: u32 = 0xFFFF_FFFF;
|
||||
|
||||
/// The `__parent` field name as an operand.
|
||||
pub fn parent_link() -> ImmOperand {
|
||||
ImmOperand::Atom(AtomRef::TaggedInt(PARENT_LINK))
|
||||
}
|
||||
|
||||
/// Lower every class in the program to prototype form.
|
||||
pub fn declass(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||
// Parameter counts, read before any pass changes them.
|
||||
let arity: Vec<usize> = fns.iter().map(|f| f.arg_count).collect();
|
||||
|
||||
// Derived constructors found while lowering, with the parent's arity.
|
||||
let mut derived: Vec<(usize, usize)> = Vec::new();
|
||||
// Every function that is a class constructor.
|
||||
let mut ctors: Vec<usize> = Vec::new();
|
||||
for f in &mut fns {
|
||||
let mut next = max_val(f) + 1;
|
||||
lower(f, &arity, &mut derived, &mut ctors, &mut next);
|
||||
}
|
||||
|
||||
for id in ctors {
|
||||
if let Some(f) = fns.get_mut(id) {
|
||||
f.is_ctor = true;
|
||||
}
|
||||
}
|
||||
|
||||
// A derived class with no constructor written gets one, as if it had been.
|
||||
for (id, parent_arity) in derived {
|
||||
if let Some(f) = fns.get_mut(id) {
|
||||
write_implicit_ctor(f, parent_arity);
|
||||
}
|
||||
}
|
||||
fns
|
||||
}
|
||||
|
||||
/// A class whose prototype is still collecting methods. It is attached to its
|
||||
/// constructor only once finished, so it never escapes half-built.
|
||||
struct Pending {
|
||||
ctor: usize,
|
||||
cval: usize,
|
||||
proto_cur: usize,
|
||||
derived: bool,
|
||||
parent: usize,
|
||||
}
|
||||
|
||||
fn lower(
|
||||
f: &mut RegFn,
|
||||
arity: &[usize],
|
||||
derived_out: &mut Vec<(usize, usize)>,
|
||||
ctors_out: &mut Vec<usize>,
|
||||
next: &mut usize,
|
||||
) {
|
||||
// Which values are function-template constants, as `(value, fn id)`.
|
||||
let templates = template_ids(f);
|
||||
|
||||
let mut ctor_of: Vec<(usize, usize)> = Vec::new(); // ctor value -> fn id
|
||||
let mut pending: Vec<Pending> = Vec::new();
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len());
|
||||
|
||||
for ri in code {
|
||||
match ri.op {
|
||||
OpCode::DefineClass if ri.dsts.len() == 2 && ri.args.len() == 2 => {
|
||||
let ImmOperand::AtomU8 { arg: flags, .. } = ri.imm else {
|
||||
out.push(ri);
|
||||
continue;
|
||||
};
|
||||
let ctor = ri.dsts[0];
|
||||
let proto = ri.dsts[1];
|
||||
let template = ri.args[0]; // popped first: top of stack
|
||||
let parent = ri.args[1];
|
||||
let derived = flags & 1 != 0;
|
||||
|
||||
let ctor_fn = templates.iter().find(|(v, _)| *v == template).map(|(_, i)| *i);
|
||||
let cval = template; // function-as-const constructor
|
||||
|
||||
if derived {
|
||||
// Flatten the hierarchy: start from a copy of the parent's
|
||||
// prototype, then install this class's methods over it.
|
||||
let raw = fresh(next);
|
||||
let pp = fresh(next);
|
||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![raw], vec![]));
|
||||
out.push(instr(OpCode::GetField, proto_atom(), vec![pp], vec![parent]));
|
||||
out.push(instr(
|
||||
OpCode::CopyDataProperties,
|
||||
ImmOperand::U8(0),
|
||||
vec![proto],
|
||||
vec![raw, pp],
|
||||
));
|
||||
|
||||
if let (Some(cid), Some(pid)) = (
|
||||
ctor_fn,
|
||||
ctor_of.iter().find(|(v, _)| *v == parent).map(|(_, i)| *i),
|
||||
) {
|
||||
derived_out.push((cid, arity.get(pid).copied().unwrap_or(0)));
|
||||
}
|
||||
} else {
|
||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![proto], vec![]));
|
||||
}
|
||||
|
||||
if let Some(id) = ctor_fn {
|
||||
ctor_of.push((ctor, id));
|
||||
ctors_out.push(id);
|
||||
}
|
||||
pending.push(Pending {
|
||||
ctor,
|
||||
cval,
|
||||
proto_cur: proto,
|
||||
derived,
|
||||
parent,
|
||||
});
|
||||
}
|
||||
|
||||
// A plain method is an ordinary property on the prototype. Kind 0
|
||||
// only; getters and setters keep their opcode.
|
||||
OpCode::DefineMethod if method_kind(&ri) == Some(0) && ri.args.len() == 2 => {
|
||||
let ImmOperand::AtomU8 { atom, .. } = ri.imm else {
|
||||
out.push(ri);
|
||||
continue;
|
||||
};
|
||||
let dst = ri.dsts[0];
|
||||
let obj = ri.args[1];
|
||||
let method = ri.args[0];
|
||||
out.push(instr(OpCode::DefineField, atom_imm(atom), vec![dst], vec![obj, method]));
|
||||
if let Some(p) = pending.iter_mut().find(|p| p.proto_cur == obj) {
|
||||
p.proto_cur = dst;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
// Reading a constructor forces its prototype to be finished.
|
||||
let mut i = 0;
|
||||
while i < pending.len() {
|
||||
let c = pending[i].ctor;
|
||||
if ri.args.contains(&c) || ri.dsts.contains(&c) {
|
||||
flush(pending.remove(i), &mut out, next);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
out.push(ri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(p) = pending.pop() {
|
||||
flush(p, &mut out, next);
|
||||
}
|
||||
f.code = out;
|
||||
}
|
||||
|
||||
/// Hang the finished prototype off the constructor — this is what `.prototype`
|
||||
/// is. `define_field` returns the object, so the constructor lands in `ctor`. A
|
||||
/// derived class also records its parent constructor in `__parent`, which is
|
||||
/// what `deprototype`'s `super` reads.
|
||||
fn flush(p: Pending, out: &mut Vec<RegInstr>, next: &mut usize) {
|
||||
let hung = if p.derived { fresh(next) } else { p.ctor };
|
||||
out.push(instr(
|
||||
OpCode::DefineField,
|
||||
proto_atom(),
|
||||
vec![hung],
|
||||
vec![p.cval, p.proto_cur],
|
||||
));
|
||||
if p.derived {
|
||||
out.push(instr(
|
||||
OpCode::DefineField,
|
||||
parent_link(),
|
||||
vec![p.ctor],
|
||||
vec![hung, p.parent],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// `class Dog extends Animal {}` compiles to a constructor holding a single
|
||||
/// `init_ctor`, which builds `this` by invoking the parent with the incoming
|
||||
/// arguments. Our calling convention is fixed-arity, so we give the
|
||||
/// constructor the parent's parameters and write out the `super(...)` it stood
|
||||
/// for — after which it is an ordinary explicit constructor.
|
||||
fn write_implicit_ctor(f: &mut RegFn, parent_arity: usize) {
|
||||
if !f.code.iter().any(|i| i.op == OpCode::InitCtor) {
|
||||
return; // an explicit constructor: nothing to write
|
||||
}
|
||||
f.arg_count = parent_arity;
|
||||
let mut next = max_val(f) + 1;
|
||||
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len() + 3 + parent_arity);
|
||||
for ri in code {
|
||||
if ri.op != OpCode::InitCtor {
|
||||
out.push(ri);
|
||||
continue;
|
||||
}
|
||||
let this = ri.dsts[0];
|
||||
|
||||
// `this = super(a0 … an)`, spelled the way a written constructor is.
|
||||
let this_fn = fresh(&mut next);
|
||||
let parent = fresh(&mut next);
|
||||
let new_target = fresh(&mut next);
|
||||
out.push(instr(OpCode::SpecialObject, ImmOperand::U8(2), vec![this_fn], vec![])); // running fn
|
||||
out.push(instr(OpCode::GetSuper, ImmOperand::None, vec![parent], vec![this_fn]));
|
||||
out.push(instr(OpCode::Undefined, ImmOperand::None, vec![new_target], vec![]));
|
||||
|
||||
let mut argvals = Vec::with_capacity(parent_arity);
|
||||
for i in 0..parent_arity {
|
||||
let a = fresh(&mut next);
|
||||
out.push(instr(OpCode::GetArg, ImmOperand::Arg(i as u16), vec![a], vec![]));
|
||||
argvals.push(a);
|
||||
}
|
||||
|
||||
// call_constructor pops [func, new_target, args..], args on top; so the
|
||||
// register args run: arguments (reversed), new_target, then func.
|
||||
let mut args = Vec::with_capacity(parent_arity + 2);
|
||||
args.extend(argvals.iter().rev().copied());
|
||||
args.push(new_target);
|
||||
args.push(parent);
|
||||
out.push(instr(
|
||||
OpCode::CallConstructor,
|
||||
ImmOperand::Npop(parent_arity as u16),
|
||||
vec![this],
|
||||
args,
|
||||
));
|
||||
}
|
||||
f.code = out;
|
||||
}
|
||||
|
||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Values bound to a nested-function constant, as `(value, fn id)`. A function
|
||||
/// is loaded with `push_const` (or `fclosure`, before it is lowered).
|
||||
fn template_ids(f: &RegFn) -> Vec<(usize, usize)> {
|
||||
let mut out = Vec::new();
|
||||
for ri in &f.code {
|
||||
let is_const = matches!(
|
||||
ri.op,
|
||||
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8
|
||||
);
|
||||
if let (true, ImmOperand::Const(c), Some(&dst)) = (is_const, ri.imm, ri.dsts.first()) {
|
||||
if let Some(JsValue::FnById(id)) = f.cpool.get(c as usize) {
|
||||
out.push((dst, *id));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The method kind carried by a `define_method` (0 = plain method).
|
||||
fn method_kind(ri: &RegInstr) -> Option<u8> {
|
||||
match ri.imm {
|
||||
ImmOperand::AtomU8 { arg, .. } => Some(arg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn instr(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> RegInstr {
|
||||
RegInstr {
|
||||
dsts,
|
||||
op,
|
||||
imm,
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
fn atom_imm(a: AtomRef) -> ImmOperand {
|
||||
ImmOperand::Atom(a)
|
||||
}
|
||||
|
||||
/// The `"prototype"` field name, resolved to its predefined atom once.
|
||||
pub fn proto_atom() -> ImmOperand {
|
||||
let idx = crate::parse::atom_names::PREDEFINED_ATOMS
|
||||
.iter()
|
||||
.position(|&a| a == "prototype")
|
||||
.expect("prototype is a predefined atom");
|
||||
ImmOperand::Atom(AtomRef::Predefined(idx as u32))
|
||||
}
|
||||
|
||||
fn fresh(next: &mut usize) -> usize {
|
||||
let v = *next;
|
||||
*next += 1;
|
||||
v
|
||||
}
|
||||
|
||||
fn max_val(f: &RegFn) -> usize {
|
||||
f.code
|
||||
.iter()
|
||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
||||
.copied()
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
//! Transform: closures become plain objects.
|
||||
//!
|
||||
//! A closure becomes an object holding its captured variables plus its
|
||||
//! function. Field 0 is the function; fields 1.. are the captures, in
|
||||
//! capture-index order. Integer field names (`AtomRef::TaggedInt`) are used
|
||||
//! so no new atoms have to be minted. The object doubles as the environment,
|
||||
//! so there is no second env value to track.
|
||||
//!
|
||||
//! Three passes, one property each:
|
||||
//!
|
||||
//! 1. `pass_env_param` — every function takes the env as its LAST
|
||||
//! parameter and reads its captures out of it, so
|
||||
//! `get_var_ref` / `put_var_ref` become field
|
||||
//! access on the env.
|
||||
//! 2. `pass_closure_object` — every `fclosure` builds the object: the
|
||||
//! function in field 0, then one field per
|
||||
//! capture, each loaded from the parent frame.
|
||||
//! 3. `pass_call_site` — 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 pass 3
|
||||
//! can append the object at a uniform call site.
|
||||
//!
|
||||
//! Notes on this lowering:
|
||||
//!
|
||||
//! * A function is treated as a plain constant — a pointer into the cpool.
|
||||
//! `fclosure` is replaced by `push_const`, so it loads like any other
|
||||
//! const, and storing it in a field is no different from storing a number.
|
||||
//! * In register form the callee is the LAST call argument (it is the
|
||||
//! deepest stack slot, popped last), so pass 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.** Pass 2 can materialize 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.** Pass 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.** Pass 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`.** They carry a receiver, so
|
||||
//! they belong to the method and class transforms; pass 3 leaves them
|
||||
//! untouched. Same for `apply`.
|
||||
|
||||
use crate::{
|
||||
flatten::CaptureFrom,
|
||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||
regify::{RegFn, RegInstr},
|
||||
};
|
||||
|
||||
/// Capture sources per fn id, snapshotted before pass 1 clears the lists.
|
||||
type Sources = Vec<Vec<CaptureFrom>>;
|
||||
|
||||
pub fn declosure(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||
let sources: Sources = fns
|
||||
.iter()
|
||||
.map(|f| f.captures.iter().map(|c| c.from).collect())
|
||||
.collect();
|
||||
|
||||
for f in &mut fns {
|
||||
let mut next = max_val(f) + 1;
|
||||
pass_env_param(f, &mut next);
|
||||
pass_closure_object(f, &sources, &mut next);
|
||||
pass_call_site(f, &mut next);
|
||||
}
|
||||
fns
|
||||
}
|
||||
|
||||
// ─── 1. every function takes an env ────────────────────────────────────────
|
||||
|
||||
/// Append `env` as the last parameter — so existing arguments keep their
|
||||
/// numbers — and turn every capture access into a field access on it. A read
|
||||
/// keeps its destination value, so uses downstream do not change.
|
||||
fn pass_env_param(f: &mut RegFn, next: &mut usize) {
|
||||
let env_idx = f.arg_count;
|
||||
f.arg_count += 1;
|
||||
let env = fresh(next);
|
||||
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len() + 1);
|
||||
let mut used = false;
|
||||
|
||||
for ri in code {
|
||||
match varref(&ri) {
|
||||
Some((false, k)) => {
|
||||
used = true;
|
||||
out.push(instr(OpCode::GetField, field(k + 1), ri.dsts, vec![env]));
|
||||
}
|
||||
Some((true, k)) => {
|
||||
used = true;
|
||||
let mut args = vec![env];
|
||||
args.extend(ri.args); // the value being stored
|
||||
out.push(instr(OpCode::PutField, field(k + 1), vec![], args));
|
||||
}
|
||||
None => out.push(ri),
|
||||
}
|
||||
}
|
||||
|
||||
// Load the env once, at entry — only if a capture actually reads it.
|
||||
if used {
|
||||
out.insert(
|
||||
0,
|
||||
instr(OpCode::GetArg, ImmOperand::Arg(env_idx as u16), vec![env], vec![]),
|
||||
);
|
||||
}
|
||||
|
||||
f.code = out;
|
||||
f.captures.clear();
|
||||
}
|
||||
|
||||
// ─── 2. every fclosure builds the object ───────────────────────────────────
|
||||
|
||||
/// Replace each `fclosure` with: the function const (field 0), then one
|
||||
/// `define_field` per capture, each loaded from the parent frame. The stores
|
||||
/// chain, and the last lands in the closure's original destination.
|
||||
fn pass_closure_object(f: &mut RegFn, sources: &Sources, next: &mut usize) {
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len());
|
||||
|
||||
for ri in code {
|
||||
let child = fclosure_target(&ri, &f.cpool);
|
||||
let (Some(child_id), Some(&dst)) = (child, ri.dsts.first()) else {
|
||||
out.push(ri);
|
||||
continue;
|
||||
};
|
||||
|
||||
// The function is a plain constant — a pointer into the cpool — so it
|
||||
// loads with `push_const`, no different from any other const.
|
||||
let load_op = match ri.op {
|
||||
OpCode::Fclosure8 => OpCode::PushConst8,
|
||||
_ => OpCode::PushConst,
|
||||
};
|
||||
let fp = fresh(next);
|
||||
out.push(instr(load_op, ri.imm, vec![fp], vec![]));
|
||||
|
||||
let obj = fresh(next);
|
||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![obj], vec![]));
|
||||
|
||||
let caps: &[CaptureFrom] = sources.get(child_id).map(Vec::as_slice).unwrap_or(&[]);
|
||||
|
||||
// Field 0 is the function. It is the last store only when there are no
|
||||
// captures; otherwise the last capture lands in `dst`.
|
||||
let mut cur = if caps.is_empty() { dst } else { fresh(next) };
|
||||
out.push(instr(OpCode::DefineField, field(0), vec![cur], vec![obj, fp]));
|
||||
|
||||
for (k, from) in caps.iter().enumerate() {
|
||||
let cap = fresh(next);
|
||||
out.push(load_capture(*from, cap));
|
||||
let last = k + 1 == caps.len();
|
||||
let d = if last { dst } else { fresh(next) };
|
||||
out.push(instr(OpCode::DefineField, field(k + 1), vec![d], vec![cur, cap]));
|
||||
cur = d;
|
||||
}
|
||||
}
|
||||
|
||||
f.code = out;
|
||||
}
|
||||
|
||||
/// The instruction to materialize one capture in the parent frame. Only parent
|
||||
/// locals and args are supported; other sources are a preserved gap and load
|
||||
/// `undefined`.
|
||||
fn load_capture(from: CaptureFrom, dst: usize) -> RegInstr {
|
||||
match from {
|
||||
CaptureFrom::ParentLocal(i) => {
|
||||
instr(OpCode::GetLoc, ImmOperand::Loc(i as u16), vec![dst], vec![])
|
||||
}
|
||||
CaptureFrom::ParentArg(i) => {
|
||||
instr(OpCode::GetArg, ImmOperand::Arg(i as u16), vec![dst], vec![])
|
||||
}
|
||||
_ => instr(OpCode::Undefined, ImmOperand::None, vec![dst], vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 3. every call goes through the object ─────────────────────────────────
|
||||
|
||||
/// A call now names a closure object, not a function, so it reads field 0 and
|
||||
/// passes the object itself as the trailing env argument.
|
||||
fn pass_call_site(f: &mut RegFn, next: &mut usize) {
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len());
|
||||
|
||||
for ri in code {
|
||||
if !is_plain_call(ri.op) || ri.args.is_empty() {
|
||||
out.push(ri);
|
||||
continue;
|
||||
}
|
||||
|
||||
// In register form the callee is the deepest arg, popped last.
|
||||
let callee = *ri.args.last().unwrap();
|
||||
let fnp = fresh(next);
|
||||
out.push(instr(OpCode::GetField, field(0), vec![fnp], vec![callee]));
|
||||
|
||||
let mut args = Vec::with_capacity(ri.args.len() + 1);
|
||||
args.push(fnp);
|
||||
args.extend_from_slice(&ri.args[..ri.args.len() - 1]);
|
||||
args.push(callee); // the env, last
|
||||
|
||||
let op = if is_tail_call(ri.op) {
|
||||
OpCode::TailCall
|
||||
} else {
|
||||
OpCode::Call
|
||||
};
|
||||
out.push(instr(op, ri.imm, ri.dsts, args));
|
||||
}
|
||||
|
||||
f.code = out;
|
||||
}
|
||||
|
||||
// ─── classification ────────────────────────────────────────────────────────
|
||||
|
||||
/// `(is_write, capture_index)` if the instruction is a closure-var access.
|
||||
fn varref(ri: &RegInstr) -> Option<(bool, usize)> {
|
||||
use OpCode::*;
|
||||
let imm_idx = || match ri.imm {
|
||||
ImmOperand::VarRef(k) => k as usize,
|
||||
_ => 0,
|
||||
};
|
||||
Some(match ri.op {
|
||||
GetVarRef | GetVarRefCheck => (false, imm_idx()),
|
||||
GetVarRef0 => (false, 0),
|
||||
GetVarRef1 => (false, 1),
|
||||
GetVarRef2 => (false, 2),
|
||||
GetVarRef3 => (false, 3),
|
||||
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => (true, imm_idx()),
|
||||
PutVarRef0 => (true, 0),
|
||||
PutVarRef1 => (true, 1),
|
||||
PutVarRef2 => (true, 2),
|
||||
PutVarRef3 => (true, 3),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The child fn id if this instruction is an `fclosure` over a cpool function.
|
||||
fn fclosure_target(ri: &RegInstr, cpool: &[JsValue]) -> Option<usize> {
|
||||
if !matches!(ri.op, OpCode::Fclosure | OpCode::Fclosure8) {
|
||||
return None;
|
||||
}
|
||||
let idx = match ri.imm {
|
||||
ImmOperand::Const(c) => c as usize,
|
||||
_ => return None,
|
||||
};
|
||||
match cpool.get(idx) {
|
||||
Some(JsValue::FnById(id)) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `call_method` and `call_constructor` carry a receiver and are left alone.
|
||||
fn is_plain_call(op: OpCode) -> bool {
|
||||
use OpCode::*;
|
||||
matches!(op, Call | Call0 | Call1 | Call2 | Call3 | TailCall)
|
||||
}
|
||||
|
||||
fn is_tail_call(op: OpCode) -> bool {
|
||||
matches!(op, OpCode::TailCall)
|
||||
}
|
||||
|
||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn instr(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> RegInstr {
|
||||
RegInstr {
|
||||
dsts,
|
||||
op,
|
||||
imm,
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
/// Integer field name: 0 for the function, k+1 for capture k.
|
||||
fn field(k: usize) -> ImmOperand {
|
||||
ImmOperand::Atom(AtomRef::TaggedInt(k as u32))
|
||||
}
|
||||
|
||||
fn fresh(next: &mut usize) -> usize {
|
||||
let v = *next;
|
||||
*next += 1;
|
||||
v
|
||||
}
|
||||
|
||||
fn max_val(f: &RegFn) -> usize {
|
||||
f.code
|
||||
.iter()
|
||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
||||
.copied()
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
//! Transform: prototypes and `this` become plain objects and parameters.
|
||||
//!
|
||||
//! After `declass`, every OOP shape is prototype form: a constructor object
|
||||
//! with a `prototype` field, methods hung off that prototype, and method
|
||||
//! bodies reading an implicit `this`. This pass removes both remaining pieces
|
||||
//! of machinery — the implicit receiver, and the prototype chain.
|
||||
//!
|
||||
//! * **`this` becomes a parameter.** Every function takes `self` as its last
|
||||
//! parameter, and `this` (the `push_this` opcode) reads it. Appended, like
|
||||
//! the env, so existing arguments keep their numbers. Applied uniformly — a
|
||||
//! function that never mentions `this` still takes one — because a call
|
||||
//! site cannot know whether its callee does.
|
||||
//!
|
||||
//! * **A constructor builds and returns its object.** `new C(a)` makes a
|
||||
//! fresh object carrying the prototype's properties (`copy_data_properties`,
|
||||
//! the opcode behind `{...obj}`) and runs the constructor over it; the
|
||||
//! object is the result. Field writes on `this` thread — each `put_field`
|
||||
//! becomes a `define_field` yielding the next version — and the constructor
|
||||
//! returns the last. The instance owns its methods (declass installed them
|
||||
//! with a plain `define_field`, so `{...proto}` copies them), so a
|
||||
//! `get_field` finds one exactly as it finds a field, and no chain is
|
||||
//! walked at run time.
|
||||
//!
|
||||
//! * **A method call is a plain call.** `o.m(a)` passes `o` as the trailing
|
||||
//! receiver rather than binding it implicitly.
|
||||
//!
|
||||
//! Runs BETWEEN declass and declosure, so parameters end up `(args…, self,
|
||||
//! env)`: this pass appends `self`, declosure appends `env` last, and each call
|
||||
//! site is left with its callee still last so declosure can find it.
|
||||
//!
|
||||
//! Divergence from the closure-IR original: there a constructor read its
|
||||
//! prototype off its env, because the env WAS the `{fn, prototype}` object.
|
||||
//! Here a function is a plain constant, so the constructor has no env; it reads
|
||||
//! the running function with `special_object #2` — which is the constructor
|
||||
//! object `declass` built — and takes `prototype` off that.
|
||||
//!
|
||||
//! Known gaps:
|
||||
//!
|
||||
//! * A field written inside a branch is not threaded: the merge would need a
|
||||
//! phi, and a linear pass cannot introduce one.
|
||||
//! * `check_ctor_return` is not modelled: a constructor that explicitly
|
||||
//! returns an object should yield it instead of the built one.
|
||||
//! * `put_super_value` is untouched, so writing through `super` is not
|
||||
//! lowered. `super(...)` is handled: it runs the parent constructor and the
|
||||
//! object it returns becomes `this`.
|
||||
//! * `apply` and the `_computed` call forms are left alone.
|
||||
|
||||
use crate::{
|
||||
declass::{parent_link, proto_atom},
|
||||
parse::{ImmOperand, opcodes::OpCode},
|
||||
regify::{RegFn, RegInstr},
|
||||
};
|
||||
|
||||
/// Sentinel for "this function has no `this` value".
|
||||
const NO_SELF: usize = usize::MAX;
|
||||
|
||||
/// Lower prototypes and `this` across the program.
|
||||
pub fn deprototype(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||
for f in &mut fns {
|
||||
let mut next = max_val(f) + 1;
|
||||
let self_v = take_self_param(f, &mut next);
|
||||
if f.is_ctor {
|
||||
produce_self(f, self_v, &mut next);
|
||||
}
|
||||
lower_calls(f, &mut next);
|
||||
}
|
||||
fns
|
||||
}
|
||||
|
||||
// ─── `this` becomes a parameter ────────────────────────────────────────────
|
||||
|
||||
/// Append `self`, then point every `push_this` at it. The `push_this` opcodes
|
||||
/// vanish, replaced by a single load of the new parameter.
|
||||
fn take_self_param(f: &mut RegFn, next: &mut usize) -> usize {
|
||||
let self_idx = f.arg_count;
|
||||
f.arg_count += 1;
|
||||
|
||||
let bound: Vec<usize> = f
|
||||
.code
|
||||
.iter()
|
||||
.filter(|i| i.op == OpCode::PushThis)
|
||||
.flat_map(|i| i.dsts.iter().copied())
|
||||
.collect();
|
||||
if bound.is_empty() {
|
||||
return NO_SELF;
|
||||
}
|
||||
|
||||
let self_v = fresh(next);
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len() + 1);
|
||||
out.push(instr(OpCode::GetArg, ImmOperand::Arg(self_idx as u16), vec![self_v], vec![]));
|
||||
for mut ri in code {
|
||||
if ri.op == OpCode::PushThis {
|
||||
continue; // its reads are redirected to `self`
|
||||
}
|
||||
for v in ri.args.iter_mut().chain(ri.dsts.iter_mut()) {
|
||||
if bound.contains(v) {
|
||||
*v = self_v;
|
||||
}
|
||||
}
|
||||
out.push(ri);
|
||||
}
|
||||
f.code = out;
|
||||
self_v
|
||||
}
|
||||
|
||||
// ─── a constructor produces its object ─────────────────────────────────────
|
||||
|
||||
/// Rewrite a constructor from "fill the object you were handed" to "build one
|
||||
/// and return it". A base constructor starts from a fresh object carrying the
|
||||
/// prototype's properties, read off the running function (the constructor
|
||||
/// object). A derived one starts from whatever `super(...)` returns.
|
||||
fn produce_self(f: &mut RegFn, self_v: usize, next: &mut usize) {
|
||||
let derived = f.code.iter().any(|i| i.op == OpCode::GetSuper);
|
||||
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len() + 4);
|
||||
let mut cur = self_v;
|
||||
|
||||
if !derived {
|
||||
let cfn = fresh(next);
|
||||
let proto = fresh(next);
|
||||
let raw = fresh(next);
|
||||
let obj = fresh(next);
|
||||
out.push(instr(OpCode::SpecialObject, ImmOperand::U8(2), vec![cfn], vec![])); // running fn
|
||||
out.push(instr(OpCode::GetField, proto_atom(), vec![proto], vec![cfn]));
|
||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![raw], vec![]));
|
||||
out.push(instr(OpCode::CopyDataProperties, ImmOperand::U8(0), vec![obj], vec![raw, proto]));
|
||||
cur = obj;
|
||||
}
|
||||
|
||||
for mut ri in code {
|
||||
// Reads of `this` see the current object version. Only uses are
|
||||
// repointed — `self`'s own load keeps its dead destination, so `cur`'s
|
||||
// definition is not duplicated.
|
||||
for v in ri.args.iter_mut() {
|
||||
if *v == self_v {
|
||||
*v = cur;
|
||||
}
|
||||
}
|
||||
match ri.op {
|
||||
// `this.f = v` becomes ordinary construction, threading a new
|
||||
// version of the object.
|
||||
OpCode::PutField if ri.args.len() == 2 && ri.args[1] == cur => {
|
||||
let d = fresh(next);
|
||||
let val = ri.args[0];
|
||||
out.push(instr(OpCode::DefineField, ri.imm, vec![d], vec![cur, val]));
|
||||
cur = d;
|
||||
}
|
||||
// `super(...)` produced `this`; thread from its result.
|
||||
OpCode::CallConstructor if derived && !ri.dsts.is_empty() => {
|
||||
cur = ri.dsts[0];
|
||||
out.push(ri);
|
||||
}
|
||||
OpCode::Return | OpCode::ReturnUndef => {
|
||||
out.push(instr(OpCode::Return, ImmOperand::None, vec![], vec![cur]));
|
||||
}
|
||||
_ => out.push(ri),
|
||||
}
|
||||
}
|
||||
f.code = out;
|
||||
}
|
||||
|
||||
// ─── calls carry their receiver ────────────────────────────────────────────
|
||||
|
||||
fn lower_calls(f: &mut RegFn, next: &mut usize) {
|
||||
let code = std::mem::take(&mut f.code);
|
||||
let mut out = Vec::with_capacity(code.len());
|
||||
|
||||
// `get_field2`'s kept receiver echoes the object it read; record what each
|
||||
// echo really is, and substitute at the end.
|
||||
let mut kept: Vec<(usize, usize)> = Vec::new();
|
||||
|
||||
for ri in code {
|
||||
match ri.op {
|
||||
// `new` is gone, so the guard against calling without it is too.
|
||||
OpCode::CheckCtor => {}
|
||||
|
||||
// `super` is the parent constructor, recorded by declass as a
|
||||
// field on the constructor object.
|
||||
OpCode::GetSuper if ri.args.len() == 1 && !ri.dsts.is_empty() => {
|
||||
out.push(instr(OpCode::GetField, parent_link(), vec![ri.dsts[0]], vec![ri.args[0]]));
|
||||
}
|
||||
|
||||
// `o.m(a) -> m(a, o)`. Register args are [call args.., func, recv];
|
||||
// the new call keeps its callee (func) last so declosure finds it.
|
||||
OpCode::CallMethod | OpCode::TailCallMethod if ri.args.len() >= 2 => {
|
||||
let n = ri.args.len();
|
||||
let recv = ri.args[n - 1];
|
||||
let func = ri.args[n - 2];
|
||||
let mut args = Vec::with_capacity(n);
|
||||
args.extend_from_slice(&ri.args[..n - 2]);
|
||||
args.push(recv); // self
|
||||
args.push(func); // callee, last
|
||||
let op = tail_or_plain(ri.op);
|
||||
out.push(instr(op, ri.imm, ri.dsts, args));
|
||||
}
|
||||
|
||||
// `new C(a)`: the constructor now builds and returns the object, so
|
||||
// this is a plain call yielding it. `self` is undefined; the
|
||||
// dropped `args[n-2]` was new.target.
|
||||
OpCode::CallConstructor if ri.args.len() >= 2 => {
|
||||
let n = ri.args.len();
|
||||
let func = ri.args[n - 1];
|
||||
let undef = fresh(next);
|
||||
out.push(instr(OpCode::Undefined, ImmOperand::None, vec![undef], vec![]));
|
||||
let mut args = Vec::with_capacity(n);
|
||||
args.extend_from_slice(&ri.args[..n - 2]);
|
||||
args.push(undef); // self
|
||||
args.push(func); // callee, last
|
||||
out.push(instr(OpCode::Call, ri.imm, ri.dsts, args));
|
||||
}
|
||||
|
||||
// `get_field2` kept a receiver under the value it read. Receivers
|
||||
// are explicit arguments now, so the kept copy is just the object,
|
||||
// and this is an ordinary read.
|
||||
OpCode::GetField2 if ri.dsts.len() == 2 && ri.args.len() == 1 => {
|
||||
kept.push((ri.dsts[0], ri.args[0]));
|
||||
out.push(instr(OpCode::GetField, ri.imm, vec![ri.dsts[1]], vec![ri.args[0]]));
|
||||
}
|
||||
|
||||
// A plain call has no receiver, so `this` is undefined. Insert it
|
||||
// before the callee, which must stay last.
|
||||
op if is_plain_call(op) && !ri.args.is_empty() => {
|
||||
let n = ri.args.len();
|
||||
let callee = ri.args[n - 1];
|
||||
let undef = fresh(next);
|
||||
out.push(instr(OpCode::Undefined, ImmOperand::None, vec![undef], vec![]));
|
||||
let mut args = Vec::with_capacity(n + 1);
|
||||
args.extend_from_slice(&ri.args[..n - 1]);
|
||||
args.push(undef); // self
|
||||
args.push(callee); // callee, last
|
||||
out.push(instr(tail_or_plain(ri.op), ri.imm, ri.dsts, args));
|
||||
}
|
||||
|
||||
_ => out.push(ri),
|
||||
}
|
||||
}
|
||||
|
||||
// Point the echoed receivers at the object itself.
|
||||
if !kept.is_empty() {
|
||||
for ri in &mut out {
|
||||
for v in ri.args.iter_mut().chain(ri.dsts.iter_mut()) {
|
||||
if let Some((_, real)) = kept.iter().find(|(k, _)| k == v) {
|
||||
*v = *real;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f.code = out;
|
||||
}
|
||||
|
||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn is_plain_call(op: OpCode) -> bool {
|
||||
use OpCode::*;
|
||||
matches!(op, Call | Call0 | Call1 | Call2 | Call3 | TailCall)
|
||||
}
|
||||
|
||||
fn tail_or_plain(op: OpCode) -> OpCode {
|
||||
use OpCode::*;
|
||||
if matches!(op, TailCall | TailCallMethod) {
|
||||
TailCall
|
||||
} else {
|
||||
Call
|
||||
}
|
||||
}
|
||||
|
||||
fn instr(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> RegInstr {
|
||||
RegInstr {
|
||||
dsts,
|
||||
op,
|
||||
imm,
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh(next: &mut usize) -> usize {
|
||||
let v = *next;
|
||||
*next += 1;
|
||||
v
|
||||
}
|
||||
|
||||
fn max_val(f: &RegFn) -> usize {
|
||||
f.code
|
||||
.iter()
|
||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
||||
.copied()
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
+27
-3
@@ -1,12 +1,18 @@
|
||||
mod declass;
|
||||
mod declosure;
|
||||
mod deprototype;
|
||||
mod flatten;
|
||||
mod parse;
|
||||
mod ssa;
|
||||
mod regify;
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use declass::declass;
|
||||
use declosure::declosure;
|
||||
use deprototype::deprototype;
|
||||
use flatten::flatten;
|
||||
use parse::QuickJSBinary;
|
||||
use ssa::convert_ssa;
|
||||
use regify::regify;
|
||||
|
||||
const QJSC: &str = "../third_party/quickjs/build/qjsc";
|
||||
const BIN_OUT: &str = "/tmp/qjsrt.bin";
|
||||
@@ -30,5 +36,23 @@ fn main() {
|
||||
let (flat_fns, atoms) = flatten(parsed);
|
||||
|
||||
println!("{atoms:#?}");
|
||||
convert_ssa(flat_fns);
|
||||
let reg_fns = regify(flat_fns);
|
||||
|
||||
println!("=== after regify ===");
|
||||
println!("{reg_fns:#?}");
|
||||
|
||||
let declassed = declass(reg_fns);
|
||||
|
||||
println!("=== after declass ===");
|
||||
println!("{declassed:#?}");
|
||||
|
||||
let deprototyped = deprototype(declassed);
|
||||
|
||||
println!("=== after deprototype ===");
|
||||
println!("{deprototyped:#?}");
|
||||
|
||||
let declosured = declosure(deprototyped);
|
||||
|
||||
println!("=== after declosure ===");
|
||||
println!("{declosured:#?}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
flatten::{Capture, FlatFn},
|
||||
parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
|
||||
};
|
||||
|
||||
pub struct RegInstr {
|
||||
pub dsts: Vec<usize>,
|
||||
pub op: OpCode,
|
||||
pub imm: ImmOperand,
|
||||
pub args: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Debug for RegInstr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:?} = {:?} {:?} {:?}",
|
||||
self.dsts, self.op, self.imm, self.args
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `FlatFn` with its stack bytecode lowered to register code.
|
||||
#[derive(Debug)]
|
||||
pub struct RegFn {
|
||||
pub name: String,
|
||||
pub arg_count: usize,
|
||||
pub var_count: usize,
|
||||
pub captures: Vec<Capture>,
|
||||
pub cpool: Vec<JsValue>,
|
||||
pub code: Vec<RegInstr>,
|
||||
/// Set by `declass` when this function is a class constructor.
|
||||
pub is_ctor: bool,
|
||||
}
|
||||
|
||||
fn regify_walk(code: &[Instr], var_count: usize) -> Vec<RegInstr> {
|
||||
let mut stack = vec![];
|
||||
let mut output = vec![];
|
||||
let mut next_val = var_count;
|
||||
|
||||
let mut fresh_val = || {
|
||||
let v = next_val;
|
||||
next_val += 1;
|
||||
v
|
||||
};
|
||||
|
||||
for instr in code {
|
||||
let mut reg_instr = RegInstr {
|
||||
dsts: vec![],
|
||||
op: instr.op,
|
||||
imm: instr.imm_operands,
|
||||
args: vec![],
|
||||
};
|
||||
|
||||
let extra_pop = match instr.op {
|
||||
OpCode::Call0 => 0,
|
||||
OpCode::Call1 => 1,
|
||||
OpCode::Call2 => 2,
|
||||
OpCode::Call3 => 3,
|
||||
_ => match instr.imm_operands {
|
||||
ImmOperand::Npop(npop) => npop as usize,
|
||||
ImmOperand::NpopU16 { npop, .. } => npop as usize,
|
||||
_ => 0,
|
||||
},
|
||||
};
|
||||
|
||||
let info = instr.op.info();
|
||||
let n_pop = info.n_pop as usize + extra_pop;
|
||||
let n_push = info.n_push as usize;
|
||||
|
||||
for _ in 0..n_pop {
|
||||
let v = stack.pop().unwrap();
|
||||
reg_instr.args.push(v);
|
||||
}
|
||||
|
||||
for _ in 0..n_push {
|
||||
let v = fresh_val();
|
||||
stack.push(v);
|
||||
reg_instr.dsts.push(v);
|
||||
}
|
||||
|
||||
output.push(reg_instr);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Lower every function's stack code to register code, keeping the rest.
|
||||
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
||||
flat_fns
|
||||
.into_iter()
|
||||
.map(|f| RegFn {
|
||||
code: regify_walk(&f.code, f.var_count),
|
||||
name: f.name,
|
||||
arg_count: f.arg_count,
|
||||
var_count: f.var_count,
|
||||
captures: f.captures,
|
||||
cpool: f.cpool,
|
||||
is_ctor: false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::{
|
||||
flatten::FlatFn,
|
||||
parse::{ImmOperand, opcodes::OpCode},
|
||||
};
|
||||
|
||||
fn is_jump_op(op: OpCode) -> bool {
|
||||
use OpCode::*;
|
||||
matches!(
|
||||
op,
|
||||
Goto | Goto8 | Goto16 | IfFalse | IfTrue | IfFalse8 | IfTrue8
|
||||
)
|
||||
}
|
||||
|
||||
fn is_terminal_op(op: OpCode) -> bool {
|
||||
use OpCode::*;
|
||||
matches!(op, Return | ReturnUndef | ReturnAsync | Throw)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Terminator {
|
||||
UncondJump(usize),
|
||||
CondJump {
|
||||
truthy: bool,
|
||||
value: usize,
|
||||
true_block: usize,
|
||||
false_block: usize,
|
||||
},
|
||||
Terminal,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BlockSpan {
|
||||
start: usize,
|
||||
end: usize,
|
||||
term: Terminator,
|
||||
}
|
||||
|
||||
fn find_basic_blocks(flat_fn: &FlatFn) -> Vec<BlockSpan> {
|
||||
// Find block starts: the first instruction, every jump target,
|
||||
// and whatever follows a jump or a return
|
||||
let code = &flat_fn.code;
|
||||
let mut starts = vec![0];
|
||||
|
||||
for (i, instr) in code.iter().enumerate() {
|
||||
// If cond/uncond jump, jump target is a start, next instr (if exists) is another start
|
||||
if is_jump_op(instr.op) {
|
||||
let jump_target = match instr.imm_operands {
|
||||
ImmOperand::Label { target } => code.iter().position(|x| x.pc == target).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
starts.push(jump_target);
|
||||
|
||||
if i + 1 < code.len() {
|
||||
starts.push(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If terminal, next instr (if exists) is a start
|
||||
if is_terminal_op(instr.op) {
|
||||
if i + 1 < code.len() {
|
||||
starts.push(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
starts.sort_unstable();
|
||||
starts.dedup();
|
||||
|
||||
// Create blocks, one per block start
|
||||
let blocks = starts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &start)| BlockSpan {
|
||||
start,
|
||||
end: starts.get(i + 1).copied().unwrap_or(code.len()),
|
||||
term: Terminator::Terminal,
|
||||
})
|
||||
.collect::<Vec<BlockSpan>>();
|
||||
|
||||
blocks
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SsaInstr {
|
||||
dsts: Vec<usize>,
|
||||
op: OpCode,
|
||||
imm: ImmOperand,
|
||||
args: Vec<usize>,
|
||||
}
|
||||
|
||||
fn ssa_walk(flat_fn: &FlatFn) -> Vec<SsaInstr> {
|
||||
//let mut locals = vec![];
|
||||
let mut stack = vec![];
|
||||
let mut output = vec![];
|
||||
let mut next_val = 0;
|
||||
|
||||
let mut fresh_val = || {
|
||||
let v = next_val;
|
||||
next_val += 1;
|
||||
v
|
||||
};
|
||||
|
||||
for instr in &flat_fn.code {
|
||||
let mut ssa_instr = SsaInstr {
|
||||
dsts: vec![],
|
||||
op: instr.op,
|
||||
imm: instr.imm_operands,
|
||||
args: vec![],
|
||||
};
|
||||
|
||||
let info = instr.op.info();
|
||||
let n_pop = info.n_pop;
|
||||
let n_push = info.n_push;
|
||||
|
||||
for _ in 0..n_pop {
|
||||
let v = stack.pop().unwrap();
|
||||
ssa_instr.args.push(v);
|
||||
}
|
||||
|
||||
for _ in 0..n_push {
|
||||
let v = fresh_val();
|
||||
stack.push(v);
|
||||
ssa_instr.dsts.push(v);
|
||||
}
|
||||
|
||||
output.push(ssa_instr);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn compute_terminators(ssa: &[SsaInstr], blocks: &mut [BlockSpan]) {
|
||||
for instr in ssa {
|
||||
if is_jump_op(instr.op) {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_ssa(flat_fns: Vec<FlatFn>) {
|
||||
for flat_fn in flat_fns {
|
||||
let blocks = find_basic_blocks(&flat_fn);
|
||||
let ssa = ssa_walk(&flat_fn);
|
||||
|
||||
println!("{flat_fn:#?}");
|
||||
println!("{blocks:#?}");
|
||||
println!("{ssa:#?}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user