//! 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..x); //! f(fn..y); //! } //! //! fn. = {}; //! fn..x = x; // by-ref //! fn..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, ``, //! 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] //! [fn, vars, value] for each capture `x` of `f` //! define_field_ref x [fn, vars] //! define_field [fn] //! //! A closure that captures nothing gets no ``: 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 ; get_field x //! put_var_ref x get_arg env; get_field ; swap; put_field x //! set_var_ref x dup; get_arg env; get_field ; swap; put_field x //! //! close_var_ref x get_arg env; get_field ; 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 `` 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, }; /// ``: 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], cpool: &[JsValue], out: &mut Vec, ) -> 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 `` 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, } }