Fable rewrite
This commit is contained in:
+151
-166
@@ -11,21 +11,20 @@
|
|||||||
//!
|
//!
|
||||||
//! becomes what the same code written with prototypes would produce:
|
//! becomes what the same code written with prototypes would produce:
|
||||||
//!
|
//!
|
||||||
//! proto = object the prototype
|
//! proto = object the prototype
|
||||||
//! proto' = define_field [proto, method] "len" an ordinary property
|
//! proto' = define_field [proto, method] "len" an ordinary property
|
||||||
//! ctor = define_field [template, proto'] "prototype" hung off the ctor
|
//! ctor = define_field [template, proto'] "prototype" hung off the ctor
|
||||||
//!
|
//!
|
||||||
//! A function is a plain constant here (see `declosure`), so the constructor
|
//! A function is a plain constant here (see `declosure`), so the constructor
|
||||||
//! is just the template constant that `push_const` already produced — no
|
//! is just the template constant that `push_const` already produced.
|
||||||
//! separate instantiation.
|
|
||||||
//!
|
//!
|
||||||
//! A derived class starts its prototype as a COPY of the parent's, then
|
//! A derived class starts its prototype as a COPY of the parent's, then
|
||||||
//! installs its own methods over it, flattening the hierarchy where it is
|
//! installs its own methods over it. That flattens the hierarchy where it is
|
||||||
//! declared so no chain survives to be walked:
|
//! declared, so no chain survives to be walked at run time:
|
||||||
//!
|
//!
|
||||||
//! raw = object
|
//! empty = object
|
||||||
//! pp = get_field parent "prototype"
|
//! parent_proto = get_field parent "prototype"
|
||||||
//! proto = copy_data_properties [raw, pp]
|
//! proto = copy_data_properties [empty, parent_proto]
|
||||||
//!
|
//!
|
||||||
//! Runs BEFORE declosure, so the constructor it leaves behind is an ordinary
|
//! Runs BEFORE declosure, so the constructor it leaves behind is an ordinary
|
||||||
//! function that declosure then handles like any other.
|
//! function that declosure then handles like any other.
|
||||||
@@ -53,31 +52,40 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||||
regify::{RegFn, RegInstr},
|
regify::{Fresh, RegFn, RegInstr, int_field},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Synthetic field name recording a derived class's parent constructor, read
|
/// Synthetic field recording a derived class's parent constructor, read by
|
||||||
/// by `deprototype`'s `super` lowering. Integer-named so no atom is minted; the
|
/// `deprototype`'s `super` lowering. The high value keeps it clear of the
|
||||||
/// high value keeps it clear of the positional fields other passes use.
|
/// small positional fields the other passes use.
|
||||||
pub const PARENT_LINK: u32 = 0xFFFF_FFFF;
|
pub const PARENT_LINK: u32 = 0xFFFF_FFFF;
|
||||||
|
|
||||||
/// The `__parent` field name as an operand.
|
/// The `__parent` field name as an operand.
|
||||||
pub fn parent_link() -> ImmOperand {
|
pub fn parent_link() -> ImmOperand {
|
||||||
ImmOperand::Atom(AtomRef::TaggedInt(PARENT_LINK))
|
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.
|
/// Lower every class in the program to prototype form.
|
||||||
pub fn declass(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
pub fn declass(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||||
// Parameter counts, read before any pass changes them.
|
// Parameter counts, read before anything changes them.
|
||||||
let arity: Vec<usize> = fns.iter().map(|f| f.arg_count).collect();
|
let arity: Vec<usize> = fns.iter().map(|f| f.arg_count).collect();
|
||||||
|
|
||||||
// Derived constructors found while lowering, with the parent's arity.
|
// Filled in while lowering: every constructor's fn id, and each derived
|
||||||
let mut derived: Vec<(usize, usize)> = Vec::new();
|
// constructor paired with its parent's arity.
|
||||||
// Every function that is a class constructor.
|
|
||||||
let mut ctors: Vec<usize> = Vec::new();
|
let mut ctors: Vec<usize> = Vec::new();
|
||||||
|
let mut derived_ctors: Vec<(usize, usize)> = Vec::new();
|
||||||
|
|
||||||
for f in &mut fns {
|
for f in &mut fns {
|
||||||
let mut next = max_val(f) + 1;
|
lower_classes_in(f, &arity, &mut ctors, &mut derived_ctors);
|
||||||
lower(f, &arity, &mut derived, &mut ctors, &mut next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in ctors {
|
for id in ctors {
|
||||||
@@ -87,9 +95,9 @@ pub fn declass(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A derived class with no constructor written gets one, as if it had been.
|
// A derived class with no constructor written gets one, as if it had been.
|
||||||
for (id, parent_arity) in derived {
|
for (id, parent_arity) in derived_ctors {
|
||||||
if let Some(f) = fns.get_mut(id) {
|
if let Some(f) = fns.get_mut(id) {
|
||||||
write_implicit_ctor(f, parent_arity);
|
expand_implicit_ctor(f, parent_arity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fns
|
fns
|
||||||
@@ -97,77 +105,81 @@ pub fn declass(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
|||||||
|
|
||||||
/// A class whose prototype is still collecting methods. It is attached to its
|
/// A class whose prototype is still collecting methods. It is attached to its
|
||||||
/// constructor only once finished, so it never escapes half-built.
|
/// constructor only once finished, so it never escapes half-built.
|
||||||
struct Pending {
|
struct OpenClass {
|
||||||
|
/// The constructor value the rest of the code uses.
|
||||||
ctor: usize,
|
ctor: usize,
|
||||||
cval: usize,
|
/// The constructor's function constant.
|
||||||
proto_cur: usize,
|
template: usize,
|
||||||
|
/// The latest version of the prototype object.
|
||||||
|
proto: usize,
|
||||||
derived: bool,
|
derived: bool,
|
||||||
parent: usize,
|
parent: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lower(
|
/// Lower every class declared in one function.
|
||||||
|
fn lower_classes_in(
|
||||||
f: &mut RegFn,
|
f: &mut RegFn,
|
||||||
arity: &[usize],
|
arity: &[usize],
|
||||||
derived_out: &mut Vec<(usize, usize)>,
|
ctors: &mut Vec<usize>,
|
||||||
ctors_out: &mut Vec<usize>,
|
derived_ctors: &mut Vec<(usize, usize)>,
|
||||||
next: &mut usize,
|
|
||||||
) {
|
) {
|
||||||
// Which values are function-template constants, as `(value, fn id)`.
|
let templates = function_templates(f);
|
||||||
let templates = template_ids(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 mut ctor_of: Vec<(usize, usize)> = Vec::new(); // ctor value -> fn id
|
|
||||||
let mut pending: Vec<Pending> = Vec::new();
|
|
||||||
let code = std::mem::take(&mut f.code);
|
let code = std::mem::take(&mut f.code);
|
||||||
let mut out = Vec::with_capacity(code.len());
|
let mut out = Vec::with_capacity(code.len());
|
||||||
|
|
||||||
for ri in code {
|
for instr in code {
|
||||||
match ri.op {
|
match instr.op {
|
||||||
OpCode::DefineClass if ri.dsts.len() == 2 && ri.args.len() == 2 => {
|
OpCode::DefineClass if instr.dsts.len() == 2 && instr.args.len() == 2 => {
|
||||||
let ImmOperand::AtomU8 { arg: flags, .. } = ri.imm else {
|
let ImmOperand::AtomU8 { arg: flags, .. } = instr.imm else {
|
||||||
out.push(ri);
|
out.push(instr);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let ctor = ri.dsts[0];
|
let ctor = instr.dsts[0];
|
||||||
let proto = ri.dsts[1];
|
let proto = instr.dsts[1];
|
||||||
let template = ri.args[0]; // popped first: top of stack
|
let template = instr.args[0]; // popped first: it was on top of the stack
|
||||||
let parent = ri.args[1];
|
let parent = instr.args[1];
|
||||||
let derived = flags & 1 != 0;
|
let derived = flags & 1 != 0;
|
||||||
|
let ctor_fn = templates.iter().find(|(v, _)| *v == template).map(|(_, id)| *id);
|
||||||
let ctor_fn = templates.iter().find(|(v, _)| *v == template).map(|(_, i)| *i);
|
|
||||||
let cval = template; // function-as-const constructor
|
|
||||||
|
|
||||||
if derived {
|
if derived {
|
||||||
// Flatten the hierarchy: start from a copy of the parent's
|
// Flatten the hierarchy here: the prototype starts as a copy
|
||||||
// prototype, then install this class's methods over it.
|
// of the parent's, and this class's own methods go over it.
|
||||||
let raw = fresh(next);
|
let empty = fresh.new_value();
|
||||||
let pp = fresh(next);
|
let parent_proto = fresh.new_value();
|
||||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![raw], vec![]));
|
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![empty], vec![]));
|
||||||
out.push(instr(OpCode::GetField, proto_atom(), vec![pp], vec![parent]));
|
out.push(RegInstr::new(OpCode::GetField, proto_atom(), vec![parent_proto], vec![parent]));
|
||||||
out.push(instr(
|
out.push(RegInstr::new(
|
||||||
OpCode::CopyDataProperties,
|
OpCode::CopyDataProperties,
|
||||||
ImmOperand::U8(0),
|
ImmOperand::U8(0),
|
||||||
vec![proto],
|
vec![proto],
|
||||||
vec![raw, pp],
|
vec![empty, parent_proto],
|
||||||
));
|
));
|
||||||
|
|
||||||
if let (Some(cid), Some(pid)) = (
|
|
||||||
ctor_fn,
|
|
||||||
ctor_of.iter().find(|(v, _)| *v == parent).map(|(_, i)| *i),
|
|
||||||
) {
|
|
||||||
derived_out.push((cid, arity.get(pid).copied().unwrap_or(0)));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![proto], vec![]));
|
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![proto], vec![]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = ctor_fn {
|
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));
|
ctor_of.push((ctor, id));
|
||||||
ctors_out.push(id);
|
|
||||||
}
|
}
|
||||||
pending.push(Pending {
|
|
||||||
|
open.push(OpenClass {
|
||||||
ctor,
|
ctor,
|
||||||
cval,
|
template,
|
||||||
proto_cur: proto,
|
proto,
|
||||||
derived,
|
derived,
|
||||||
parent,
|
parent,
|
||||||
});
|
});
|
||||||
@@ -175,60 +187,66 @@ fn lower(
|
|||||||
|
|
||||||
// A plain method is an ordinary property on the prototype. Kind 0
|
// A plain method is an ordinary property on the prototype. Kind 0
|
||||||
// only; getters and setters keep their opcode.
|
// only; getters and setters keep their opcode.
|
||||||
OpCode::DefineMethod if method_kind(&ri) == Some(0) && ri.args.len() == 2 => {
|
OpCode::DefineMethod if method_kind(&instr) == Some(0) && instr.args.len() == 2 => {
|
||||||
let ImmOperand::AtomU8 { atom, .. } = ri.imm else {
|
let ImmOperand::AtomU8 { atom: name, .. } = instr.imm else {
|
||||||
out.push(ri);
|
out.push(instr);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let dst = ri.dsts[0];
|
let newer_proto = instr.dsts[0];
|
||||||
let obj = ri.args[1];
|
let method = instr.args[0];
|
||||||
let method = ri.args[0];
|
let proto = instr.args[1];
|
||||||
out.push(instr(OpCode::DefineField, atom_imm(atom), vec![dst], vec![obj, method]));
|
out.push(RegInstr::new(
|
||||||
if let Some(p) = pending.iter_mut().find(|p| p.proto_cur == obj) {
|
OpCode::DefineField,
|
||||||
p.proto_cur = dst;
|
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.
|
// Reading a constructor forces its prototype to be finished.
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < pending.len() {
|
while i < open.len() {
|
||||||
let c = pending[i].ctor;
|
if instr.mentions(open[i].ctor) {
|
||||||
if ri.args.contains(&c) || ri.dsts.contains(&c) {
|
finish_class(open.remove(i), &mut out, &mut fresh);
|
||||||
flush(pending.remove(i), &mut out, next);
|
|
||||||
} else {
|
} else {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.push(ri);
|
out.push(instr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(p) = pending.pop() {
|
// Whatever is still open finishes at the end of the function.
|
||||||
flush(p, &mut out, next);
|
while let Some(class) = open.pop() {
|
||||||
|
finish_class(class, &mut out, &mut fresh);
|
||||||
}
|
}
|
||||||
f.code = out;
|
f.code = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hang the finished prototype off the constructor — this is what `.prototype`
|
/// Hang the finished prototype off the constructor — that is what `.prototype`
|
||||||
/// is. `define_field` returns the object, so the constructor lands in `ctor`. A
|
/// is. `define_field` returns the object, so the constructor lands in `ctor`.
|
||||||
/// derived class also records its parent constructor in `__parent`, which is
|
/// A derived class also records its parent in `__parent`, for `super`.
|
||||||
/// what `deprototype`'s `super` reads.
|
fn finish_class(class: OpenClass, out: &mut Vec<RegInstr>, fresh: &mut Fresh) {
|
||||||
fn flush(p: Pending, out: &mut Vec<RegInstr>, next: &mut usize) {
|
let with_proto = if class.derived { fresh.new_value() } else { class.ctor };
|
||||||
let hung = if p.derived { fresh(next) } else { p.ctor };
|
out.push(RegInstr::new(
|
||||||
out.push(instr(
|
|
||||||
OpCode::DefineField,
|
OpCode::DefineField,
|
||||||
proto_atom(),
|
proto_atom(),
|
||||||
vec![hung],
|
vec![with_proto],
|
||||||
vec![p.cval, p.proto_cur],
|
vec![class.template, class.proto],
|
||||||
));
|
));
|
||||||
if p.derived {
|
if class.derived {
|
||||||
out.push(instr(
|
out.push(RegInstr::new(
|
||||||
OpCode::DefineField,
|
OpCode::DefineField,
|
||||||
parent_link(),
|
parent_link(),
|
||||||
vec![p.ctor],
|
vec![class.ctor],
|
||||||
vec![hung, p.parent],
|
vec![with_proto, class.parent],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,44 +256,44 @@ fn flush(p: Pending, out: &mut Vec<RegInstr>, next: &mut usize) {
|
|||||||
/// arguments. Our calling convention is fixed-arity, so we give the
|
/// arguments. Our calling convention is fixed-arity, so we give the
|
||||||
/// constructor the parent's parameters and write out the `super(...)` it stood
|
/// constructor the parent's parameters and write out the `super(...)` it stood
|
||||||
/// for — after which it is an ordinary explicit constructor.
|
/// for — after which it is an ordinary explicit constructor.
|
||||||
fn write_implicit_ctor(f: &mut RegFn, parent_arity: usize) {
|
fn expand_implicit_ctor(f: &mut RegFn, parent_arity: usize) {
|
||||||
if !f.code.iter().any(|i| i.op == OpCode::InitCtor) {
|
if !f.code.iter().any(|i| i.op == OpCode::InitCtor) {
|
||||||
return; // an explicit constructor: nothing to write
|
return; // an explicit constructor: nothing to write
|
||||||
}
|
}
|
||||||
f.arg_count = parent_arity;
|
f.arg_count = parent_arity;
|
||||||
let mut next = max_val(f) + 1;
|
let mut fresh = Fresh::for_fn(f);
|
||||||
|
|
||||||
let code = std::mem::take(&mut f.code);
|
let code = std::mem::take(&mut f.code);
|
||||||
let mut out = Vec::with_capacity(code.len() + 3 + parent_arity);
|
let mut out = Vec::with_capacity(code.len() + 3 + parent_arity);
|
||||||
for ri in code {
|
for instr in code {
|
||||||
if ri.op != OpCode::InitCtor {
|
if instr.op != OpCode::InitCtor {
|
||||||
out.push(ri);
|
out.push(instr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let this = ri.dsts[0];
|
let this = instr.dsts[0];
|
||||||
|
|
||||||
// `this = super(a0 … an)`, spelled the way a written constructor is.
|
// `this = super(a0 … an)`, spelled the way a written constructor is.
|
||||||
let this_fn = fresh(&mut next);
|
let running_fn = fresh.new_value();
|
||||||
let parent = fresh(&mut next);
|
let parent = fresh.new_value();
|
||||||
let new_target = fresh(&mut next);
|
let new_target = fresh.new_value();
|
||||||
out.push(instr(OpCode::SpecialObject, ImmOperand::U8(2), vec![this_fn], vec![])); // running fn
|
out.push(RegInstr::new(OpCode::SpecialObject, ImmOperand::U8(2), vec![running_fn], vec![]));
|
||||||
out.push(instr(OpCode::GetSuper, ImmOperand::None, vec![parent], vec![this_fn]));
|
out.push(RegInstr::new(OpCode::GetSuper, ImmOperand::None, vec![parent], vec![running_fn]));
|
||||||
out.push(instr(OpCode::Undefined, ImmOperand::None, vec![new_target], vec![]));
|
out.push(RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![new_target], vec![]));
|
||||||
|
|
||||||
let mut argvals = Vec::with_capacity(parent_arity);
|
let mut forwarded = Vec::with_capacity(parent_arity);
|
||||||
for i in 0..parent_arity {
|
for i in 0..parent_arity {
|
||||||
let a = fresh(&mut next);
|
let arg = fresh.new_value();
|
||||||
out.push(instr(OpCode::GetArg, ImmOperand::Arg(i as u16), vec![a], vec![]));
|
out.push(RegInstr::new(OpCode::GetArg, ImmOperand::Arg(i as u16), vec![arg], vec![]));
|
||||||
argvals.push(a);
|
forwarded.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// call_constructor pops [func, new_target, args..], args on top; so the
|
// In register form a call's args run in pop order: the last argument
|
||||||
// register args run: arguments (reversed), new_target, then func.
|
// first, then new.target, then the callee.
|
||||||
let mut args = Vec::with_capacity(parent_arity + 2);
|
let mut args = Vec::with_capacity(parent_arity + 2);
|
||||||
args.extend(argvals.iter().rev().copied());
|
args.extend(forwarded.iter().rev().copied());
|
||||||
args.push(new_target);
|
args.push(new_target);
|
||||||
args.push(parent);
|
args.push(parent);
|
||||||
out.push(instr(
|
out.push(RegInstr::new(
|
||||||
OpCode::CallConstructor,
|
OpCode::CallConstructor,
|
||||||
ImmOperand::Npop(parent_arity as u16),
|
ImmOperand::Npop(parent_arity as u16),
|
||||||
vec![this],
|
vec![this],
|
||||||
@@ -287,65 +305,32 @@ fn write_implicit_ctor(f: &mut RegFn, parent_arity: usize) {
|
|||||||
|
|
||||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
// ─── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Values bound to a nested-function constant, as `(value, fn id)`. A function
|
/// Values holding a nested-function constant, as (value, fn id). A function
|
||||||
/// is loaded with `push_const` (or `fclosure`, before it is lowered).
|
/// loads with `push_const`, or with `fclosure` until declosure lowers it.
|
||||||
fn template_ids(f: &RegFn) -> Vec<(usize, usize)> {
|
fn function_templates(f: &RegFn) -> Vec<(usize, usize)> {
|
||||||
let mut out = Vec::new();
|
let mut templates = Vec::new();
|
||||||
for ri in &f.code {
|
for instr in &f.code {
|
||||||
let is_const = matches!(
|
let loads_function = matches!(
|
||||||
ri.op,
|
instr.op,
|
||||||
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8
|
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8
|
||||||
);
|
);
|
||||||
if let (true, ImmOperand::Const(c), Some(&dst)) = (is_const, ri.imm, ri.dsts.first()) {
|
let (ImmOperand::Const(index), Some(&value)) = (instr.imm, instr.dsts.first()) else {
|
||||||
if let Some(JsValue::FnById(id)) = f.cpool.get(c as usize) {
|
continue;
|
||||||
out.push((dst, *id));
|
};
|
||||||
}
|
if !loads_function {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(JsValue::FnById(id)) = f.cpool.get(index as usize) {
|
||||||
|
templates.push((value, *id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
templates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The method kind carried by a `define_method` (0 = plain method).
|
/// The method kind carried by a `define_method` (0 = plain method).
|
||||||
fn method_kind(ri: &RegInstr) -> Option<u8> {
|
fn method_kind(instr: &RegInstr) -> Option<u8> {
|
||||||
match ri.imm {
|
match instr.imm {
|
||||||
ImmOperand::AtomU8 { arg, .. } => Some(arg),
|
ImmOperand::AtomU8 { arg, .. } => Some(arg),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn instr(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> RegInstr {
|
|
||||||
RegInstr {
|
|
||||||
dsts,
|
|
||||||
op,
|
|
||||||
imm,
|
|
||||||
args,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn atom_imm(a: AtomRef) -> ImmOperand {
|
|
||||||
ImmOperand::Atom(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `"prototype"` field name, resolved to its predefined atom once.
|
|
||||||
pub fn proto_atom() -> ImmOperand {
|
|
||||||
let idx = crate::parse::atom_names::PREDEFINED_ATOMS
|
|
||||||
.iter()
|
|
||||||
.position(|&a| a == "prototype")
|
|
||||||
.expect("prototype is a predefined atom");
|
|
||||||
ImmOperand::Atom(AtomRef::Predefined(idx as u32))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fresh(next: &mut usize) -> usize {
|
|
||||||
let v = *next;
|
|
||||||
*next += 1;
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
fn max_val(f: &RegFn) -> usize {
|
|
||||||
f.code
|
|
||||||
.iter()
|
|
||||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
|
||||||
.copied()
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|||||||
+178
-219
@@ -1,293 +1,252 @@
|
|||||||
//! Transform: closures become plain objects.
|
//! Transform: closures become plain objects.
|
||||||
//!
|
//!
|
||||||
//! A closure becomes an object holding its captured variables plus its
|
//! A closure becomes an object holding its function plus its captured
|
||||||
//! function. Field 0 is the function; fields 1.. are the captures, in
|
//! variables. Field 0 is the function; fields 1.. are the captures, in
|
||||||
//! capture-index order. Integer field names (`AtomRef::TaggedInt`) are used
|
//! capture-index order. The object doubles as the environment, so there is no
|
||||||
//! so no new atoms have to be minted. The object doubles as the environment,
|
//! second env value to track.
|
||||||
//! so there is no second env value to track.
|
|
||||||
//!
|
//!
|
||||||
//! Three passes, one property each:
|
//! Three steps, one property each:
|
||||||
//!
|
//!
|
||||||
//! 1. `pass_env_param` — every function takes the env as its LAST
|
//! 1. `take_env_param` — every function takes the env as its LAST
|
||||||
//! parameter and reads its captures out of it, so
|
//! parameter and reads its captures out of
|
||||||
//! `get_var_ref` / `put_var_ref` become field
|
//! it: `get_var_ref` / `put_var_ref` become
|
||||||
//! access on the env.
|
//! field access on the env.
|
||||||
//! 2. `pass_closure_object` — every `fclosure` builds the object: the
|
//! 2. `build_closure_objects` — every `fclosure` builds the object: the
|
||||||
//! function in field 0, then one field per
|
//! function in field 0, then one field per
|
||||||
//! capture, each loaded from the parent frame.
|
//! capture, loaded from the parent frame.
|
||||||
//! 3. `pass_call_site` — every plain call reads field 0 and passes the
|
//! 3. `route_calls_through_object` — every plain call reads field 0 and
|
||||||
//! object itself as the trailing env argument.
|
//! passes the object itself as the trailing
|
||||||
|
//! env argument.
|
||||||
//!
|
//!
|
||||||
//! Every function takes an env whether it captures anything or not, so pass 3
|
//! Every function takes an env whether it captures anything or not, so step 3
|
||||||
//! can append the object at a uniform call site.
|
//! can append the object at a uniform call site.
|
||||||
//!
|
//!
|
||||||
//! Notes on this lowering:
|
//! Two facts about the register form this leans on:
|
||||||
//!
|
//!
|
||||||
//! * A function is treated as a plain constant — a pointer into the cpool.
|
//! * A function is a plain constant — a pointer into the cpool. `fclosure`
|
||||||
//! `fclosure` is replaced by `push_const`, so it loads like any other
|
//! becomes `push_const`, so it loads like any other const, and storing it
|
||||||
//! const, and storing it in a field is no different from storing a number.
|
//! in a field is no different from storing a number.
|
||||||
//! * In register form the callee is the LAST call argument (it is the
|
//! * The callee of a call is its LAST argument (the deepest stack slot,
|
||||||
//! deepest stack slot, popped last), so pass 3 reads `args.last()`.
|
//! popped last), so step 3 reads `args.last()`.
|
||||||
//!
|
//!
|
||||||
//! Known gaps, in rough order of how likely they are to bite:
|
//! Known gaps, in rough order of how likely they are to bite:
|
||||||
//!
|
//!
|
||||||
//! * **A captured variable the parent later writes.** The write produces a
|
//! * **A captured variable the parent later writes.** The write produces a
|
||||||
//! new value instead of storing through the alias, so the closure never
|
//! 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.
|
//! sees it. A capture aliases the value a slot held at capture time.
|
||||||
//! * **Non-local capture sources.** Pass 2 can materialize a capture that
|
//! * **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
|
//! 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
|
//! parent's captures, a module binding, or a global is left as an
|
||||||
//! `undefined` placeholder — it needs the parent to forward its own env.
|
//! `undefined` placeholder — it needs the parent to forward its own env.
|
||||||
//! * **Foreign callees.** Pass 3 rewrites every plain call into a field-0
|
//! * **Foreign callees.** Step 3 rewrites every plain call into a field-0
|
||||||
//! read. That is only valid for functions we produced; a host builtin
|
//! read. That is only valid for functions we produced; a host builtin
|
||||||
//! such as `Math.sqrt` has no such field.
|
//! such as `Math.sqrt` has no such field.
|
||||||
//! * **Entry points.** Pass 1 gives every function an env parameter, but the
|
//! * **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
|
//! module root and any host-invoked function have no caller of ours to
|
||||||
//! supply one.
|
//! supply one.
|
||||||
//! * **`call_method` and `call_constructor`.** They carry a receiver, so
|
//! * **`call_method` and `call_constructor`.** They carry a receiver, so
|
||||||
//! they belong to the method and class transforms; pass 3 leaves them
|
//! they belong to the class passes; step 3 leaves them untouched. Same for
|
||||||
//! untouched. Same for `apply`.
|
//! `apply`.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
flatten::CaptureFrom,
|
flatten::CaptureFrom,
|
||||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
parse::{ImmOperand, JsValue, opcodes::OpCode},
|
||||||
regify::{RegFn, RegInstr},
|
regify::{Fresh, RegFn, RegInstr, as_plain_call, int_field, is_plain_call},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Capture sources per fn id, snapshotted before pass 1 clears the lists.
|
/// Which capture (closure variable) an instruction touches, and how.
|
||||||
type Sources = Vec<Vec<CaptureFrom>>;
|
enum CaptureAccess {
|
||||||
|
Read(usize),
|
||||||
|
Write(usize),
|
||||||
|
}
|
||||||
|
|
||||||
pub fn declosure(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
pub fn declosure(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||||
let sources: Sources = fns
|
// 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()
|
.iter()
|
||||||
.map(|f| f.captures.iter().map(|c| c.from).collect())
|
.map(|f| f.captures.iter().map(|c| c.from).collect())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for f in &mut fns {
|
for f in &mut fns {
|
||||||
let mut next = max_val(f) + 1;
|
let mut fresh = Fresh::for_fn(f);
|
||||||
pass_env_param(f, &mut next);
|
take_env_param(f, &mut fresh);
|
||||||
pass_closure_object(f, &sources, &mut next);
|
build_closure_objects(f, &capture_sources, &mut fresh);
|
||||||
pass_call_site(f, &mut next);
|
route_calls_through_object(f, &mut fresh);
|
||||||
}
|
}
|
||||||
fns
|
fns
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 1. every function takes an env ────────────────────────────────────────
|
// ─── 1. every function takes an env ────────────────────────────────────────
|
||||||
|
|
||||||
/// Append `env` as the last parameter — so existing arguments keep their
|
/// Append `env` as the last parameter — existing arguments keep their numbers —
|
||||||
/// numbers — and turn every capture access into a field access on it. A read
|
/// and turn every capture access into a field access on it.
|
||||||
/// keeps its destination value, so uses downstream do not change.
|
fn take_env_param(f: &mut RegFn, fresh: &mut Fresh) {
|
||||||
fn pass_env_param(f: &mut RegFn, next: &mut usize) {
|
let env_param = f.arg_count;
|
||||||
let env_idx = f.arg_count;
|
|
||||||
f.arg_count += 1;
|
f.arg_count += 1;
|
||||||
let env = fresh(next);
|
let env = fresh.new_value();
|
||||||
|
|
||||||
let code = std::mem::take(&mut f.code);
|
let code = std::mem::take(&mut f.code);
|
||||||
let mut out = Vec::with_capacity(code.len() + 1);
|
let touches_captures = code.iter().any(|i| capture_access(i).is_some());
|
||||||
let mut used = false;
|
|
||||||
|
|
||||||
for ri in code {
|
let mut out = Vec::with_capacity(code.len() + 1);
|
||||||
match varref(&ri) {
|
|
||||||
Some((false, k)) => {
|
// Load the env once at entry — only if a capture is actually accessed.
|
||||||
used = true;
|
if touches_captures {
|
||||||
out.push(instr(OpCode::GetField, field(k + 1), ri.dsts, vec![env]));
|
let load_env = ImmOperand::Arg(env_param as u16);
|
||||||
}
|
out.push(RegInstr::new(OpCode::GetArg, load_env, vec![env], vec![]));
|
||||||
Some((true, k)) => {
|
|
||||||
used = true;
|
|
||||||
let mut args = vec![env];
|
|
||||||
args.extend(ri.args); // the value being stored
|
|
||||||
out.push(instr(OpCode::PutField, field(k + 1), vec![], args));
|
|
||||||
}
|
|
||||||
None => out.push(ri),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the env once, at entry — only if a capture actually reads it.
|
for instr in code {
|
||||||
if used {
|
match capture_access(&instr) {
|
||||||
out.insert(
|
// A read keeps its destination, so downstream uses do not change.
|
||||||
0,
|
Some(CaptureAccess::Read(k)) => {
|
||||||
instr(OpCode::GetArg, ImmOperand::Arg(env_idx as u16), vec![env], vec![]),
|
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.code = out;
|
||||||
f.captures.clear();
|
f.captures.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 2. every fclosure builds the object ───────────────────────────────────
|
/// Which capture an instruction reads or writes, if any.
|
||||||
|
fn capture_access(instr: &RegInstr) -> Option<CaptureAccess> {
|
||||||
/// Replace each `fclosure` with: the function const (field 0), then one
|
|
||||||
/// `define_field` per capture, each loaded from the parent frame. The stores
|
|
||||||
/// chain, and the last lands in the closure's original destination.
|
|
||||||
fn pass_closure_object(f: &mut RegFn, sources: &Sources, next: &mut usize) {
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len());
|
|
||||||
|
|
||||||
for ri in code {
|
|
||||||
let child = fclosure_target(&ri, &f.cpool);
|
|
||||||
let (Some(child_id), Some(&dst)) = (child, ri.dsts.first()) else {
|
|
||||||
out.push(ri);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The function is a plain constant — a pointer into the cpool — so it
|
|
||||||
// loads with `push_const`, no different from any other const.
|
|
||||||
let load_op = match ri.op {
|
|
||||||
OpCode::Fclosure8 => OpCode::PushConst8,
|
|
||||||
_ => OpCode::PushConst,
|
|
||||||
};
|
|
||||||
let fp = fresh(next);
|
|
||||||
out.push(instr(load_op, ri.imm, vec![fp], vec![]));
|
|
||||||
|
|
||||||
let obj = fresh(next);
|
|
||||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![obj], vec![]));
|
|
||||||
|
|
||||||
let caps: &[CaptureFrom] = sources.get(child_id).map(Vec::as_slice).unwrap_or(&[]);
|
|
||||||
|
|
||||||
// Field 0 is the function. It is the last store only when there are no
|
|
||||||
// captures; otherwise the last capture lands in `dst`.
|
|
||||||
let mut cur = if caps.is_empty() { dst } else { fresh(next) };
|
|
||||||
out.push(instr(OpCode::DefineField, field(0), vec![cur], vec![obj, fp]));
|
|
||||||
|
|
||||||
for (k, from) in caps.iter().enumerate() {
|
|
||||||
let cap = fresh(next);
|
|
||||||
out.push(load_capture(*from, cap));
|
|
||||||
let last = k + 1 == caps.len();
|
|
||||||
let d = if last { dst } else { fresh(next) };
|
|
||||||
out.push(instr(OpCode::DefineField, field(k + 1), vec![d], vec![cur, cap]));
|
|
||||||
cur = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The instruction to materialize one capture in the parent frame. Only parent
|
|
||||||
/// locals and args are supported; other sources are a preserved gap and load
|
|
||||||
/// `undefined`.
|
|
||||||
fn load_capture(from: CaptureFrom, dst: usize) -> RegInstr {
|
|
||||||
match from {
|
|
||||||
CaptureFrom::ParentLocal(i) => {
|
|
||||||
instr(OpCode::GetLoc, ImmOperand::Loc(i as u16), vec![dst], vec![])
|
|
||||||
}
|
|
||||||
CaptureFrom::ParentArg(i) => {
|
|
||||||
instr(OpCode::GetArg, ImmOperand::Arg(i as u16), vec![dst], vec![])
|
|
||||||
}
|
|
||||||
_ => instr(OpCode::Undefined, ImmOperand::None, vec![dst], vec![]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 3. every call goes through the object ─────────────────────────────────
|
|
||||||
|
|
||||||
/// A call now names a closure object, not a function, so it reads field 0 and
|
|
||||||
/// passes the object itself as the trailing env argument.
|
|
||||||
fn pass_call_site(f: &mut RegFn, next: &mut usize) {
|
|
||||||
let code = std::mem::take(&mut f.code);
|
|
||||||
let mut out = Vec::with_capacity(code.len());
|
|
||||||
|
|
||||||
for ri in code {
|
|
||||||
if !is_plain_call(ri.op) || ri.args.is_empty() {
|
|
||||||
out.push(ri);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In register form the callee is the deepest arg, popped last.
|
|
||||||
let callee = *ri.args.last().unwrap();
|
|
||||||
let fnp = fresh(next);
|
|
||||||
out.push(instr(OpCode::GetField, field(0), vec![fnp], vec![callee]));
|
|
||||||
|
|
||||||
let mut args = Vec::with_capacity(ri.args.len() + 1);
|
|
||||||
args.push(fnp);
|
|
||||||
args.extend_from_slice(&ri.args[..ri.args.len() - 1]);
|
|
||||||
args.push(callee); // the env, last
|
|
||||||
|
|
||||||
let op = if is_tail_call(ri.op) {
|
|
||||||
OpCode::TailCall
|
|
||||||
} else {
|
|
||||||
OpCode::Call
|
|
||||||
};
|
|
||||||
out.push(instr(op, ri.imm, ri.dsts, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
f.code = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── classification ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// `(is_write, capture_index)` if the instruction is a closure-var access.
|
|
||||||
fn varref(ri: &RegInstr) -> Option<(bool, usize)> {
|
|
||||||
use OpCode::*;
|
use OpCode::*;
|
||||||
let imm_idx = || match ri.imm {
|
let slot_in_operand = || match instr.imm {
|
||||||
ImmOperand::VarRef(k) => k as usize,
|
ImmOperand::VarRef(k) => k as usize,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
Some(match ri.op {
|
Some(match instr.op {
|
||||||
GetVarRef | GetVarRefCheck => (false, imm_idx()),
|
GetVarRef | GetVarRefCheck => CaptureAccess::Read(slot_in_operand()),
|
||||||
GetVarRef0 => (false, 0),
|
GetVarRef0 => CaptureAccess::Read(0),
|
||||||
GetVarRef1 => (false, 1),
|
GetVarRef1 => CaptureAccess::Read(1),
|
||||||
GetVarRef2 => (false, 2),
|
GetVarRef2 => CaptureAccess::Read(2),
|
||||||
GetVarRef3 => (false, 3),
|
GetVarRef3 => CaptureAccess::Read(3),
|
||||||
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => (true, imm_idx()),
|
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => CaptureAccess::Write(slot_in_operand()),
|
||||||
PutVarRef0 => (true, 0),
|
PutVarRef0 => CaptureAccess::Write(0),
|
||||||
PutVarRef1 => (true, 1),
|
PutVarRef1 => CaptureAccess::Write(1),
|
||||||
PutVarRef2 => (true, 2),
|
PutVarRef2 => CaptureAccess::Write(2),
|
||||||
PutVarRef3 => (true, 3),
|
PutVarRef3 => CaptureAccess::Write(3),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The child fn id if this instruction is an `fclosure` over a cpool function.
|
// ─── 2. every fclosure builds the object ───────────────────────────────────
|
||||||
fn fclosure_target(ri: &RegInstr, cpool: &[JsValue]) -> Option<usize> {
|
|
||||||
if !matches!(ri.op, OpCode::Fclosure | OpCode::Fclosure8) {
|
/// 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;
|
return None;
|
||||||
}
|
}
|
||||||
let idx = match ri.imm {
|
let ImmOperand::Const(index) = instr.imm else {
|
||||||
ImmOperand::Const(c) => c as usize,
|
return None;
|
||||||
_ => return None,
|
|
||||||
};
|
};
|
||||||
match cpool.get(idx) {
|
match cpool.get(index as usize) {
|
||||||
Some(JsValue::FnById(id)) => Some(*id),
|
Some(JsValue::FnById(id)) => Some(*id),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `call_method` and `call_constructor` carry a receiver and are left alone.
|
/// Load one captured value out of the parent frame into `into`. Only parent
|
||||||
fn is_plain_call(op: OpCode) -> bool {
|
/// locals and args are supported; any other source is a preserved gap and
|
||||||
use OpCode::*;
|
/// loads `undefined`.
|
||||||
matches!(op, Call | Call0 | Call1 | Call2 | Call3 | TailCall)
|
fn load_capture(source: CaptureFrom, into: usize) -> RegInstr {
|
||||||
}
|
match source {
|
||||||
|
CaptureFrom::ParentLocal(slot) => {
|
||||||
fn is_tail_call(op: OpCode) -> bool {
|
RegInstr::new(OpCode::GetLoc, ImmOperand::Loc(slot as u16), vec![into], vec![])
|
||||||
matches!(op, OpCode::TailCall)
|
}
|
||||||
}
|
CaptureFrom::ParentArg(slot) => {
|
||||||
|
RegInstr::new(OpCode::GetArg, ImmOperand::Arg(slot as u16), vec![into], vec![])
|
||||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
}
|
||||||
|
_ => RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![into], vec![]),
|
||||||
fn instr(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> RegInstr {
|
|
||||||
RegInstr {
|
|
||||||
dsts,
|
|
||||||
op,
|
|
||||||
imm,
|
|
||||||
args,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Integer field name: 0 for the function, k+1 for capture k.
|
// ─── 3. every call goes through the object ─────────────────────────────────
|
||||||
fn field(k: usize) -> ImmOperand {
|
|
||||||
ImmOperand::Atom(AtomRef::TaggedInt(k as u32))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fresh(next: &mut usize) -> usize {
|
/// A call names a closure object now, not a function. Read the function out of
|
||||||
let v = *next;
|
/// field 0, and pass the object itself as the trailing env argument.
|
||||||
*next += 1;
|
fn route_calls_through_object(f: &mut RegFn, fresh: &mut Fresh) {
|
||||||
v
|
let code = std::mem::take(&mut f.code);
|
||||||
}
|
let mut out = Vec::with_capacity(code.len());
|
||||||
|
|
||||||
fn max_val(f: &RegFn) -> usize {
|
for instr in code {
|
||||||
f.code
|
if !is_plain_call(instr.op) || instr.args.is_empty() {
|
||||||
.iter()
|
out.push(instr);
|
||||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
continue;
|
||||||
.copied()
|
}
|
||||||
.max()
|
|
||||||
.unwrap_or(0)
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-172
@@ -5,28 +5,32 @@
|
|||||||
//! bodies reading an implicit `this`. This pass removes both remaining pieces
|
//! bodies reading an implicit `this`. This pass removes both remaining pieces
|
||||||
//! of machinery — the implicit receiver, and the prototype chain.
|
//! of machinery — the implicit receiver, and the prototype chain.
|
||||||
//!
|
//!
|
||||||
//! * **`this` becomes a parameter.** Every function takes `self` as its last
|
//! Three steps:
|
||||||
//! parameter, and `this` (the `push_this` opcode) reads it. Appended, like
|
|
||||||
//! the env, so existing arguments keep their numbers. Applied uniformly — a
|
|
||||||
//! function that never mentions `this` still takes one — because a call
|
|
||||||
//! site cannot know whether its callee does.
|
|
||||||
//!
|
//!
|
||||||
//! * **A constructor builds and returns its object.** `new C(a)` makes a
|
//! 1. `take_self_param` — every function takes `self` as its last
|
||||||
//! fresh object carrying the prototype's properties (`copy_data_properties`,
|
//! parameter, and `this` (`push_this`) reads
|
||||||
//! the opcode behind `{...obj}`) and runs the constructor over it; the
|
//! it. Applied uniformly — a function that
|
||||||
//! object is the result. Field writes on `this` thread — each `put_field`
|
//! never mentions `this` still takes one —
|
||||||
//! becomes a `define_field` yielding the next version — and the constructor
|
//! because a call site cannot know whether
|
||||||
//! returns the last. The instance owns its methods (declass installed them
|
//! its callee does.
|
||||||
//! with a plain `define_field`, so `{...proto}` copies them), so a
|
//! 2. `build_and_return_object` — constructors only. `new C(a)` makes a
|
||||||
//! `get_field` finds one exactly as it finds a field, and no chain is
|
//! fresh object carrying the prototype's
|
||||||
//! walked at run time.
|
//! properties (`copy_data_properties`, the
|
||||||
//!
|
//! opcode behind `{...obj}`) and runs the
|
||||||
//! * **A method call is a plain call.** `o.m(a)` passes `o` as the trailing
|
//! constructor over it; the object is the
|
||||||
//! receiver rather than binding it implicitly.
|
//! 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,
|
//! Runs BETWEEN declass and declosure, so parameters end up `(args…, self,
|
||||||
//! env)`: this pass appends `self`, declosure appends `env` last, and each call
|
//! env)`: this pass appends `self`, declosure appends `env` last, and each call
|
||||||
//! site is left with its callee still last so declosure can find it.
|
//! keeps its callee last so declosure can find it.
|
||||||
//!
|
//!
|
||||||
//! Divergence from the closure-IR original: there a constructor read its
|
//! Divergence from the closure-IR original: there a constructor read its
|
||||||
//! prototype off its env, because the env WAS the `{fn, prototype}` object.
|
//! prototype off its env, because the env WAS the `{fn, prototype}` object.
|
||||||
@@ -48,201 +52,203 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
declass::{parent_link, proto_atom},
|
declass::{parent_link, proto_atom},
|
||||||
parse::{ImmOperand, opcodes::OpCode},
|
parse::{ImmOperand, opcodes::OpCode},
|
||||||
regify::{RegFn, RegInstr},
|
regify::{Fresh, RegFn, RegInstr, as_plain_call, is_plain_call},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sentinel for "this function has no `this` value".
|
|
||||||
const NO_SELF: usize = usize::MAX;
|
|
||||||
|
|
||||||
/// Lower prototypes and `this` across the program.
|
/// Lower prototypes and `this` across the program.
|
||||||
pub fn deprototype(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
pub fn deprototype(mut fns: Vec<RegFn>) -> Vec<RegFn> {
|
||||||
for f in &mut fns {
|
for f in &mut fns {
|
||||||
let mut next = max_val(f) + 1;
|
let mut fresh = Fresh::for_fn(f);
|
||||||
let self_v = take_self_param(f, &mut next);
|
let this = take_self_param(f, &mut fresh);
|
||||||
if f.is_ctor {
|
if f.is_ctor {
|
||||||
produce_self(f, self_v, &mut next);
|
build_and_return_object(f, this, &mut fresh);
|
||||||
}
|
}
|
||||||
lower_calls(f, &mut next);
|
pass_receivers_explicitly(f, &mut fresh);
|
||||||
}
|
}
|
||||||
fns
|
fns
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── `this` becomes a parameter ────────────────────────────────────────────
|
// ─── 1. `this` becomes a parameter ─────────────────────────────────────────
|
||||||
|
|
||||||
/// Append `self`, then point every `push_this` at it. The `push_this` opcodes
|
/// Append `self` as the last parameter, then point every `push_this` at it.
|
||||||
/// vanish, replaced by a single load of the new parameter.
|
/// Returns the value `this` now lives in — `None` if the function never reads
|
||||||
fn take_self_param(f: &mut RegFn, next: &mut usize) -> usize {
|
/// it, in which case nothing is loaded.
|
||||||
let self_idx = f.arg_count;
|
fn take_self_param(f: &mut RegFn, fresh: &mut Fresh) -> Option<usize> {
|
||||||
|
let self_param = f.arg_count;
|
||||||
f.arg_count += 1;
|
f.arg_count += 1;
|
||||||
|
|
||||||
let bound: Vec<usize> = f
|
// The values `push_this` produced.
|
||||||
|
let this_values: Vec<usize> = f
|
||||||
.code
|
.code
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.op == OpCode::PushThis)
|
.filter(|i| i.op == OpCode::PushThis)
|
||||||
.flat_map(|i| i.dsts.iter().copied())
|
.flat_map(|i| i.dsts.iter().copied())
|
||||||
.collect();
|
.collect();
|
||||||
if bound.is_empty() {
|
if this_values.is_empty() {
|
||||||
return NO_SELF;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let self_v = fresh(next);
|
// 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 code = std::mem::take(&mut f.code);
|
||||||
let mut out = Vec::with_capacity(code.len() + 1);
|
let mut out = Vec::with_capacity(code.len() + 1);
|
||||||
out.push(instr(OpCode::GetArg, ImmOperand::Arg(self_idx as u16), vec![self_v], vec![]));
|
let load_self = ImmOperand::Arg(self_param as u16);
|
||||||
for mut ri in code {
|
out.push(RegInstr::new(OpCode::GetArg, load_self, vec![this], vec![]));
|
||||||
if ri.op == OpCode::PushThis {
|
|
||||||
continue; // its reads are redirected to `self`
|
for mut instr in code {
|
||||||
|
if instr.op == OpCode::PushThis {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
for v in ri.args.iter_mut().chain(ri.dsts.iter_mut()) {
|
for v in instr.args.iter_mut().chain(instr.dsts.iter_mut()) {
|
||||||
if bound.contains(v) {
|
if this_values.contains(v) {
|
||||||
*v = self_v;
|
*v = this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.push(ri);
|
out.push(instr);
|
||||||
}
|
}
|
||||||
f.code = out;
|
f.code = out;
|
||||||
self_v
|
Some(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── a constructor produces its object ─────────────────────────────────────
|
// ─── 2. a constructor produces its object ──────────────────────────────────
|
||||||
|
|
||||||
/// Rewrite a constructor from "fill the object you were handed" to "build one
|
/// 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
|
/// and return it". A base constructor starts from a fresh object carrying the
|
||||||
/// prototype's properties, read off the running function (the constructor
|
/// prototype's properties, read off the running function (the constructor
|
||||||
/// object). A derived one starts from whatever `super(...)` returns.
|
/// object). A derived one starts from whatever `super(...)` returns.
|
||||||
fn produce_self(f: &mut RegFn, self_v: usize, next: &mut usize) {
|
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 derived = f.code.iter().any(|i| i.op == OpCode::GetSuper);
|
||||||
|
|
||||||
let code = std::mem::take(&mut f.code);
|
let code = std::mem::take(&mut f.code);
|
||||||
let mut out = Vec::with_capacity(code.len() + 4);
|
let mut out = Vec::with_capacity(code.len() + 4);
|
||||||
let mut cur = self_v;
|
|
||||||
|
// The current version of the object under construction. A derived class
|
||||||
|
// has none until `super(...)` returns one.
|
||||||
|
let mut obj = this;
|
||||||
|
|
||||||
if !derived {
|
if !derived {
|
||||||
let cfn = fresh(next);
|
let running_fn = fresh.new_value();
|
||||||
let proto = fresh(next);
|
let proto = fresh.new_value();
|
||||||
let raw = fresh(next);
|
let empty = fresh.new_value();
|
||||||
let obj = fresh(next);
|
let built = fresh.new_value();
|
||||||
out.push(instr(OpCode::SpecialObject, ImmOperand::U8(2), vec![cfn], vec![])); // running fn
|
out.push(RegInstr::new(OpCode::SpecialObject, ImmOperand::U8(2), vec![running_fn], vec![]));
|
||||||
out.push(instr(OpCode::GetField, proto_atom(), vec![proto], vec![cfn]));
|
out.push(RegInstr::new(OpCode::GetField, proto_atom(), vec![proto], vec![running_fn]));
|
||||||
out.push(instr(OpCode::Object, ImmOperand::None, vec![raw], vec![]));
|
out.push(RegInstr::new(OpCode::Object, ImmOperand::None, vec![empty], vec![]));
|
||||||
out.push(instr(OpCode::CopyDataProperties, ImmOperand::U8(0), vec![obj], vec![raw, proto]));
|
out.push(RegInstr::new(
|
||||||
cur = obj;
|
OpCode::CopyDataProperties,
|
||||||
|
ImmOperand::U8(0),
|
||||||
|
vec![built],
|
||||||
|
vec![empty, proto],
|
||||||
|
));
|
||||||
|
obj = Some(built);
|
||||||
}
|
}
|
||||||
|
|
||||||
for mut ri in code {
|
for mut instr in code {
|
||||||
// Reads of `this` see the current object version. Only uses are
|
// Reads of `this` see the current version of the object. Only uses are
|
||||||
// repointed — `self`'s own load keeps its dead destination, so `cur`'s
|
// repointed: `self`'s own load keeps its (now dead) destination.
|
||||||
// definition is not duplicated.
|
if let (Some(this), Some(current)) = (this, obj) {
|
||||||
for v in ri.args.iter_mut() {
|
for v in instr.args.iter_mut() {
|
||||||
if *v == self_v {
|
if *v == this {
|
||||||
*v = cur;
|
*v = current;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match ri.op {
|
|
||||||
// `this.f = v` becomes ordinary construction, threading a new
|
match instr.op {
|
||||||
// version of the object.
|
// `this.f = v` becomes construction: the next version of the object.
|
||||||
OpCode::PutField if ri.args.len() == 2 && ri.args[1] == cur => {
|
OpCode::PutField if instr.args.len() == 2 && obj == Some(instr.args[1]) => {
|
||||||
let d = fresh(next);
|
let value = instr.args[0];
|
||||||
let val = ri.args[0];
|
let current = instr.args[1];
|
||||||
out.push(instr(OpCode::DefineField, ri.imm, vec![d], vec![cur, val]));
|
let newer = fresh.new_value();
|
||||||
cur = d;
|
out.push(RegInstr::new(OpCode::DefineField, instr.imm, vec![newer], vec![current, value]));
|
||||||
|
obj = Some(newer);
|
||||||
}
|
}
|
||||||
// `super(...)` produced `this`; thread from its result.
|
// `super(...)` produced the object; continue from its result.
|
||||||
OpCode::CallConstructor if derived && !ri.dsts.is_empty() => {
|
OpCode::CallConstructor if derived && !instr.dsts.is_empty() => {
|
||||||
cur = ri.dsts[0];
|
obj = Some(instr.dsts[0]);
|
||||||
out.push(ri);
|
out.push(instr);
|
||||||
}
|
}
|
||||||
OpCode::Return | OpCode::ReturnUndef => {
|
// The constructor returns the object it built.
|
||||||
out.push(instr(OpCode::Return, ImmOperand::None, vec![], vec![cur]));
|
OpCode::Return | OpCode::ReturnUndef if obj.is_some() => {
|
||||||
|
out.push(RegInstr::new(OpCode::Return, ImmOperand::None, vec![], vec![obj.unwrap()]));
|
||||||
}
|
}
|
||||||
_ => out.push(ri),
|
_ => out.push(instr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.code = out;
|
f.code = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── calls carry their receiver ────────────────────────────────────────────
|
// ─── 3. calls carry their receiver ─────────────────────────────────────────
|
||||||
|
|
||||||
fn lower_calls(f: &mut RegFn, next: &mut usize) {
|
/// 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 code = std::mem::take(&mut f.code);
|
||||||
let mut out = Vec::with_capacity(code.len());
|
let mut out = Vec::with_capacity(code.len());
|
||||||
|
|
||||||
// `get_field2`'s kept receiver echoes the object it read; record what each
|
// `get_field2` also keeps the object it read from, under a second value.
|
||||||
// echo really is, and substitute at the end.
|
// That echo is just the object; recorded here and substituted at the end.
|
||||||
let mut kept: Vec<(usize, usize)> = Vec::new();
|
let mut receiver_alias: Vec<(usize, usize)> = Vec::new();
|
||||||
|
|
||||||
for ri in code {
|
for instr in code {
|
||||||
match ri.op {
|
match instr.op {
|
||||||
// `new` is gone, so the guard against calling without it is too.
|
// `new` is gone, so the guard against calling without it goes too.
|
||||||
OpCode::CheckCtor => {}
|
OpCode::CheckCtor => {}
|
||||||
|
|
||||||
// `super` is the parent constructor, recorded by declass as a
|
// `super` is the parent constructor, recorded by declass as a
|
||||||
// field on the constructor object.
|
// field on the constructor object.
|
||||||
OpCode::GetSuper if ri.args.len() == 1 && !ri.dsts.is_empty() => {
|
OpCode::GetSuper if instr.args.len() == 1 && !instr.dsts.is_empty() => {
|
||||||
out.push(instr(OpCode::GetField, parent_link(), vec![ri.dsts[0]], vec![ri.args[0]]));
|
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 [call args.., func, recv];
|
// `o.m(a)` -> `m(a, o)`. Register args are `[a.., m, o]`.
|
||||||
// the new call keeps its callee (func) last so declosure finds it.
|
OpCode::CallMethod | OpCode::TailCallMethod if instr.args.len() >= 2 => {
|
||||||
OpCode::CallMethod | OpCode::TailCallMethod if ri.args.len() >= 2 => {
|
let n = instr.args.len();
|
||||||
let n = ri.args.len();
|
let receiver = instr.args[n - 1];
|
||||||
let recv = ri.args[n - 1];
|
let callee = instr.args[n - 2];
|
||||||
let func = ri.args[n - 2];
|
let args = args_with_self(&instr.args[..n - 2], receiver, callee);
|
||||||
let mut args = Vec::with_capacity(n);
|
out.push(RegInstr::new(as_plain_call(instr.op), instr.imm, instr.dsts, args));
|
||||||
args.extend_from_slice(&ri.args[..n - 2]);
|
|
||||||
args.push(recv); // self
|
|
||||||
args.push(func); // callee, last
|
|
||||||
let op = tail_or_plain(ri.op);
|
|
||||||
out.push(instr(op, ri.imm, ri.dsts, args));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `new C(a)`: the constructor now builds and returns the object, so
|
// `new C(a)`: the constructor builds and returns the object now, so
|
||||||
// this is a plain call yielding it. `self` is undefined; the
|
// this is a plain call yielding it. `self` is undefined; the value
|
||||||
// dropped `args[n-2]` was new.target.
|
// dropped is new.target. Register args are `[a.., new.target, C]`.
|
||||||
OpCode::CallConstructor if ri.args.len() >= 2 => {
|
OpCode::CallConstructor if instr.args.len() >= 2 => {
|
||||||
let n = ri.args.len();
|
let n = instr.args.len();
|
||||||
let func = ri.args[n - 1];
|
let callee = instr.args[n - 1];
|
||||||
let undef = fresh(next);
|
let undefined = push_undefined(&mut out, fresh);
|
||||||
out.push(instr(OpCode::Undefined, ImmOperand::None, vec![undef], vec![]));
|
let args = args_with_self(&instr.args[..n - 2], undefined, callee);
|
||||||
let mut args = Vec::with_capacity(n);
|
out.push(RegInstr::new(OpCode::Call, instr.imm, instr.dsts, args));
|
||||||
args.extend_from_slice(&ri.args[..n - 2]);
|
|
||||||
args.push(undef); // self
|
|
||||||
args.push(func); // callee, last
|
|
||||||
out.push(instr(OpCode::Call, ri.imm, ri.dsts, args));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `get_field2` kept a receiver under the value it read. Receivers
|
// `get_field2` kept the object under a second value. The object is
|
||||||
// are explicit arguments now, so the kept copy is just the object,
|
// an explicit argument now, so that copy is just the object, and
|
||||||
// and this is an ordinary read.
|
// this becomes an ordinary read.
|
||||||
OpCode::GetField2 if ri.dsts.len() == 2 && ri.args.len() == 1 => {
|
OpCode::GetField2 if instr.dsts.len() == 2 && instr.args.len() == 1 => {
|
||||||
kept.push((ri.dsts[0], ri.args[0]));
|
receiver_alias.push((instr.dsts[0], instr.args[0]));
|
||||||
out.push(instr(OpCode::GetField, ri.imm, vec![ri.dsts[1]], vec![ri.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. Insert it
|
// A plain call has no receiver, so `this` is undefined.
|
||||||
// before the callee, which must stay last.
|
op if is_plain_call(op) && !instr.args.is_empty() => {
|
||||||
op if is_plain_call(op) && !ri.args.is_empty() => {
|
let n = instr.args.len();
|
||||||
let n = ri.args.len();
|
let callee = instr.args[n - 1];
|
||||||
let callee = ri.args[n - 1];
|
let undefined = push_undefined(&mut out, fresh);
|
||||||
let undef = fresh(next);
|
let args = args_with_self(&instr.args[..n - 1], undefined, callee);
|
||||||
out.push(instr(OpCode::Undefined, ImmOperand::None, vec![undef], vec![]));
|
out.push(RegInstr::new(as_plain_call(op), instr.imm, instr.dsts, args));
|
||||||
let mut args = Vec::with_capacity(n + 1);
|
|
||||||
args.extend_from_slice(&ri.args[..n - 1]);
|
|
||||||
args.push(undef); // self
|
|
||||||
args.push(callee); // callee, last
|
|
||||||
out.push(instr(tail_or_plain(ri.op), ri.imm, ri.dsts, args));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => out.push(ri),
|
_ => out.push(instr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Point the echoed receivers at the object itself.
|
// Point the echoed receivers at the object itself.
|
||||||
if !kept.is_empty() {
|
if !receiver_alias.is_empty() {
|
||||||
for ri in &mut out {
|
for instr in &mut out {
|
||||||
for v in ri.args.iter_mut().chain(ri.dsts.iter_mut()) {
|
for v in instr.args.iter_mut().chain(instr.dsts.iter_mut()) {
|
||||||
if let Some((_, real)) = kept.iter().find(|(k, _)| k == v) {
|
if let Some((_, object)) = receiver_alias.iter().find(|(echo, _)| echo == v) {
|
||||||
*v = *real;
|
*v = *object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,42 +256,19 @@ fn lower_calls(f: &mut RegFn, next: &mut usize) {
|
|||||||
f.code = out;
|
f.code = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
/// 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 is_plain_call(op: OpCode) -> bool {
|
fn args_with_self(args: &[usize], self_val: usize, callee: usize) -> Vec<usize> {
|
||||||
use OpCode::*;
|
let mut all = Vec::with_capacity(args.len() + 2);
|
||||||
matches!(op, Call | Call0 | Call1 | Call2 | Call3 | TailCall)
|
all.extend_from_slice(args);
|
||||||
|
all.push(self_val);
|
||||||
|
all.push(callee);
|
||||||
|
all
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tail_or_plain(op: OpCode) -> OpCode {
|
/// Emit `undefined` and return the value holding it.
|
||||||
use OpCode::*;
|
fn push_undefined(out: &mut Vec<RegInstr>, fresh: &mut Fresh) -> usize {
|
||||||
if matches!(op, TailCall | TailCallMethod) {
|
let v = fresh.new_value();
|
||||||
TailCall
|
out.push(RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![v], vec![]));
|
||||||
} else {
|
|
||||||
Call
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instr(op: OpCode, imm: ImmOperand, dsts: Vec<usize>, args: Vec<usize>) -> RegInstr {
|
|
||||||
RegInstr {
|
|
||||||
dsts,
|
|
||||||
op,
|
|
||||||
imm,
|
|
||||||
args,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fresh(next: &mut usize) -> usize {
|
|
||||||
let v = *next;
|
|
||||||
*next += 1;
|
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_val(f: &RegFn) -> usize {
|
|
||||||
f.code
|
|
||||||
.iter()
|
|
||||||
.flat_map(|i| i.dsts.iter().chain(i.args.iter()))
|
|
||||||
.copied()
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|||||||
+66
-1
@@ -4,7 +4,7 @@ use std::fmt::Debug;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
flatten::{Capture, FlatFn},
|
flatten::{Capture, FlatFn},
|
||||||
parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
|
parse::{AtomRef, ImmOperand, Instr, JsValue, opcodes::OpCode},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct RegInstr {
|
pub struct RegInstr {
|
||||||
@@ -104,3 +104,68 @@ pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
|||||||
})
|
})
|
||||||
.collect()
|
.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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user