New direction
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
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 (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, <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.
|
||||
Reference in New Issue
Block a user