declosure: closures become plain objects Plain text, same dump format as class_form.txt / proto_form.txt. A closure becomes an object. Field 0 holds the function. Fields 1.. hold the captured variables, in capture order. The object is also the environment, so there is no separate env value. Field names are plain integers, so a field operand reads Atom(int(k)): field 0 the function field k+1 capture number k Running example: function outer(a) { function inner() { return a; } // inner captures a return inner(); } FnById(1) = outer arg_count 1 (a is arg 0) FnById(2) = inner outer's cpool Const(0) captures: [ Capture { name: "a", from: ParentArg(0) } ] Three steps, one property each. ==================================================================== STEP 1 every function takes the env as its LAST parameter ==================================================================== Runs on every function. Capture reads/writes become field access on the env. arg_count grows by one; existing args keep their numbers. The env is loaded once at entry, but only if the function touches a capture. Then the capture list is cleared. DETECT, abstractly (any function; each capture access, one at a time): capture read get_var_ref | get_var_ref_check | get_var_ref0..3 capture write put_var_ref | put_var_ref_check | put_var_ref_check_init | put_var_ref0..3 inner, which in the example is: captures: [ Capture { name: "a", from: ParentArg(0) } ], arg_count: 0, [0] = GetVarRefCheck VarRef(0) [], [] = Return None [0], REPLACE with: captures: [], arg_count: 1, [1] = GetArg Arg(0) [], // env, the new last parameter [0] = GetField Atom(int(1)) [1], // capture 0 -> field 1 [] = Return None [0], INSTRUCTION BY INSTRUCTION: load env once at entry -> GetArg Arg() [env] GetVarRef / GetVarRefCheck k -> GetField Atom(int(k+1)) [env] GetVarRef0..3 -> GetField Atom(int(1..4)) [env] PutVarRef / PutVarRefCheck k -> PutField Atom(int(k+1)) [value, env] PutVarRef0..3 -> PutField Atom(int(1..4)) [value, env] A function that touches no capture still gains the env parameter, but no GetArg is emitted for it. The uniform parameter is what lets step 3 append the env at every call site. ==================================================================== STEP 2 every fclosure builds the object, in the PARENT ==================================================================== Each fclosure becomes: load the function as a plain constant, make the object, store the function in field 0, then one field per capture loaded from the parent frame. The DefineField results chain; the LAST store must land in the value id the rest of the code already uses for the closure. DETECT, abstractly: fclosure #child (#child resolves to a FnById in cpool) outer, which in the example is: [3] = Fclosure8 Const(0) [], // Const(0) = FnById(2) = inner REPLACE with: [10] = PushConst8 Const(0) [], // the function, as a plain constant [11] = Object None [], // the closure object [12] = DefineField Atom(int(0)) [10, 11], // field 0 = function [13] = GetArg Arg(0) [], // capture 0 source: outer's arg 0 (a) [3] = DefineField Atom(int(1)) [13, 12], // field 1 = capture a; last store -> [3] A closure with NO captures is just field 0, and that store lands in [3]: [3] = Fclosure8 Const(0) [], -> [10] = PushConst8 Const(0) [], [11] = Object None [], [3] = DefineField Atom(int(0)) [10, 11], CAPTURE SOURCE -> how it loads (from the parent frame): ParentLocal(s) -> GetLoc Loc(s) [value] ParentArg(s) -> GetArg Arg(s) [value] anything else -> Undefined None [value] // gap, see below DefineField args are [value, obj], and it returns the object, so the obj of each store is the result of the store before it. ==================================================================== STEP 3 every plain call goes through the object, in the CALLER ==================================================================== A call names a closure object now. Read the function out of field 0, call it, and pass the object itself as the trailing env argument. The callee is the last argument, so it is args.last(). DETECT, abstractly: plain call call | call0..3 (callee = last arg; not call_method, call_constructor, or apply) outer, which in the example is: [5] = Call Npop(0) [4], // [4] is the closure object REPLACE with: [14] = GetField Atom(int(0)) [4], // the function, out of field 0 [5] = Call Npop(1) [4, 14], // call it; env (the object) appended Npop grows by one, for the appended env. Args become: [ env, , func ] env = the object value (the old callee) func = field 0 of the object (the new callee, still last) ==================================================================== ELIMINATED (opcodes this pass removes from live code) ==================================================================== Step 1 removes: GetVarRef, GetVarRefCheck, GetVarRef0..3 replaced by GetField on env PutVarRef, PutVarRefCheck, PutVarRefCheckInit, PutVarRef0..3 replaced by PutField on env Step 2 removes: Fclosure, Fclosure8 replaced by PushConst8 + Object + DefineField(s) After the pass, these never appear again anywhere: Fclosure, Fclosure8 every GetVarRef* and PutVarRef* ==================================================================== NOT TOUCHED (passed through unchanged) ==================================================================== CallMethod carries a receiver; the class passes own it CallConstructor carries a receiver; the class passes own it Apply carries a receiver GetVarRef* / PutVarRef* that name a source this pass does not model (see gaps) still become field access, but load Undefined ==================================================================== GAPS (known, left for later) ==================================================================== A captured var the parent later writes: the write makes a new value, so the closure never sees it. A capture aliases the value at capture time. Capture source is ParentCapture / module binding / global: step 2 has no parent frame slot to read, so it loads Undefined. Foreign callee (e.g. Math.sqrt): step 3 reads field 0, but a host builtin has no field 0. Entry points (module root, host-invoked functions): step 1 gives them an env parameter, but no caller of ours supplies one.