Files
jsrt/compiler/src/declass/step_2.rs
T
2026-09-07 13:13:03 +00:00

127 lines
4.7 KiB
Rust

//! Step 2 of declass: splice the field initialiser into the constructor.
//!
//! A class constructor does not set its own fields; quickjs hoisted them into a
//! separate initialiser function that the constructor reaches through the
//! captured <class_fields_init> and runs with a call (declass_algo.txt, step 2):
//!
//! get_var_ref_check <class_fields_init> ; dup ; if_false
//! ; get_loc ; swap ; call_method
//!
//! This step takes the initialiser's field writes -- its body minus the
//! this/home-object prologue and the trailing return -- and splices them into
//! the constructor in place of that call, renumbered onto fresh value ids. The
//! initialiser is left with an empty body. It cannot be dropped from the
//! function list, because a function id is an index into that list.
//!
//! Deficiency, left for step 3: a class with NO instance fields compiles
//! `<class_fields_init>` to `Undefined` rather than a closure, so there is no
//! initialiser to splice and `field_init_links` records no link. Its
//! constructor keeps the call block above, which the `if_false` skips at run
//! time -- a harmless no-op. Step 3 must strip that block when it drops the
//! `<class_fields_init>` capture, or the `get_var_ref_check` would dangle.
use super::step_1::ClassSequence;
use crate::{
parse::{ImmOperand, opcodes::OpCode},
regify::{RegFn, RegInstr},
};
/// The name quickjs gives the hoisted field initialiser.
const FIELDS_INIT: &str = "<class_fields_init>";
pub fn splice_ctors(fns: &mut [RegFn], class_seqs: &[Vec<ClassSequence>]) {
// Only a class with instance fields has an initialiser to splice.
for seq in class_seqs.iter().flatten() {
if let Some(field_init_fn) = seq.field_init_fn {
splice_one(fns, seq.ctor_fn, field_init_fn);
}
}
}
fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) {
// Find the call block first. If the constructor does not have one there is
// nothing to splice into, and the initialiser has to keep working.
let Some((start, end)) = call_span(&fns[ctor]) else {
return;
};
// Take the field writes out of the initialiser (a separate function), then
// hand them to the constructor.
let writes = take_field_writes(&mut fns[fields]);
let ctor_fn = &mut fns[ctor];
let writes = renumber(writes, ctor_fn);
ctor_fn.code.splice(start..=end, writes);
}
/// The initialiser's field writes: its body minus the this/home-object prologue
/// and the trailing return. They reach `this` through get_loc0, which is `this`
/// in the constructor too, so they carry over unchanged.
///
/// This empties the initialiser, which is dead once its writes move into the
/// constructor. The function itself has to stay: a function id is an index into
/// the function list, so removing one would renumber every function after it.
fn take_field_writes(init: &mut RegFn) -> Vec<RegInstr> {
init.code.retain(|i| !is_prologue(i.op));
std::mem::take(&mut init.code)
}
/// Renumber the writes' value ids onto fresh ids in the constructor. Each write
/// only uses values it or an earlier write produced, so a flat map suffices.
fn renumber(mut writes: Vec<RegInstr>, ctor_fn: &mut RegFn) -> Vec<RegInstr> {
let hi = writes
.iter()
.flat_map(|i| i.dsts.iter().chain(&i.args))
.copied()
.max()
.unwrap_or(0);
let mut map = vec![usize::MAX; hi + 1];
for instr in &mut writes {
for arg in &mut instr.args {
*arg = map[*arg];
}
for dst in &mut instr.dsts {
let fresh = ctor_fn.fresh_val();
map[*dst] = fresh;
*dst = fresh;
}
}
writes
}
/// The call block `get_var_ref_check <class_fields_init> .. call_method`, as an
/// inclusive `start..=end` into the constructor's code.
fn call_span(ctor: &RegFn) -> Option<(usize, usize)> {
let start = ctor.code.iter().position(|i| reads_fields_init(i, ctor))?;
let end = ctor.code[start..]
.iter()
.position(|i| matches!(i.op, OpCode::CallMethod | OpCode::TailCallMethod))?;
Some((start, start + end))
}
/// True for `get_var_ref_check` on the `<class_fields_init>` capture.
fn reads_fields_init(i: &RegInstr, ctor: &RegFn) -> bool {
let ImmOperand::VarRef(k) = i.imm else {
return false;
};
i.op == OpCode::GetVarRefCheck && ctor.captures[k as usize].name == FIELDS_INIT
}
/// The initialiser prologue and terminator, dropped when splicing.
fn is_prologue(op: OpCode) -> bool {
matches!(
op,
OpCode::SpecialObject
| OpCode::PushThis
| OpCode::PutLoc0
| OpCode::PutLoc1
| OpCode::PutLoc2
| OpCode::PutLoc3
| OpCode::Goto8
| OpCode::Goto
| OpCode::ReturnUndef
)
}