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(<new last 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 | tail_call   (callee = last arg; not
                                               call_method, call_constructor,
                                               or apply)

    A tail call stays a tail call. It pushes nothing and returns, so turning it
    into an ordinary call would leave a value behind and drop the return.

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, <original call args>, 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.
