New direction
This commit is contained in:
@@ -1,336 +0,0 @@
|
|||||||
//! 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.
|
|
||||||
//!
|
|
||||||
//! A derived class starts its prototype as a COPY of the parent's, then
|
|
||||||
//! installs its own methods over it. That flattens the hierarchy where it is
|
|
||||||
//! declared, so no chain survives to be walked at run time:
|
|
||||||
//!
|
|
||||||
//! empty = object
|
|
||||||
//! parent_proto = get_field parent "prototype"
|
|
||||||
//! proto = copy_data_properties [empty, parent_proto]
|
|
||||||
//!
|
|
||||||
//! 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::{Fresh, RegFn, RegInstr, int_field},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Synthetic field recording a derived class's parent constructor, read by
|
|
||||||
/// `deprototype`'s `super` lowering. The high value keeps it clear of the
|
|
||||||
/// small positional fields the other passes use.
|
|
||||||
pub const PARENT_LINK: u32 = 0xFFFF_FFFF;
|
|
||||||
|
|
||||||
/// The `__parent` field name as an operand.
|
|
||||||
pub fn parent_link() -> ImmOperand {
|
|
||||||
int_field(PARENT_LINK)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `"prototype"` field name, resolved to its predefined atom.
|
|
||||||
pub fn proto_atom() -> ImmOperand {
|
|
||||||
let index = crate::parse::atom_names::PREDEFINED_ATOMS
|
|
||||||
.iter()
|
|
||||||
.position(|&name| name == "prototype")
|
|
||||||
.expect("prototype is a predefined atom");
|
|
||||||
ImmOperand::Atom(AtomRef::Predefined(index as u32))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lower every class in the program to prototype form.
|
|
||||||
pub fn declass(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
|
||||||
// Parameter counts, read before anything changes them.
|
|
||||||
let arity: Vec<usize> = fns.iter().map(|f| f.arg_count).collect();
|
|
||||||
|
|
||||||
// Filled in while lowering: every constructor's fn id, and each derived
|
|
||||||
// constructor paired with its parent's arity.
|
|
||||||
let mut ctors: Vec<usize> = Vec::new();
|
|
||||||
let mut derived_ctors: Vec<(usize, usize)> = Vec::new();
|
|
||||||
|
|
||||||
for f in &mut fns {
|
|
||||||
lower_classes_in(f, &arity, &mut ctors, &mut derived_ctors);
|
|
||||||
}
|
|
||||||
|
|
||||||
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_ctors {
|
|
||||||
if let Some(f) = fns.get_mut(id) {
|
|
||||||
expand_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 OpenClass {
|
|
||||||
/// The constructor value the rest of the code uses.
|
|
||||||
ctor: usize,
|
|
||||||
/// The constructor's function constant.
|
|
||||||
template: usize,
|
|
||||||
/// The latest version of the prototype object.
|
|
||||||
proto: usize,
|
|
||||||
derived: bool,
|
|
||||||
parent: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lower every class declared in one function.
|
|
||||||
fn lower_classes_in(
|
|
||||||
f: &mut RegFn,
|
|
||||||
arity: &[usize],
|
|
||||||
ctors: &mut Vec<usize>,
|
|
||||||
derived_ctors: &mut Vec<(usize, usize)>,
|
|
||||||
) {
|
|
||||||
let templates = function_templates(f);
|
|
||||||
let mut fresh = Fresh::for_fn(f);
|
|
||||||
|
|
||||||
// Constructor values seen so far, as (value, fn id), so a derived class
|
|
||||||
// declared later in this function can find its parent.
|
|
||||||
let mut ctor_of: Vec<(usize, usize)> = Vec::new();
|
|
||||||
let mut open: Vec<OpenClass> = Vec::new();
|
|
||||||
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len());
|
|
||||||
|
|
||||||
for instr in code {
|
|
||||||
match instr.op {
|
|
||||||
OpCode::DefineClass if instr.dsts.len() == 2 && instr.args.len() == 2 => {
|
|
||||||
let ImmOperand::AtomU8 { arg: flags, .. } = instr.imm else {
|
|
||||||
out.push(instr);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let ctor = instr.dsts[0];
|
|
||||||
let proto = instr.dsts[1];
|
|
||||||
let template = instr.args[0]; // popped first: it was on top of the stack
|
|
||||||
let parent = instr.args[1];
|
|
||||||
let derived = flags & 1 != 0;
|
|
||||||
let ctor_fn = templates.iter().find(|(v, _)| *v == template).map(|(_, id)| *id);
|
|
||||||
|
|
||||||
if derived {
|
|
||||||
// Flatten the hierarchy here: the prototype starts as a copy
|
|
||||||
// of the parent's, and this class's own methods go over it.
|
|
||||||
let empty = fresh.new_value();
|
|
||||||
let parent_proto = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![empty], vec![]));
|
|
||||||
out.push(RegInstr::new(OpCode::GetField, proto_atom(), vec![parent_proto], vec![parent]));
|
|
||||||
out.push(RegInstr::new(
|
|
||||||
OpCode::CopyDataProperties,
|
|
||||||
ImmOperand::U8(0),
|
|
||||||
vec![proto],
|
|
||||||
vec![empty, parent_proto],
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![proto], vec![]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = ctor_fn {
|
|
||||||
// A derived class whose parent was declared here is paired
|
|
||||||
// with the parent's arity, for the implicit constructor.
|
|
||||||
let parent_fn = ctor_of.iter().find(|(v, _)| *v == parent).map(|(_, id)| *id);
|
|
||||||
if let (true, Some(parent_fn)) = (derived, parent_fn) {
|
|
||||||
derived_ctors.push((id, arity.get(parent_fn).copied().unwrap_or(0)));
|
|
||||||
}
|
|
||||||
ctors.push(id);
|
|
||||||
ctor_of.push((ctor, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
open.push(OpenClass {
|
|
||||||
ctor,
|
|
||||||
template,
|
|
||||||
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(&instr) == Some(0) && instr.args.len() == 2 => {
|
|
||||||
let ImmOperand::AtomU8 { atom: name, .. } = instr.imm else {
|
|
||||||
out.push(instr);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let newer_proto = instr.dsts[0];
|
|
||||||
let method = instr.args[0];
|
|
||||||
let proto = instr.args[1];
|
|
||||||
out.push(RegInstr::new(
|
|
||||||
OpCode::DefineField,
|
|
||||||
ImmOperand::Atom(name),
|
|
||||||
vec![newer_proto],
|
|
||||||
vec![proto, method],
|
|
||||||
));
|
|
||||||
|
|
||||||
// The class's prototype is now this newer version.
|
|
||||||
if let Some(class) = open.iter_mut().find(|c| c.proto == proto) {
|
|
||||||
class.proto = newer_proto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
// Reading a constructor forces its prototype to be finished.
|
|
||||||
let mut i = 0;
|
|
||||||
while i < open.len() {
|
|
||||||
if instr.mentions(open[i].ctor) {
|
|
||||||
finish_class(open.remove(i), &mut out, &mut fresh);
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push(instr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whatever is still open finishes at the end of the function.
|
|
||||||
while let Some(class) = open.pop() {
|
|
||||||
finish_class(class, &mut out, &mut fresh);
|
|
||||||
}
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hang the finished prototype off the constructor — that is what `.prototype`
|
|
||||||
/// is. `define_field` returns the object, so the constructor lands in `ctor`.
|
|
||||||
/// A derived class also records its parent in `__parent`, for `super`.
|
|
||||||
fn finish_class(class: OpenClass, out: &mut Vec<RegInstr>, fresh: &mut Fresh) {
|
|
||||||
let with_proto = if class.derived { fresh.new_value() } else { class.ctor };
|
|
||||||
out.push(RegInstr::new(
|
|
||||||
OpCode::DefineField,
|
|
||||||
proto_atom(),
|
|
||||||
vec![with_proto],
|
|
||||||
vec![class.template, class.proto],
|
|
||||||
));
|
|
||||||
if class.derived {
|
|
||||||
out.push(RegInstr::new(
|
|
||||||
OpCode::DefineField,
|
|
||||||
parent_link(),
|
|
||||||
vec![class.ctor],
|
|
||||||
vec![with_proto, class.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 expand_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 fresh = Fresh::for_fn(f);
|
|
||||||
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len() + 3 + parent_arity);
|
|
||||||
for instr in code {
|
|
||||||
if instr.op != OpCode::InitCtor {
|
|
||||||
out.push(instr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let this = instr.dsts[0];
|
|
||||||
|
|
||||||
// `this = super(a0 … an)`, spelled the way a written constructor is.
|
|
||||||
let running_fn = fresh.new_value();
|
|
||||||
let parent = fresh.new_value();
|
|
||||||
let new_target = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::SpecialObject, ImmOperand::U8(2), vec![running_fn], vec![]));
|
|
||||||
out.push(RegInstr::new(OpCode::GetSuper, ImmOperand::None, vec![parent], vec![running_fn]));
|
|
||||||
out.push(RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![new_target], vec![]));
|
|
||||||
|
|
||||||
let mut forwarded = Vec::with_capacity(parent_arity);
|
|
||||||
for i in 0..parent_arity {
|
|
||||||
let arg = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::GetArg, ImmOperand::Arg(i as u16), vec![arg], vec![]));
|
|
||||||
forwarded.push(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In register form a call's args run in pop order: the last argument
|
|
||||||
// first, then new.target, then the callee.
|
|
||||||
let mut args = Vec::with_capacity(parent_arity + 2);
|
|
||||||
args.extend(forwarded.iter().rev().copied());
|
|
||||||
args.push(new_target);
|
|
||||||
args.push(parent);
|
|
||||||
out.push(RegInstr::new(
|
|
||||||
OpCode::CallConstructor,
|
|
||||||
ImmOperand::Npop(parent_arity as u16),
|
|
||||||
vec![this],
|
|
||||||
args,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Values holding a nested-function constant, as (value, fn id). A function
|
|
||||||
/// loads with `push_const`, or with `fclosure` until declosure lowers it.
|
|
||||||
fn function_templates(f: &RegFn) -> Vec<(usize, usize)> {
|
|
||||||
let mut templates = Vec::new();
|
|
||||||
for instr in &f.code {
|
|
||||||
let loads_function = matches!(
|
|
||||||
instr.op,
|
|
||||||
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8
|
|
||||||
);
|
|
||||||
let (ImmOperand::Const(index), Some(&value)) = (instr.imm, instr.dsts.first()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !loads_function {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(JsValue::FnById(id)) = f.cpool.get(index as usize) {
|
|
||||||
templates.push((value, *id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templates
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The method kind carried by a `define_method` (0 = plain method).
|
|
||||||
fn method_kind(instr: &RegInstr) -> Option<u8> {
|
|
||||||
match instr.imm {
|
|
||||||
ImmOperand::AtomU8 { arg, .. } => Some(arg),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! 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::{parse::Atom, regify::RegFn};
|
||||||
|
|
||||||
|
/// A class's constructor and its hoisted field initialiser, as fn ids into the
|
||||||
|
/// flat function list. Read by step 1, consumed by step 2.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct FieldInit {
|
||||||
|
ctor: usize,
|
||||||
|
fields: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn declass(reg_fns: Vec<RegFn>, _atoms: &[Atom]) -> Vec<RegFn> {
|
||||||
|
// Read the constructor <-> field-initialiser links before step 1 rewrites
|
||||||
|
// the class builds that carry them.
|
||||||
|
let field_inits = step_1::field_init_links(®_fns);
|
||||||
|
|
||||||
|
let mut reg_fns: Vec<RegFn> = reg_fns.into_iter().map(step_1::rewrite).collect();
|
||||||
|
|
||||||
|
step_2::splice(&mut reg_fns, &field_inits);
|
||||||
|
reg_fns
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
//! Step 1 of declass: rewrite how a class is built, in prototype form.
|
||||||
|
//!
|
||||||
|
//! A class-build cluster (declass_algo.txt, step 1):
|
||||||
|
//!
|
||||||
|
//! push_const8 #ctor ; define_class @Name
|
||||||
|
//! ( fclosure8 #m ; define_method @meth )*
|
||||||
|
//! ( fclosure8 #fields ; set_home_object ; put_loc ; drop )?
|
||||||
|
//!
|
||||||
|
//! becomes `fclosure8 #ctor` followed, per method, by
|
||||||
|
//! `get_field "prototype" ; put_field @meth`; the field-initialiser tail is
|
||||||
|
//! dropped. Detection (`find_class_seqs`) records the ranges; replacement
|
||||||
|
//! (`ClassSequence::emit_prototype`) reads operands out of each matched range.
|
||||||
|
|
||||||
|
use std::range::Range;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parse::{AtomRef, ImmOperand, JsValue, atom_names::PREDEFINED_ATOMS, opcodes::OpCode},
|
||||||
|
regify::{RegFn, RegInstr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::FieldInit;
|
||||||
|
|
||||||
|
/// Read each constructor <-> field-initialiser link off the class builds, as
|
||||||
|
/// fn ids. Run before `rewrite`, which drops the instructions carrying them.
|
||||||
|
pub(super) fn field_init_links(fns: &[RegFn]) -> Vec<FieldInit> {
|
||||||
|
let mut links = vec![];
|
||||||
|
for f in fns {
|
||||||
|
for seq in find_class_seqs(&f.code) {
|
||||||
|
if let Some(link) = seq.field_init(&f.code, &f.cpool) {
|
||||||
|
links.push(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
links
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite every class build in one function to prototype form.
|
||||||
|
pub(super) fn rewrite(mut reg_fn: RegFn) -> RegFn {
|
||||||
|
let seqs = find_class_seqs(®_fn.code);
|
||||||
|
// Move the code out of reg_fn so we can consume it while still calling
|
||||||
|
// reg_fn.fresh_val(); the empty Vec left behind costs nothing.
|
||||||
|
let code = std::mem::take(&mut reg_fn.code);
|
||||||
|
let total = code.len();
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(total);
|
||||||
|
let mut instrs = code.into_iter();
|
||||||
|
let mut seqs = seqs.iter();
|
||||||
|
let mut next_seq = seqs.next();
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
while pos < total {
|
||||||
|
match next_seq {
|
||||||
|
Some(seq) if seq.range.start == pos => {
|
||||||
|
let adv = seq.range.end - seq.range.start;
|
||||||
|
let matched: Vec<RegInstr> = instrs.by_ref().take(adv).collect();
|
||||||
|
seq.emit_prototype(&mut reg_fn, &mut out, &matched);
|
||||||
|
pos += adv;
|
||||||
|
next_seq = seqs.next();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
out.push(instrs.next().unwrap());
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reg_fn.code = out;
|
||||||
|
reg_fn
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClassSequence {
|
||||||
|
range: Range<usize>,
|
||||||
|
num_methods: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClassSequence {
|
||||||
|
/// Emit the prototype form, reading operands straight out of `matched` --
|
||||||
|
/// the class sequence's own instructions.
|
||||||
|
fn emit_prototype(&self, reg_fn: &mut RegFn, out: &mut Vec<RegInstr>, matched: &[RegInstr]) {
|
||||||
|
// push_const8 #ctor ; define_class -> fclosure8 #ctor (constructor kept)
|
||||||
|
let (ctor_const, ctor_val) = self.ctor(matched);
|
||||||
|
out.push(fclosure8(ctor_const, ctor_val));
|
||||||
|
|
||||||
|
// each ( fclosure8 #m ; define_method @meth ) -> get_field + put_field
|
||||||
|
for k in 0..self.num_methods {
|
||||||
|
let (method_const, method_val, meth) = self.method(matched, k);
|
||||||
|
out.push(fclosure8(method_const, method_val));
|
||||||
|
let proto = reg_fn.fresh_val();
|
||||||
|
out.push(get_field(proto_atom(), proto, ctor_val));
|
||||||
|
out.push(put_field(ImmOperand::Atom(meth), method_val, proto));
|
||||||
|
}
|
||||||
|
// the fclosure8 #fields ; set_home_object ; put_loc ; drop tail is dropped.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The constructor's constant and value: 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The constructor and field-initialiser fn ids, if the class has a
|
||||||
|
/// field-initialiser tail. `code` is the whole enclosing function, so the
|
||||||
|
/// range indexes it directly.
|
||||||
|
fn field_init(&self, code: &[RegInstr], cpool: &[JsValue]) -> Option<FieldInit> {
|
||||||
|
let base = 2 + 2 * self.num_methods; // constructor + methods, before the tail
|
||||||
|
if self.range.end - self.range.start == base {
|
||||||
|
return None; // no field-initialiser tail
|
||||||
|
}
|
||||||
|
let ctor_const = as_const(code[self.range.start].imm);
|
||||||
|
let fields_const = as_const(code[self.range.start + base].imm);
|
||||||
|
Some(FieldInit {
|
||||||
|
ctor: fn_id(cpool, ctor_const),
|
||||||
|
fields: fn_id(cpool, fields_const),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_class_seqs(mut ins: &[RegInstr]) -> Vec<ClassSequence> {
|
||||||
|
let mut seqs = vec![];
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
while !ins.is_empty() {
|
||||||
|
match match_class_seq(ins) {
|
||||||
|
Ok((rest, num_methods)) => {
|
||||||
|
let adv = ins.len() - rest.len();
|
||||||
|
|
||||||
|
seqs.push(ClassSequence {
|
||||||
|
range: Range {
|
||||||
|
start: offset,
|
||||||
|
end: offset + adv,
|
||||||
|
},
|
||||||
|
num_methods,
|
||||||
|
});
|
||||||
|
|
||||||
|
offset += adv;
|
||||||
|
ins = rest;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
offset += 1;
|
||||||
|
ins = &ins[1..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seqs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── the grammar, as matchers over the instruction stream ───────────────────
|
||||||
|
|
||||||
|
fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> {
|
||||||
|
let ins = match_one(ins, |i| i.op == OpCode::PushConst8)?;
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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::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 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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//! 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 function is left unreferenced.
|
||||||
|
//!
|
||||||
|
//! 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::FieldInit;
|
||||||
|
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(super) fn splice(fns: &mut [RegFn], field_inits: &[FieldInit]) {
|
||||||
|
for &FieldInit { ctor, fields } in field_inits {
|
||||||
|
splice_one(fns, ctor, fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) {
|
||||||
|
// Pull the field writes out of the initialiser (a separate function), then
|
||||||
|
// hand them to the constructor.
|
||||||
|
let writes = field_writes(&fns[fields]);
|
||||||
|
|
||||||
|
let ctor_fn = &mut fns[ctor];
|
||||||
|
let Some((start, end)) = call_span(ctor_fn) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
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.
|
||||||
|
fn field_writes(init: &RegFn) -> Vec<RegInstr> {
|
||||||
|
init.code
|
||||||
|
.iter()
|
||||||
|
.filter(|i| !is_prologue(i.op))
|
||||||
|
.map(|i| RegInstr::new(i.op, i.imm, i.dsts.clone(), i.args.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,252 +0,0 @@
|
|||||||
//! Transform: closures become plain objects.
|
|
||||||
//!
|
|
||||||
//! A closure becomes an object holding its function plus its captured
|
|
||||||
//! variables. Field 0 is the function; fields 1.. are the captures, in
|
|
||||||
//! capture-index order. The object doubles as the environment, so there is no
|
|
||||||
//! second env value to track.
|
|
||||||
//!
|
|
||||||
//! 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 — a pointer 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 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.** 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`.** They carry a receiver, so
|
|
||||||
//! they belong to the class passes; step 3 leaves them untouched. Same for
|
|
||||||
//! `apply`.
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
flatten::CaptureFrom,
|
|
||||||
parse::{ImmOperand, JsValue, opcodes::OpCode},
|
|
||||||
regify::{Fresh, RegFn, RegInstr, as_plain_call, int_field, is_plain_call},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Which capture (closure variable) an instruction touches, and how.
|
|
||||||
enum CaptureAccess {
|
|
||||||
Read(usize),
|
|
||||||
Write(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn declosure(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
|
||||||
// Where each function's captures come from, per fn id. Snapshotted first,
|
|
||||||
// because step 1 clears the capture lists this is read from.
|
|
||||||
let capture_sources: Vec<Vec<CaptureFrom>> = fns
|
|
||||||
.iter()
|
|
||||||
.map(|f| f.captures.iter().map(|c| c.from).collect())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for f in &mut fns {
|
|
||||||
let mut fresh = Fresh::for_fn(f);
|
|
||||||
take_env_param(f, &mut fresh);
|
|
||||||
build_closure_objects(f, &capture_sources, &mut fresh);
|
|
||||||
route_calls_through_object(f, &mut fresh);
|
|
||||||
}
|
|
||||||
fns
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 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(f: &mut RegFn, fresh: &mut Fresh) {
|
|
||||||
let env_param = f.arg_count;
|
|
||||||
f.arg_count += 1;
|
|
||||||
let env = fresh.new_value();
|
|
||||||
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let touches_captures = code.iter().any(|i| capture_access(i).is_some());
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(code.len() + 1);
|
|
||||||
|
|
||||||
// Load the env once at entry — only if a capture is actually accessed.
|
|
||||||
if touches_captures {
|
|
||||||
let load_env = ImmOperand::Arg(env_param as u16);
|
|
||||||
out.push(RegInstr::new(OpCode::GetArg, load_env, vec![env], vec![]));
|
|
||||||
}
|
|
||||||
|
|
||||||
for instr in code {
|
|
||||||
match capture_access(&instr) {
|
|
||||||
// A read keeps its destination, so downstream uses do not change.
|
|
||||||
Some(CaptureAccess::Read(k)) => {
|
|
||||||
let field = int_field(k as u32 + 1);
|
|
||||||
out.push(RegInstr::new(OpCode::GetField, field, instr.dsts, vec![env]));
|
|
||||||
}
|
|
||||||
Some(CaptureAccess::Write(k)) => {
|
|
||||||
let field = int_field(k as u32 + 1);
|
|
||||||
let mut args = vec![env];
|
|
||||||
args.extend(instr.args); // the value being stored
|
|
||||||
out.push(RegInstr::new(OpCode::PutField, field, vec![], args));
|
|
||||||
}
|
|
||||||
None => out.push(instr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.code = out;
|
|
||||||
f.captures.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Which capture an instruction reads or writes, if any.
|
|
||||||
fn capture_access(instr: &RegInstr) -> Option<CaptureAccess> {
|
|
||||||
use OpCode::*;
|
|
||||||
let slot_in_operand = || match instr.imm {
|
|
||||||
ImmOperand::VarRef(k) => k as usize,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
Some(match instr.op {
|
|
||||||
GetVarRef | GetVarRefCheck => CaptureAccess::Read(slot_in_operand()),
|
|
||||||
GetVarRef0 => CaptureAccess::Read(0),
|
|
||||||
GetVarRef1 => CaptureAccess::Read(1),
|
|
||||||
GetVarRef2 => CaptureAccess::Read(2),
|
|
||||||
GetVarRef3 => CaptureAccess::Read(3),
|
|
||||||
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => CaptureAccess::Write(slot_in_operand()),
|
|
||||||
PutVarRef0 => CaptureAccess::Write(0),
|
|
||||||
PutVarRef1 => CaptureAccess::Write(1),
|
|
||||||
PutVarRef2 => CaptureAccess::Write(2),
|
|
||||||
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(f: &mut RegFn, capture_sources: &[Vec<CaptureFrom>], fresh: &mut Fresh) {
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len());
|
|
||||||
|
|
||||||
for instr in code {
|
|
||||||
let (Some(child), Some(&closure)) = (closure_child(&instr, &f.cpool), instr.dsts.first())
|
|
||||||
else {
|
|
||||||
out.push(instr);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(load, instr.imm, vec![func], vec![]));
|
|
||||||
|
|
||||||
let mut obj = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![obj], vec![]));
|
|
||||||
|
|
||||||
// Each `define_field` returns the object, so the stores chain. The
|
|
||||||
// LAST store must land in `closure` — the value the rest of the code
|
|
||||||
// already uses for this closure.
|
|
||||||
let captures: &[CaptureFrom] = capture_sources.get(child).map(Vec::as_slice).unwrap_or(&[]);
|
|
||||||
|
|
||||||
// Store the function in field 0.
|
|
||||||
let is_last = captures.is_empty();
|
|
||||||
let result = if is_last { closure } else { fresh.new_value() };
|
|
||||||
out.push(RegInstr::new(OpCode::DefineField, int_field(0), vec![result], vec![obj, func]));
|
|
||||||
obj = result;
|
|
||||||
|
|
||||||
// Store each capture in field k+1.
|
|
||||||
for (k, source) in captures.iter().enumerate() {
|
|
||||||
let value = fresh.new_value();
|
|
||||||
out.push(load_capture(*source, value));
|
|
||||||
|
|
||||||
let is_last = k + 1 == captures.len();
|
|
||||||
let result = if is_last { closure } else { fresh.new_value() };
|
|
||||||
let field = int_field(k as u32 + 1);
|
|
||||||
out.push(RegInstr::new(OpCode::DefineField, field, vec![result], vec![obj, value]));
|
|
||||||
obj = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The nested function an `fclosure` instantiates, as a fn id.
|
|
||||||
fn closure_child(instr: &RegInstr, cpool: &[JsValue]) -> Option<usize> {
|
|
||||||
if !matches!(instr.op, OpCode::Fclosure | OpCode::Fclosure8) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let ImmOperand::Const(index) = instr.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) => {
|
|
||||||
RegInstr::new(OpCode::GetLoc, ImmOperand::Loc(slot as u16), vec![into], vec![])
|
|
||||||
}
|
|
||||||
CaptureFrom::ParentArg(slot) => {
|
|
||||||
RegInstr::new(OpCode::GetArg, ImmOperand::Arg(slot as u16), vec![into], vec![])
|
|
||||||
}
|
|
||||||
_ => 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(f: &mut RegFn, fresh: &mut Fresh) {
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len());
|
|
||||||
|
|
||||||
for instr in code {
|
|
||||||
if !is_plain_call(instr.op) || instr.args.is_empty() {
|
|
||||||
out.push(instr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let closure = *instr.args.last().unwrap(); // the callee is always last
|
|
||||||
let func = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::GetField, int_field(0), vec![func], vec![closure]));
|
|
||||||
|
|
||||||
let call_args = &instr.args[..instr.args.len() - 1];
|
|
||||||
let mut args = Vec::with_capacity(instr.args.len() + 1);
|
|
||||||
args.push(func);
|
|
||||||
args.extend_from_slice(call_args);
|
|
||||||
args.push(closure); // the env, last
|
|
||||||
out.push(RegInstr::new(as_plain_call(instr.op), instr.imm, instr.dsts, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
//! 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.
|
|
||||||
//!
|
|
||||||
//! Three steps:
|
|
||||||
//!
|
|
||||||
//! 1. `take_self_param` — every function takes `self` as its last
|
|
||||||
//! parameter, and `this` (`push_this`) reads
|
|
||||||
//! it. Applied uniformly — a function that
|
|
||||||
//! never mentions `this` still takes one —
|
|
||||||
//! because a call site cannot know whether
|
|
||||||
//! its callee does.
|
|
||||||
//! 2. `build_and_return_object` — constructors only. `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, so `get_field` finds one
|
|
||||||
//! exactly as it finds a field, and no chain
|
|
||||||
//! is walked at run time.
|
|
||||||
//! 3. `pass_receivers_explicitly` — `o.m(a)` passes `o` as an explicit
|
|
||||||
//! argument 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
|
|
||||||
//! keeps its callee 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::{Fresh, RegFn, RegInstr, as_plain_call, is_plain_call},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Lower prototypes and `this` across the program.
|
|
||||||
pub fn deprototype(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
|
||||||
for f in &mut fns {
|
|
||||||
let mut fresh = Fresh::for_fn(f);
|
|
||||||
let this = take_self_param(f, &mut fresh);
|
|
||||||
if f.is_ctor {
|
|
||||||
build_and_return_object(f, this, &mut fresh);
|
|
||||||
}
|
|
||||||
pass_receivers_explicitly(f, &mut fresh);
|
|
||||||
}
|
|
||||||
fns
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 1. `this` becomes a parameter ─────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Append `self` as the last parameter, then point every `push_this` at it.
|
|
||||||
/// Returns the value `this` now lives in — `None` if the function never reads
|
|
||||||
/// it, in which case nothing is loaded.
|
|
||||||
fn take_self_param(f: &mut RegFn, fresh: &mut Fresh) -> Option<usize> {
|
|
||||||
let self_param = f.arg_count;
|
|
||||||
f.arg_count += 1;
|
|
||||||
|
|
||||||
// The values `push_this` produced.
|
|
||||||
let this_values: Vec<usize> = f
|
|
||||||
.code
|
|
||||||
.iter()
|
|
||||||
.filter(|i| i.op == OpCode::PushThis)
|
|
||||||
.flat_map(|i| i.dsts.iter().copied())
|
|
||||||
.collect();
|
|
||||||
if this_values.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load `self` once at entry; every `push_this` read points at that instead.
|
|
||||||
let this = fresh.new_value();
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len() + 1);
|
|
||||||
let load_self = ImmOperand::Arg(self_param as u16);
|
|
||||||
out.push(RegInstr::new(OpCode::GetArg, load_self, vec![this], vec![]));
|
|
||||||
|
|
||||||
for mut instr in code {
|
|
||||||
if instr.op == OpCode::PushThis {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for v in instr.args.iter_mut().chain(instr.dsts.iter_mut()) {
|
|
||||||
if this_values.contains(v) {
|
|
||||||
*v = this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push(instr);
|
|
||||||
}
|
|
||||||
f.code = out;
|
|
||||||
Some(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 2. 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 build_and_return_object(f: &mut RegFn, this: Option<usize>, fresh: &mut Fresh) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// The current version of the object under construction. A derived class
|
|
||||||
// has none until `super(...)` returns one.
|
|
||||||
let mut obj = this;
|
|
||||||
|
|
||||||
if !derived {
|
|
||||||
let running_fn = fresh.new_value();
|
|
||||||
let proto = fresh.new_value();
|
|
||||||
let empty = fresh.new_value();
|
|
||||||
let built = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::SpecialObject, ImmOperand::U8(2), vec![running_fn], vec![]));
|
|
||||||
out.push(RegInstr::new(OpCode::GetField, proto_atom(), vec![proto], vec![running_fn]));
|
|
||||||
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![empty], vec![]));
|
|
||||||
out.push(RegInstr::new(
|
|
||||||
OpCode::CopyDataProperties,
|
|
||||||
ImmOperand::U8(0),
|
|
||||||
vec![built],
|
|
||||||
vec![empty, proto],
|
|
||||||
));
|
|
||||||
obj = Some(built);
|
|
||||||
}
|
|
||||||
|
|
||||||
for mut instr in code {
|
|
||||||
// Reads of `this` see the current version of the object. Only uses are
|
|
||||||
// repointed: `self`'s own load keeps its (now dead) destination.
|
|
||||||
if let (Some(this), Some(current)) = (this, obj) {
|
|
||||||
for v in instr.args.iter_mut() {
|
|
||||||
if *v == this {
|
|
||||||
*v = current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match instr.op {
|
|
||||||
// `this.f = v` becomes construction: the next version of the object.
|
|
||||||
OpCode::PutField if instr.args.len() == 2 && obj == Some(instr.args[1]) => {
|
|
||||||
let value = instr.args[0];
|
|
||||||
let current = instr.args[1];
|
|
||||||
let newer = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::DefineField, instr.imm, vec![newer], vec![current, value]));
|
|
||||||
obj = Some(newer);
|
|
||||||
}
|
|
||||||
// `super(...)` produced the object; continue from its result.
|
|
||||||
OpCode::CallConstructor if derived && !instr.dsts.is_empty() => {
|
|
||||||
obj = Some(instr.dsts[0]);
|
|
||||||
out.push(instr);
|
|
||||||
}
|
|
||||||
// The constructor returns the object it built.
|
|
||||||
OpCode::Return | OpCode::ReturnUndef if obj.is_some() => {
|
|
||||||
out.push(RegInstr::new(OpCode::Return, ImmOperand::None, vec![], vec![obj.unwrap()]));
|
|
||||||
}
|
|
||||||
_ => out.push(instr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 3. calls carry their receiver ─────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Every call passes its receiver as an explicit argument, slotted in just
|
|
||||||
/// before the callee.
|
|
||||||
fn pass_receivers_explicitly(f: &mut RegFn, fresh: &mut Fresh) {
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len());
|
|
||||||
|
|
||||||
// `get_field2` also keeps the object it read from, under a second value.
|
|
||||||
// That echo is just the object; recorded here and substituted at the end.
|
|
||||||
let mut receiver_alias: Vec<(usize, usize)> = Vec::new();
|
|
||||||
|
|
||||||
for instr in code {
|
|
||||||
match instr.op {
|
|
||||||
// `new` is gone, so the guard against calling without it goes too.
|
|
||||||
OpCode::CheckCtor => {}
|
|
||||||
|
|
||||||
// `super` is the parent constructor, recorded by declass as a
|
|
||||||
// field on the constructor object.
|
|
||||||
OpCode::GetSuper if instr.args.len() == 1 && !instr.dsts.is_empty() => {
|
|
||||||
let read_parent = RegInstr::new(OpCode::GetField, parent_link(), vec![instr.dsts[0]], vec![instr.args[0]]);
|
|
||||||
out.push(read_parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `o.m(a)` -> `m(a, o)`. Register args are `[a.., m, o]`.
|
|
||||||
OpCode::CallMethod | OpCode::TailCallMethod if instr.args.len() >= 2 => {
|
|
||||||
let n = instr.args.len();
|
|
||||||
let receiver = instr.args[n - 1];
|
|
||||||
let callee = instr.args[n - 2];
|
|
||||||
let args = args_with_self(&instr.args[..n - 2], receiver, callee);
|
|
||||||
out.push(RegInstr::new(as_plain_call(instr.op), instr.imm, instr.dsts, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
// `new C(a)`: the constructor builds and returns the object now, so
|
|
||||||
// this is a plain call yielding it. `self` is undefined; the value
|
|
||||||
// dropped is new.target. Register args are `[a.., new.target, C]`.
|
|
||||||
OpCode::CallConstructor if instr.args.len() >= 2 => {
|
|
||||||
let n = instr.args.len();
|
|
||||||
let callee = instr.args[n - 1];
|
|
||||||
let undefined = push_undefined(&mut out, fresh);
|
|
||||||
let args = args_with_self(&instr.args[..n - 2], undefined, callee);
|
|
||||||
out.push(RegInstr::new(OpCode::Call, instr.imm, instr.dsts, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
// `get_field2` kept the object under a second value. The object is
|
|
||||||
// an explicit argument now, so that copy is just the object, and
|
|
||||||
// this becomes an ordinary read.
|
|
||||||
OpCode::GetField2 if instr.dsts.len() == 2 && instr.args.len() == 1 => {
|
|
||||||
receiver_alias.push((instr.dsts[0], instr.args[0]));
|
|
||||||
out.push(RegInstr::new(OpCode::GetField, instr.imm, vec![instr.dsts[1]], vec![instr.args[0]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A plain call has no receiver, so `this` is undefined.
|
|
||||||
op if is_plain_call(op) && !instr.args.is_empty() => {
|
|
||||||
let n = instr.args.len();
|
|
||||||
let callee = instr.args[n - 1];
|
|
||||||
let undefined = push_undefined(&mut out, fresh);
|
|
||||||
let args = args_with_self(&instr.args[..n - 1], undefined, callee);
|
|
||||||
out.push(RegInstr::new(as_plain_call(op), instr.imm, instr.dsts, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => out.push(instr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point the echoed receivers at the object itself.
|
|
||||||
if !receiver_alias.is_empty() {
|
|
||||||
for instr in &mut out {
|
|
||||||
for v in instr.args.iter_mut().chain(instr.dsts.iter_mut()) {
|
|
||||||
if let Some((_, object)) = receiver_alias.iter().find(|(echo, _)| echo == v) {
|
|
||||||
*v = *object;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The argument list of a call with `self` slotted in before the callee. The
|
|
||||||
/// callee must stay last so declosure can find it: `[args.., self, callee]`.
|
|
||||||
fn args_with_self(args: &[usize], self_val: usize, callee: usize) -> Vec<usize> {
|
|
||||||
let mut all = Vec::with_capacity(args.len() + 2);
|
|
||||||
all.extend_from_slice(args);
|
|
||||||
all.push(self_val);
|
|
||||||
all.push(callee);
|
|
||||||
all
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emit `undefined` and return the value holding it.
|
|
||||||
fn push_undefined(out: &mut Vec<RegInstr>, fresh: &mut Fresh) -> usize {
|
|
||||||
let v = fresh.new_value();
|
|
||||||
out.push(RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![v], vec![]));
|
|
||||||
v
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ use crate::parse::{
|
|||||||
Atom, AtomRef, ClosureKind, Function, Instr, JsString, JsValue, QuickJSBinary, atom_names,
|
Atom, AtomRef, ClosureKind, Function, Instr, JsString, JsValue, QuickJSBinary, atom_names,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug)]
|
||||||
pub enum CaptureFrom {
|
pub enum CaptureFrom {
|
||||||
/// `parent.var_defs[i]`
|
/// `parent.var_defs[i]`
|
||||||
ParentArg(usize),
|
ParentArg(usize),
|
||||||
@@ -31,7 +31,8 @@ pub struct Capture {
|
|||||||
pub struct FlatFn {
|
pub struct FlatFn {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub arg_count: usize,
|
pub arg_count: usize,
|
||||||
pub var_count: usize, // num locals
|
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 captures: Vec<Capture>,
|
||||||
pub cpool: Vec<JsValue>,
|
pub cpool: Vec<JsValue>,
|
||||||
pub code: Vec<Instr>,
|
pub code: Vec<Instr>,
|
||||||
@@ -71,6 +72,7 @@ fn walk(flat_fns: &mut Vec<FlatFn>, func: Function, atoms: &[Atom]) -> usize {
|
|||||||
name,
|
name,
|
||||||
arg_count: func.arg_count,
|
arg_count: func.arg_count,
|
||||||
var_count: func.var_count,
|
var_count: func.var_count,
|
||||||
|
value_names: func.var_defs.iter().map(|v| v.name).collect(),
|
||||||
captures,
|
captures,
|
||||||
cpool: Vec::new(),
|
cpool: Vec::new(),
|
||||||
code: func.byte_code,
|
code: func.byte_code,
|
||||||
|
|||||||
+8
-13
@@ -1,6 +1,4 @@
|
|||||||
mod declass;
|
mod declass;
|
||||||
mod declosure;
|
|
||||||
mod deprototype;
|
|
||||||
mod flatten;
|
mod flatten;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod regify;
|
mod regify;
|
||||||
@@ -8,8 +6,6 @@ mod regify;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use declass::declass;
|
use declass::declass;
|
||||||
use declosure::declosure;
|
|
||||||
use deprototype::deprototype;
|
|
||||||
use flatten::flatten;
|
use flatten::flatten;
|
||||||
use parse::QuickJSBinary;
|
use parse::QuickJSBinary;
|
||||||
use regify::regify;
|
use regify::regify;
|
||||||
@@ -35,24 +31,23 @@ fn main() {
|
|||||||
let (_, parsed) = QuickJSBinary::parse(&bin).unwrap();
|
let (_, parsed) = QuickJSBinary::parse(&bin).unwrap();
|
||||||
let (flat_fns, atoms) = flatten(parsed);
|
let (flat_fns, atoms) = flatten(parsed);
|
||||||
|
|
||||||
println!("{atoms:#?}");
|
println!("{flat_fns:#?}");
|
||||||
let reg_fns = regify(flat_fns);
|
|
||||||
|
|
||||||
println!("=== after regify ===");
|
println!("=== after regify ===");
|
||||||
|
let reg_fns = regify(flat_fns);
|
||||||
println!("{reg_fns:#?}");
|
println!("{reg_fns:#?}");
|
||||||
|
|
||||||
let declassed = declass(reg_fns);
|
|
||||||
|
|
||||||
println!("=== after declass ===");
|
println!("=== after declass ===");
|
||||||
|
let declassed = declass(reg_fns, &atoms);
|
||||||
println!("{declassed:#?}");
|
println!("{declassed:#?}");
|
||||||
|
|
||||||
let deprototyped = deprototype(declassed);
|
println!("{atoms:#?}");
|
||||||
|
|
||||||
println!("=== after deprototype ===");
|
/*println!("=== after deprototype ===");
|
||||||
|
let deprototyped = deprototype(declassed);
|
||||||
println!("{deprototyped:#?}");
|
println!("{deprototyped:#?}");
|
||||||
|
|
||||||
let declosured = declosure(deprototyped);
|
|
||||||
|
|
||||||
println!("=== after declosure ===");
|
println!("=== after declosure ===");
|
||||||
println!("{declosured:#?}");
|
let declosured = declosure(deprototyped);
|
||||||
|
println!("{declosured:#?}");*/
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-121
@@ -14,6 +14,17 @@ pub struct RegInstr {
|
|||||||
pub args: Vec<usize>,
|
pub args: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RegInstr {
|
||||||
|
pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> Self {
|
||||||
|
Self {
|
||||||
|
dsts,
|
||||||
|
op,
|
||||||
|
imm,
|
||||||
|
args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Debug for RegInstr {
|
impl Debug for RegInstr {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
@@ -24,148 +35,87 @@ impl Debug for RegInstr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `FlatFn` with its stack bytecode lowered to register code.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RegFn {
|
pub struct RegFn {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub arg_count: usize,
|
pub arg_count: usize,
|
||||||
pub var_count: usize,
|
pub var_count: usize,
|
||||||
|
pub value_names: Vec<AtomRef>,
|
||||||
pub captures: Vec<Capture>,
|
pub captures: Vec<Capture>,
|
||||||
pub cpool: Vec<JsValue>,
|
pub cpool: Vec<JsValue>,
|
||||||
pub code: Vec<RegInstr>,
|
pub code: Vec<RegInstr>,
|
||||||
/// Set by `declass` when this function is a class constructor.
|
next_val: usize,
|
||||||
pub is_ctor: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn regify_walk(code: &[Instr], var_count: usize) -> Vec<RegInstr> {
|
impl RegFn {
|
||||||
let mut stack = vec![];
|
fn from_flat(flat_fn: FlatFn) -> Self {
|
||||||
let mut output = vec![];
|
let mut stack = vec![];
|
||||||
let mut next_val = var_count;
|
let mut output = Vec::with_capacity(flat_fn.code.len());
|
||||||
|
let mut next_val = flat_fn.var_count;
|
||||||
|
|
||||||
let mut fresh_val = || {
|
let mut fresh_val = || {
|
||||||
let v = next_val;
|
let v = next_val;
|
||||||
next_val += 1;
|
next_val += 1;
|
||||||
v
|
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 {
|
for instr in flat_fn.code {
|
||||||
OpCode::Call0 => 0,
|
let mut reg_instr = RegInstr {
|
||||||
OpCode::Call1 => 1,
|
dsts: vec![],
|
||||||
OpCode::Call2 => 2,
|
op: instr.op,
|
||||||
OpCode::Call3 => 3,
|
imm: instr.imm_operands,
|
||||||
_ => match instr.imm_operands {
|
args: vec![],
|
||||||
ImmOperand::Npop(npop) => npop as usize,
|
};
|
||||||
ImmOperand::NpopU16 { npop, .. } => npop as usize,
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let info = instr.op.info();
|
let extra_pop = match instr.op {
|
||||||
let n_pop = info.n_pop as usize + extra_pop;
|
OpCode::Call0 => 0,
|
||||||
let n_push = info.n_push as usize;
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
for _ in 0..n_pop {
|
let info = instr.op.info();
|
||||||
let v = stack.pop().unwrap();
|
let n_pop = info.n_pop as usize + extra_pop;
|
||||||
reg_instr.args.push(v);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in 0..n_push {
|
Self {
|
||||||
let v = fresh_val();
|
name: flat_fn.name,
|
||||||
stack.push(v);
|
arg_count: flat_fn.arg_count,
|
||||||
reg_instr.dsts.push(v);
|
var_count: flat_fn.var_count,
|
||||||
|
value_names: flat_fn.value_names,
|
||||||
|
captures: flat_fn.captures,
|
||||||
|
cpool: flat_fn.cpool,
|
||||||
|
code: output,
|
||||||
|
next_val,
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push(reg_instr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output
|
pub fn fresh_val(&mut self) -> usize {
|
||||||
|
let v = self.next_val;
|
||||||
|
self.next_val += 1;
|
||||||
|
v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lower every function's stack code to register code, keeping the rest.
|
|
||||||
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
||||||
flat_fns
|
flat_fns.into_iter().map(RegFn::from_flat).collect()
|
||||||
.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── shared by the de* passes ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
impl RegInstr {
|
|
||||||
pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> Self {
|
|
||||||
RegInstr {
|
|
||||||
dsts,
|
|
||||||
op,
|
|
||||||
imm,
|
|
||||||
args,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this instruction reads or writes `value`.
|
|
||||||
pub fn mentions(&self, value: usize) -> bool {
|
|
||||||
self.args.contains(&value) || self.dsts.contains(&value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hands out value numbers that nothing in the function uses yet.
|
|
||||||
pub struct Fresh(usize);
|
|
||||||
|
|
||||||
impl Fresh {
|
|
||||||
/// Start just past the highest value the function already uses.
|
|
||||||
pub fn for_fn(f: &RegFn) -> Fresh {
|
|
||||||
let highest = f
|
|
||||||
.code
|
|
||||||
.iter()
|
|
||||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
|
||||||
.copied()
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
Fresh(highest + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_value(&mut self) -> usize {
|
|
||||||
let v = self.0;
|
|
||||||
self.0 += 1;
|
|
||||||
v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A call with no receiver. `call_method` and `call_constructor` carry one and
|
|
||||||
/// are handled by the class passes instead.
|
|
||||||
pub fn is_plain_call(op: OpCode) -> bool {
|
|
||||||
use OpCode::*;
|
|
||||||
matches!(op, Call | Call0 | Call1 | Call2 | Call3 | TailCall)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The generic form of a call opcode: any tail variant becomes `tail_call`,
|
|
||||||
/// everything else `call`.
|
|
||||||
pub fn as_plain_call(op: OpCode) -> OpCode {
|
|
||||||
use OpCode::*;
|
|
||||||
if matches!(op, TailCall | TailCallMethod) {
|
|
||||||
TailCall
|
|
||||||
} else {
|
|
||||||
Call
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An integer-named field. The synthetic objects these passes build use
|
|
||||||
/// integer names so no new atoms have to be minted.
|
|
||||||
pub fn int_field(index: u32) -> ImmOperand {
|
|
||||||
ImmOperand::Atom(AtomRef::TaggedInt(index))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
declass: class form -> prototype form
|
||||||
|
|
||||||
|
Plain text. Read it next to class_form.txt (the input) and proto_form.txt (the target).
|
||||||
|
Every instruction below is copied in the same dump format those files use.
|
||||||
|
|
||||||
|
The class in function "test" has this cpool:
|
||||||
|
|
||||||
|
cpool: [ FnById(2), FnById(3), FnById(4) ]
|
||||||
|
|
||||||
|
Const(0) = FnById(2) = the m1 method
|
||||||
|
Const(1) = FnById(3) = the constructor
|
||||||
|
Const(2) = FnById(4) = the fields initializer
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
STEP 1 in function "test": rewrite how the class is built
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
DETECT, abstractly:
|
||||||
|
|
||||||
|
push_const8 #ctor ; define_class @Name
|
||||||
|
( fclosure8 #method ; define_method @meth )*
|
||||||
|
( fclosure8 #fields ; set_home_object ; put_loc ; drop )?
|
||||||
|
|
||||||
|
which in the example is:
|
||||||
|
|
||||||
|
[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],
|
||||||
|
|
||||||
|
REPLACE it with:
|
||||||
|
|
||||||
|
[6] = Fclosure8 Const(1) [],
|
||||||
|
[8] = Fclosure8 Const(0) [],
|
||||||
|
[18] = GetField Atom(atom("prototype")) [6],
|
||||||
|
[] = PutField Atom(file_atom[5]) [8, 18],
|
||||||
|
|
||||||
|
INSTRUCTION BY INSTRUCTION:
|
||||||
|
|
||||||
|
PushConst8 Const(1) + DefineClass [5,4] -> Fclosure8 Const(1), keep dst [6]
|
||||||
|
Fclosure8 Const(0) -> unchanged (the method closure)
|
||||||
|
DefineMethod atom=A [8,7] -> GetField "prototype" [6]
|
||||||
|
then PutField atom=A [8,18]
|
||||||
|
Fclosure8 Const(2) -> deleted
|
||||||
|
SetHomeObject -> deleted
|
||||||
|
PutLoc1 [12] -> deleted
|
||||||
|
Drop [11] -> deleted
|
||||||
|
|
||||||
|
[18] is any fresh value id.
|
||||||
|
Repeat the DefineMethod line for each method in the class.
|
||||||
|
|
||||||
|
REMEMBER for step 2 and 3:
|
||||||
|
|
||||||
|
constructor function = Const(1) = FnById(3)
|
||||||
|
fields function = Const(2) = FnById(4)
|
||||||
|
class name = file_atom[2]
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
STEP 2 splice the field initialiser into the constructor FnById(3)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
The constructor does not set its own fields. quickjs hoisted them into a
|
||||||
|
separate initialiser function (FnById(4)) that the constructor reaches through
|
||||||
|
the captured <class_fields_init> and runs with a call. Splice that function's
|
||||||
|
field writes straight into the constructor, in place of the call, so the
|
||||||
|
constructor initialises its fields inline and the separate initialiser is left
|
||||||
|
unused.
|
||||||
|
|
||||||
|
DETECT the call block, abstractly (from the get_var_ref_check of
|
||||||
|
<class_fields_init> through the next call_method):
|
||||||
|
|
||||||
|
get_var_ref_check <class_fields_init> ; dup ; if_false
|
||||||
|
; get_loc ; swap ; call_method
|
||||||
|
|
||||||
|
which in the example is:
|
||||||
|
|
||||||
|
[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],
|
||||||
|
|
||||||
|
REPLACE the call block by splicing in the field writes from the initialiser
|
||||||
|
FnById(4):
|
||||||
|
|
||||||
|
[9] = GetLoc0 None [],
|
||||||
|
[10] = Push1 None [],
|
||||||
|
[11] = DefineField Atom(file_atom[4]) [10, 9],
|
||||||
|
|
||||||
|
The initialiser FnById(4) in full looks like this:
|
||||||
|
|
||||||
|
[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 [],
|
||||||
|
|
||||||
|
HOW THE SPLICE IS BUILT:
|
||||||
|
|
||||||
|
Take the initialiser FnById(4). Drop these opcodes:
|
||||||
|
SpecialObject, PushThis, PutLoc0, PutLoc1, PutLoc2, PutLoc3,
|
||||||
|
Goto8, Goto, ReturnUndef
|
||||||
|
What is left is the field writes; these are what gets spliced in.
|
||||||
|
Renumber their value ids to fresh ids in the constructor: 4,5,6 -> 9,10,11.
|
||||||
|
The constructor's own PushThis / PutLoc0 / CheckCtor prologue and its
|
||||||
|
final ReturnUndef stay as they were, wrapping the spliced-in writes.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
STEP 3 in the constructor FnById(3): fix its header
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
DETECT, abstractly:
|
||||||
|
|
||||||
|
a constructor (name "<null>") that captures <class_fields_init>
|
||||||
|
|
||||||
|
which in the example is:
|
||||||
|
|
||||||
|
name: "<null>",
|
||||||
|
captures: [ Capture { name: "<class_fields_init>", ... } ],
|
||||||
|
|
||||||
|
REPLACE with:
|
||||||
|
|
||||||
|
name: "Counter",
|
||||||
|
captures: [],
|
||||||
|
|
||||||
|
name becomes the class name from step 1.
|
||||||
|
Remove the <class_fields_init> capture.
|
||||||
|
If other captures remain after it, subtract 1 from every VarRef(n)
|
||||||
|
whose n is greater than the removed capture index.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
ELIMINATED (opcodes this pass removes from live code)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Step 1 removes these from the class build:
|
||||||
|
|
||||||
|
DefineClass replaced by Fclosure8
|
||||||
|
DefineMethod replaced by GetField + PutField
|
||||||
|
SetHomeObject deleted
|
||||||
|
PushConst8 the class-template one, folded into Fclosure8
|
||||||
|
Fclosure8 the fields-init one, deleted
|
||||||
|
PutLoc1 the SetHomeObject leftover store, deleted
|
||||||
|
Drop the SetHomeObject leftover drop, deleted
|
||||||
|
|
||||||
|
Step 2 removes these from the constructor (the field-init call block):
|
||||||
|
|
||||||
|
GetVarRefCheck of the <class_fields_init> capture
|
||||||
|
Dup
|
||||||
|
IfFalse8
|
||||||
|
GetLoc0 the one feeding the call
|
||||||
|
Swap
|
||||||
|
CallMethod
|
||||||
|
|
||||||
|
After the pass, these three class-only opcodes never appear again anywhere:
|
||||||
|
|
||||||
|
DefineClass
|
||||||
|
DefineMethod
|
||||||
|
SetHomeObject
|
||||||
|
|
||||||
|
The rest (PushConst8, Fclosure8, GetVarRefCheck, Dup, IfFalse8, GetLoc0,
|
||||||
|
Swap, CallMethod, PutLoc1, Drop) still occur elsewhere for ordinary reasons.
|
||||||
|
Only the specific class-pattern instances listed above are removed.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
NOT TOUCHED (passed through unchanged)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
DefineClass AtomU8 { arg: 1 } derived class
|
||||||
|
DefineMethod AtomU8 { arg: 1 } getter
|
||||||
|
DefineMethod AtomU8 { arg: 2 } setter
|
||||||
|
DefineMethodComputed ... computed method name
|
||||||
|
DefineClassComputed ... computed class name
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
LEFT BEHIND (removed by a later pass)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
FnById(4), the fields function. Nothing references it anymore.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
declosure: closures become plain objects
|
||||||
|
|
||||||
|
Plain text, same dump format as class_form.txt / proto_form.txt.
|
||||||
|
|
||||||
|
A closure becomes an object. Field 0 holds the function. Fields 1.. hold the
|
||||||
|
captured variables, in capture order. The object is also the environment, so
|
||||||
|
there is no separate env value.
|
||||||
|
|
||||||
|
Field names are plain integers, so a field operand reads Atom(int(k)):
|
||||||
|
field 0 the function
|
||||||
|
field k+1 capture number k
|
||||||
|
|
||||||
|
Running example:
|
||||||
|
|
||||||
|
function outer(a) {
|
||||||
|
function inner() { return a; } // inner captures a
|
||||||
|
return inner();
|
||||||
|
}
|
||||||
|
|
||||||
|
FnById(1) = outer arg_count 1 (a is arg 0)
|
||||||
|
FnById(2) = inner outer's cpool Const(0)
|
||||||
|
captures: [ Capture { name: "a", from: ParentArg(0) } ]
|
||||||
|
|
||||||
|
Three steps, one property each.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
STEP 1 every function takes the env as its LAST parameter
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Runs on every function. Capture reads/writes become field access on the env.
|
||||||
|
arg_count grows by one; existing args keep their numbers. The env is loaded
|
||||||
|
once at entry, but only if the function touches a capture. Then the capture
|
||||||
|
list is cleared.
|
||||||
|
|
||||||
|
DETECT, abstractly (any function; each capture access, one at a time):
|
||||||
|
|
||||||
|
capture read get_var_ref | get_var_ref_check | get_var_ref0..3
|
||||||
|
capture write put_var_ref | put_var_ref_check | put_var_ref_check_init
|
||||||
|
| put_var_ref0..3
|
||||||
|
|
||||||
|
inner, which in the example is:
|
||||||
|
|
||||||
|
captures: [ Capture { name: "a", from: ParentArg(0) } ],
|
||||||
|
arg_count: 0,
|
||||||
|
[0] = GetVarRefCheck VarRef(0) [],
|
||||||
|
[] = Return None [0],
|
||||||
|
|
||||||
|
REPLACE with:
|
||||||
|
|
||||||
|
captures: [],
|
||||||
|
arg_count: 1,
|
||||||
|
[1] = GetArg Arg(0) [], // env, the new last parameter
|
||||||
|
[0] = GetField Atom(int(1)) [1], // capture 0 -> field 1
|
||||||
|
[] = Return None [0],
|
||||||
|
|
||||||
|
INSTRUCTION BY INSTRUCTION:
|
||||||
|
|
||||||
|
load env once at entry -> GetArg Arg(<new last arg>) [env]
|
||||||
|
GetVarRef / GetVarRefCheck k -> GetField Atom(int(k+1)) [env]
|
||||||
|
GetVarRef0..3 -> GetField Atom(int(1..4)) [env]
|
||||||
|
PutVarRef / PutVarRefCheck k -> PutField Atom(int(k+1)) [value, env]
|
||||||
|
PutVarRef0..3 -> PutField Atom(int(1..4)) [value, env]
|
||||||
|
|
||||||
|
A function that touches no capture still gains the env parameter, but no
|
||||||
|
GetArg is emitted for it. The uniform parameter is what lets step 3 append
|
||||||
|
the env at every call site.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
STEP 2 every fclosure builds the object, in the PARENT
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Each fclosure becomes: load the function as a plain constant, make the object,
|
||||||
|
store the function in field 0, then one field per capture loaded from the
|
||||||
|
parent frame. The DefineField results chain; the LAST store must land in the
|
||||||
|
value id the rest of the code already uses for the closure.
|
||||||
|
|
||||||
|
DETECT, abstractly:
|
||||||
|
|
||||||
|
fclosure #child (#child resolves to a FnById in cpool)
|
||||||
|
|
||||||
|
outer, which in the example is:
|
||||||
|
|
||||||
|
[3] = Fclosure8 Const(0) [], // Const(0) = FnById(2) = inner
|
||||||
|
|
||||||
|
REPLACE with:
|
||||||
|
|
||||||
|
[10] = PushConst8 Const(0) [], // the function, as a plain constant
|
||||||
|
[11] = Object None [], // the closure object
|
||||||
|
[12] = DefineField Atom(int(0)) [10, 11], // field 0 = function
|
||||||
|
[13] = GetArg Arg(0) [], // capture 0 source: outer's arg 0 (a)
|
||||||
|
[3] = DefineField Atom(int(1)) [13, 12], // field 1 = capture a; last store -> [3]
|
||||||
|
|
||||||
|
A closure with NO captures is just field 0, and that store lands in [3]:
|
||||||
|
|
||||||
|
[3] = Fclosure8 Const(0) [],
|
||||||
|
->
|
||||||
|
[10] = PushConst8 Const(0) [],
|
||||||
|
[11] = Object None [],
|
||||||
|
[3] = DefineField Atom(int(0)) [10, 11],
|
||||||
|
|
||||||
|
CAPTURE SOURCE -> how it loads (from the parent frame):
|
||||||
|
|
||||||
|
ParentLocal(s) -> GetLoc Loc(s) [value]
|
||||||
|
ParentArg(s) -> GetArg Arg(s) [value]
|
||||||
|
anything else -> Undefined None [value] // gap, see below
|
||||||
|
|
||||||
|
DefineField args are [value, obj], and it returns the object, so the obj
|
||||||
|
of each store is the result of the store before it.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
STEP 3 every plain call goes through the object, in the CALLER
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
A call names a closure object now. Read the function out of field 0, call it,
|
||||||
|
and pass the object itself as the trailing env argument. The callee is the
|
||||||
|
last argument, so it is args.last().
|
||||||
|
|
||||||
|
DETECT, abstractly:
|
||||||
|
|
||||||
|
plain call call | call0..3 (callee = last arg; not call_method,
|
||||||
|
call_constructor, or apply)
|
||||||
|
|
||||||
|
outer, which in the example is:
|
||||||
|
|
||||||
|
[5] = Call Npop(0) [4], // [4] is the closure object
|
||||||
|
|
||||||
|
REPLACE with:
|
||||||
|
|
||||||
|
[14] = GetField Atom(int(0)) [4], // the function, out of field 0
|
||||||
|
[5] = Call Npop(1) [4, 14], // call it; env (the object) appended
|
||||||
|
|
||||||
|
Npop grows by one, for the appended env. Args become:
|
||||||
|
|
||||||
|
[ env, <original call args>, func ]
|
||||||
|
|
||||||
|
env = the object value (the old callee)
|
||||||
|
func = field 0 of the object (the new callee, still last)
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
ELIMINATED (opcodes this pass removes from live code)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Step 1 removes:
|
||||||
|
|
||||||
|
GetVarRef, GetVarRefCheck, GetVarRef0..3 replaced by GetField on env
|
||||||
|
PutVarRef, PutVarRefCheck,
|
||||||
|
PutVarRefCheckInit, PutVarRef0..3 replaced by PutField on env
|
||||||
|
|
||||||
|
Step 2 removes:
|
||||||
|
|
||||||
|
Fclosure, Fclosure8 replaced by PushConst8 + Object
|
||||||
|
+ DefineField(s)
|
||||||
|
|
||||||
|
After the pass, these never appear again anywhere:
|
||||||
|
|
||||||
|
Fclosure, Fclosure8
|
||||||
|
every GetVarRef* and PutVarRef*
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
NOT TOUCHED (passed through unchanged)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
CallMethod carries a receiver; the class passes own it
|
||||||
|
CallConstructor carries a receiver; the class passes own it
|
||||||
|
Apply carries a receiver
|
||||||
|
GetVarRef* / PutVarRef* that name a source this pass does not model
|
||||||
|
(see gaps) still become field access, but load
|
||||||
|
Undefined
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
GAPS (known, left for later)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
A captured var the parent later writes: the write makes a new value, so
|
||||||
|
the closure never sees it. A capture aliases the value at capture time.
|
||||||
|
Capture source is ParentCapture / module binding / global: step 2 has no
|
||||||
|
parent frame slot to read, so it loads Undefined.
|
||||||
|
Foreign callee (e.g. Math.sqrt): step 3 reads field 0, but a host builtin
|
||||||
|
has no field 0.
|
||||||
|
Entry points (module root, host-invoked functions): step 1 gives them an
|
||||||
|
env parameter, but no caller of ours supplies one.
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
demutate: object create + in-place mutation become function calls
|
||||||
|
|
||||||
|
Plain text, same dump format as class_form.txt / proto_form.txt.
|
||||||
|
|
||||||
|
Goal: every point that builds or mutates an object becomes a call to a function
|
||||||
|
that RETURNS a fresh object. A mutation function copies the input's fields with
|
||||||
|
a spread and re-applies only the writes it was given; it never names the rest.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
THE KEY IDEA
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Find a maximal contiguous RUN of static-key field writes to one object -- the
|
||||||
|
Object that creates it, the DefineField / PutField writes with an atom key, and
|
||||||
|
the value computations they consume -- and lift the whole run into one function.
|
||||||
|
|
||||||
|
creation run Object ; ( <value> ; DefineField @F )+
|
||||||
|
mutation run ( <object> ; <value> ; PutField @F )+ (same object)
|
||||||
|
|
||||||
|
The function returns the finished object. The call site replaces the run with:
|
||||||
|
load the parameters, make the closure, call it, and put the result where the
|
||||||
|
run's object went (the escaping value for a creation, a store-back for a
|
||||||
|
mutation).
|
||||||
|
|
||||||
|
Creation and mutation are ONE code path. The only difference is whether the
|
||||||
|
run's object is made inside it (creation -> start from an empty Object) or
|
||||||
|
loaded from outside (mutation -> the object is a parameter, spread in first).
|
||||||
|
Creation is just mutation of a freshly-made empty object; PutField and
|
||||||
|
DefineField both become DefineField on the fresh copy the function owns.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
PARAMETERS -- the rule that keeps this general
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
A value the run USES but does not itself PRODUCE becomes a parameter, in
|
||||||
|
first-use order. Everything produced inside the run (Object, constants,
|
||||||
|
arithmetic that is wholly inside) moves into the function unchanged.
|
||||||
|
|
||||||
|
the object being mutated is used-but-not-produced -> it is a parameter.
|
||||||
|
a constant field value (Push1) is produced inside -> it moves in.
|
||||||
|
a field value from the enclosing frame (GetArg p) -> a parameter.
|
||||||
|
|
||||||
|
So a pure literal of constants lifts to a 0-parameter function; a mutation of
|
||||||
|
`a` lifts to a 1-parameter function; `a.x = p + 1` lifts to a 2-parameter one.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
FUNCTION BODY -- how the run is rebuilt
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
The lifted function always builds ONE fresh object and returns it.
|
||||||
|
|
||||||
|
if the run has an incoming object (a parameter), start as a spread copy:
|
||||||
|
|
||||||
|
[0] = Object None [],
|
||||||
|
[1] = GetArg0 None [], // the incoming object
|
||||||
|
[2] = Null None [],
|
||||||
|
[3, 4, 5] = CopyDataProperties U8(6) [2, 1, 0], // copy ALL its fields
|
||||||
|
[] = Drop None [5],
|
||||||
|
[] = Drop None [4],
|
||||||
|
|
||||||
|
otherwise (a creation), start empty:
|
||||||
|
|
||||||
|
[0] = Object None [],
|
||||||
|
|
||||||
|
then, for each field write in the run, in order:
|
||||||
|
|
||||||
|
<value> // moved in, or GetArgK for a parameter
|
||||||
|
DefineField Atom(@F) [value, obj]
|
||||||
|
|
||||||
|
and finally:
|
||||||
|
|
||||||
|
[] = Return None [obj],
|
||||||
|
|
||||||
|
Each generated function is appended to the flat function list, gets the next fn
|
||||||
|
id N, and FnById(N) is added to the caller's cpool at some slot K; the call site
|
||||||
|
loads it with Fclosure8 Const(K).
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
DETECTION (one linear walk)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
start a run at an Object, or at the first static-key write to an object.
|
||||||
|
extend it while the next instruction is one of:
|
||||||
|
Object (a nested object is its own run)
|
||||||
|
a value computation feeding a write in this run
|
||||||
|
DefineField, imm == Atom(F) (same object)
|
||||||
|
PutField, imm == Atom(F) (same object)
|
||||||
|
a (re)load of the same object
|
||||||
|
END the run at any of:
|
||||||
|
the object is READ for anything but a field write -- passed to a call,
|
||||||
|
used in an expression, branched on.
|
||||||
|
a static-key write to a DIFFERENT object.
|
||||||
|
|
||||||
|
"Same object": a mutation re-loads the object before each write (each
|
||||||
|
GetArg0 / GetLocCheck is a fresh value), so sameness is judged by the load
|
||||||
|
SOURCE -- the same Arg or Loc -- not the value id. quickjs keeps writes in
|
||||||
|
source order and does not group by object, so interleaved writes to two
|
||||||
|
objects (a.x; b.y; a.z) split into three runs, not two.
|
||||||
|
|
||||||
|
Ignore computed-key writes (DefineArrayEl / PutArrayEl) -- see extensions.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
WORKED EXAMPLE (test), real instructions
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
--- the creation run ---
|
||||||
|
|
||||||
|
DETECT:
|
||||||
|
|
||||||
|
[1] = Object None [],
|
||||||
|
[2] = Push1 None [],
|
||||||
|
[3] = DefineField Atom(file_atom[7]) [2, 1],
|
||||||
|
[4] = Push0 None [],
|
||||||
|
[5] = DefineField Atom(file_atom[8]) [4, 3],
|
||||||
|
[] = PutLoc0 None [5],
|
||||||
|
|
||||||
|
nothing is used from outside, so the function takes no parameters.
|
||||||
|
|
||||||
|
REPLACE with (the run's result stays value [5], so PutLoc0 is untouched):
|
||||||
|
|
||||||
|
[20] = Fclosure8 Const(Kc) [],
|
||||||
|
[5] = Call0 None [20],
|
||||||
|
[] = PutLoc0 None [5],
|
||||||
|
|
||||||
|
new FnById(Nc), name "<create>", arg_count 0:
|
||||||
|
|
||||||
|
[0] = Object None [],
|
||||||
|
[1] = Push1 None [],
|
||||||
|
[2] = DefineField Atom(file_atom[7]) [1, 0],
|
||||||
|
[3] = Push0 None [],
|
||||||
|
[4] = DefineField Atom(file_atom[8]) [3, 2],
|
||||||
|
[] = Return None [4],
|
||||||
|
|
||||||
|
--- the mutation run (a.field1 = 2 ; a.other = 3) ---
|
||||||
|
|
||||||
|
DETECT:
|
||||||
|
|
||||||
|
[6] = GetLocCheck Loc(0) [],
|
||||||
|
[7] = Push2 None [],
|
||||||
|
[] = PutField Atom(file_atom[7]) [7, 6],
|
||||||
|
[8] = GetLocCheck Loc(0) [],
|
||||||
|
[9] = Push3 None [],
|
||||||
|
[] = PutField Atom(file_atom[9]) [9, 8],
|
||||||
|
|
||||||
|
the object (Loc 0) is used-but-not-produced -> the one parameter; 2 and 3 are
|
||||||
|
produced inside -> they move in.
|
||||||
|
|
||||||
|
REPLACE with (store the returned object back):
|
||||||
|
|
||||||
|
[21] = Fclosure8 Const(Km) [],
|
||||||
|
[22] = GetLocCheck Loc(0) [], // the incoming object
|
||||||
|
[23] = Call1 None [22, 21], // a = mutate(a); callee is last
|
||||||
|
[] = PutLoc0 None [23],
|
||||||
|
|
||||||
|
new FnById(Nm), name "<mutate>", arg_count 1:
|
||||||
|
|
||||||
|
[0] = Object None [],
|
||||||
|
[1] = GetArg0 None [],
|
||||||
|
[2] = Null None [],
|
||||||
|
[3, 4, 5] = CopyDataProperties U8(6) [2, 1, 0],
|
||||||
|
[] = Drop None [5],
|
||||||
|
[] = Drop None [4],
|
||||||
|
[6] = Push2 None [],
|
||||||
|
[7] = DefineField Atom(file_atom[7]) [6, 3],
|
||||||
|
[8] = Push3 None [],
|
||||||
|
[9] = DefineField Atom(file_atom[9]) [8, 7],
|
||||||
|
[] = Return None [9],
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
FIELD VALUE FORMS -- all handled the same way
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
constant a.x = 1 Push1 moves into the function
|
||||||
|
argument { x: p } GetArg becomes a parameter
|
||||||
|
computed a.x = p + 1 p is a parameter; the Add moves in
|
||||||
|
field read a.x = b.y b is a parameter; the GetField moves in
|
||||||
|
nested object { x: { y: 1 } } the inner Object is its own run, lifted
|
||||||
|
first; its result is a value the outer
|
||||||
|
run uses -> a parameter of the outer
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
NEEDS A SEPARATE HELPER (extensions)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
spread in a literal { ...b, x: 1 } CopyDataProperties of a source
|
||||||
|
object, not just the input copy
|
||||||
|
computed key { [k]: 1 }, a[k] = 1 DefineArrayEl / PutArrayEl,
|
||||||
|
a[0] = 1 keyed by a value, not an atom
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
GAPS (left for later)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
A mutation run's object must live in a local, so the returned object can be
|
||||||
|
written back and the next read sees it. A bare param or a sub-expression
|
||||||
|
has nowhere to write back.
|
||||||
|
Aliasing / identity: the run returns a NEW object, so two names for the same
|
||||||
|
object diverge, and === changes.
|
||||||
|
Ordering: a run must stay in program order; it is cut wherever the object is
|
||||||
|
read for anything but a field write.
|
||||||
|
Methods ({ m() {} } -> DefineMethod), getters/setters, delete,
|
||||||
|
defineProperty: not plain field writes.
|
||||||
Reference in New Issue
Block a user