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