127 lines
4.7 KiB
Rust
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
|
|
)
|
|
}
|