Files
jsrt/compiler/src/transform/closure.rs
T
2026-10-23 12:07:23 +00:00

219 lines
7.6 KiB
Rust

//! This module lowers closures by making implicit environment captures explicit
//!
//! Replace:
//! ```js
//! let x = 0;
//! let y = true;
//!
//! function fn() {
//! f(x)
//! f(y)
//! }
//! ```
//!
//! With:
//! ```js
//! let x = 0;
//! let y = true;
//!
//! function fn() {
//! f(fn.<var>.x);
//! f(fn.<var>.y);
//! }
//!
//! fn.<var> = {};
//! fn.<var>.x = x; // by-ref
//! fn.<var>.y = y; // by-ref
//! ```
//!
//! i.e. captured bindings are turned into fields on the function. A closure is an object, so
//! it can have fields of its own. The captures are kept apart from them in one field, `<var>`,
//! so that a write to a field of the closure cannot clobber a capture
//!
//! # Locations
//!
//! Every binding (a local or an arg) is a location. A capture must share the location of the
//! binding it captures: a write by the owner is seen by the closure, and a write by the
//! closure is seen by the owner. Normally a field holds a copy of a value, so a special
//! `define_field_ref` instr is introduced by `c_parse.py`:
//!
//! define_field_ref x [obj, value] -> [obj]
//!
//! It defines `obj.x` as the location that `value` was read from, not as a copy of `value`.
//! So the instr right before it is always the read of a location. This module only emits:
//! - get_loc k a local of the owner
//! - get_arg k an arg of the owner
//! - get_field y a capture of the owner, to give to its own child. The owner's `y` is
//! the location of a binding further out, so the child shares that
//!
//! Where a location lives (the stack or the heap) is not decided here. `escape.rs` decides
//! that from these instrs. This module only states which fields share which locations
//!
//! # `close_loc`: a new binding for each loop iteration
//!
//! In JS, each iteration of a loop gets its own copy of the bindings that are declared in
//! the loop:
//! ```js
//! for (let i = 0; i < 3; i++) fns.push(() => i);
//! // the closures give 0, 1, 2, not 3, 3, 3
//! ```
//!
//! QuickJS uses one local for `i`, and emits `close_loc i` at each way out of the loop body.
//! After this module, `close_loc k` means: the binding of local `k` moves to a new location,
//! with the value of the old location. The closures that were made before the `close_loc`
//! keep the old location. So in the example, `i++` writes the new location of `i`, and the
//! closure of each iteration keeps the `i` of its iteration
//!
//! The owner's code does not change for this: `get_loc k` and `put_loc k` always use the
//! current location of `k`. This module keeps `close_loc` as it is. Then:
//! - infer can ignore it. The old and new locations of a binding have the same address
//! (`Addr::Shared`), so their types are joined
//! - `escape.rs` must give each iteration a new location if a closure can see the old one
//! after the `close_loc`
//!
//! # The replacements
//!
//! In the owner, for `fclosure f`:
//! fclosure f [fn]
//! object [fn, vars]
//! <read of the location> [fn, vars, value] for each capture `x` of `f`
//! define_field_ref x [fn, vars]
//! define_field <var> [fn]
//!
//! A closure that captures nothing gets no `<var>`: only the `fclosure` stays
//!
//! In the closure, with `env` as the closure itself (arg `ENV_ARG`):
//! get_var_ref x get_arg env; get_field <var>; get_field x
//! put_var_ref x get_arg env; get_field <var>; swap; put_field x
//! set_var_ref x dup; get_arg env; get_field <var>; swap; put_field x
//!
//! close_var_ref x get_arg env; get_field <var>; close_field x
//!
//! The `_check` forms are the same: the TDZ is not checked. `close_var_ref` is `close_loc` for
//! a capture: transform/async.rs makes it, when a binding that it moves out of a fn's frame
//! needs a new location for each loop iteration
//!
//! A module binding is a local of the synthetic entry fn, which makes `<eval>` once (see
//! flatten.rs), so there is one location for each module binding
//!
//! In the emitted C, every underlying "plain" function is called as:
//! `fn(env, this, arg_0, arg_1, ... arg_n)`
//! where `env` is the closure itself
use super::{ENV_ARG, FIRST_ARG};
use crate::libqjs::parse::{
AtomRef, ClosureKind, ClosureVar, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode,
};
/// `<var>`: the field of a closure that holds its captures
pub const VAR_FIELD: AtomRef = AtomRef::Predefined(86);
/// Replace a read or write of a capture, or the making of a closure
///
/// `own` is the captures of the fn that holds `instr`
pub fn try_replace_closure(
instr: &Instr,
own: &[ClosureVar],
captures: &[Vec<ClosureVar>],
cpool: &[JsValue],
out: &mut Vec<Instr>,
) -> bool {
use OpCode::*;
let mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, Imm::None);
}
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
if writes {
push(Swap, Imm::None);
push(PutField, Imm::Atom(own[i].name));
} else {
push(GetField, Imm::Atom(own[i].name));
}
return true;
}
// A new location for the binding that a capture shares (see `close_loc` above)
if let (CloseVarRef, Imm::VarRef(i)) = (instr.op, instr.imm) {
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
push(CloseField, Imm::Atom(own[i as usize].name));
return true;
}
if !matches!(instr.op, Fclosure | Fclosure8) {
return false;
}
let JsValue::FnByIdx(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
// A closure with no captures needs no `<var>`
if captures[child].is_empty() {
return true;
}
push(Object, Imm::None);
for c in &captures[child] {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, Imm::Loc(c.var_idx as u16)),
ClosureKind::Arg => push(GetArg, Imm::Arg(FIRST_ARG + c.var_idx as u16)),
ClosureKind::Ref => {
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
push(GetField, Imm::Atom(own[c.var_idx].name));
}
_ => unimplemented!("a capture of kind {:?}", c.flags.closure_kind),
}
push(DefineFieldRef, Imm::Atom(c.name));
}
push(DefineField, Imm::Atom(VAR_FIELD));
true
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes, and whether a
/// write also leaves the value on the stack
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
Imm::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => {
Some((instr.op as usize - GetVarRef0 as usize, false, false))
}
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => {
Some((instr.op as usize - PutVarRef0 as usize, true, false))
}
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => {
Some((instr.op as usize - SetVarRef0 as usize, true, true))
}
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}