New direction
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
declass: class form -> prototype form
|
||||
|
||||
Plain text. Read it next to class_form.txt (the input) and proto_form.txt (the target).
|
||||
Every instruction below is copied in the same dump format those files use.
|
||||
|
||||
The class in function "test" has this cpool:
|
||||
|
||||
cpool: [ FnById(2), FnById(3), FnById(4) ]
|
||||
|
||||
Const(0) = FnById(2) = the m1 method
|
||||
Const(1) = FnById(3) = the constructor
|
||||
Const(2) = FnById(4) = the fields initializer
|
||||
|
||||
|
||||
====================================================================
|
||||
STEP 1 in function "test": rewrite how the class is built
|
||||
====================================================================
|
||||
|
||||
DETECT, abstractly:
|
||||
|
||||
push_const8 #ctor ; define_class @Name
|
||||
( fclosure8 #method ; define_method @meth )*
|
||||
( fclosure8 #fields ; set_home_object ; put_loc ; drop )?
|
||||
|
||||
which in the example is:
|
||||
|
||||
[5] = PushConst8 Const(1) [],
|
||||
[6, 7] = DefineClass AtomU8 { atom: file_atom[2], arg: 0 } [5, 4],
|
||||
[8] = Fclosure8 Const(0) [],
|
||||
[9] = DefineMethod AtomU8 { atom: file_atom[5], arg: 0 } [8, 7],
|
||||
[10] = Fclosure8 Const(2) [],
|
||||
[11, 12] = SetHomeObject None [10, 9],
|
||||
[] = PutLoc1 None [12],
|
||||
[] = Drop None [11],
|
||||
|
||||
REPLACE it with:
|
||||
|
||||
[6] = Fclosure8 Const(1) [],
|
||||
[8] = Fclosure8 Const(0) [],
|
||||
[18] = GetField Atom(atom("prototype")) [6],
|
||||
[] = PutField Atom(file_atom[5]) [8, 18],
|
||||
|
||||
INSTRUCTION BY INSTRUCTION:
|
||||
|
||||
PushConst8 Const(1) + DefineClass [5,4] -> Fclosure8 Const(1), keep dst [6]
|
||||
Fclosure8 Const(0) -> unchanged (the method closure)
|
||||
DefineMethod atom=A [8,7] -> GetField "prototype" [6]
|
||||
then PutField atom=A [8,18]
|
||||
Fclosure8 Const(2) -> deleted
|
||||
SetHomeObject -> deleted
|
||||
PutLoc1 [12] -> deleted
|
||||
Drop [11] -> deleted
|
||||
|
||||
[18] is any fresh value id.
|
||||
Repeat the DefineMethod line for each method in the class.
|
||||
|
||||
REMEMBER for step 2 and 3:
|
||||
|
||||
constructor function = Const(1) = FnById(3)
|
||||
fields function = Const(2) = FnById(4)
|
||||
class name = file_atom[2]
|
||||
|
||||
|
||||
====================================================================
|
||||
STEP 2 splice the field initialiser into the constructor FnById(3)
|
||||
====================================================================
|
||||
|
||||
The constructor does not set its own fields. quickjs hoisted them into a
|
||||
separate initialiser function (FnById(4)) that the constructor reaches through
|
||||
the captured <class_fields_init> and runs with a call. Splice that function's
|
||||
field writes straight into the constructor, in place of the call, so the
|
||||
constructor initialises its fields inline and the separate initialiser is left
|
||||
unused.
|
||||
|
||||
DETECT the call block, abstractly (from the get_var_ref_check of
|
||||
<class_fields_init> through the next call_method):
|
||||
|
||||
get_var_ref_check <class_fields_init> ; dup ; if_false
|
||||
; get_loc ; swap ; call_method
|
||||
|
||||
which in the example is:
|
||||
|
||||
[2] = GetVarRefCheck VarRef(0) [],
|
||||
[3, 4] = Dup None [2],
|
||||
[] = IfFalse8 Label { target: 14 } [4],
|
||||
[5] = GetLoc0 None [],
|
||||
[6, 7] = Swap None [5, 3],
|
||||
[8] = CallMethod Npop(0) [7, 6],
|
||||
|
||||
REPLACE the call block by splicing in the field writes from the initialiser
|
||||
FnById(4):
|
||||
|
||||
[9] = GetLoc0 None [],
|
||||
[10] = Push1 None [],
|
||||
[11] = DefineField Atom(file_atom[4]) [10, 9],
|
||||
|
||||
The initialiser FnById(4) in full looks like this:
|
||||
|
||||
[2] = SpecialObject U8(4) [],
|
||||
[] = PutLoc1 None [2],
|
||||
[3] = PushThis None [],
|
||||
[] = PutLoc0 None [3],
|
||||
[] = Goto8 Label { target: 7 } [],
|
||||
[4] = GetLoc0 None [],
|
||||
[5] = Push1 None [],
|
||||
[6] = DefineField Atom(file_atom[4]) [5, 4],
|
||||
[] = ReturnUndef None [],
|
||||
|
||||
HOW THE SPLICE IS BUILT:
|
||||
|
||||
Take the initialiser FnById(4). Drop these opcodes:
|
||||
SpecialObject, PushThis, PutLoc0, PutLoc1, PutLoc2, PutLoc3,
|
||||
Goto8, Goto, ReturnUndef
|
||||
What is left is the field writes; these are what gets spliced in.
|
||||
Renumber their value ids to fresh ids in the constructor: 4,5,6 -> 9,10,11.
|
||||
The constructor's own PushThis / PutLoc0 / CheckCtor prologue and its
|
||||
final ReturnUndef stay as they were, wrapping the spliced-in writes.
|
||||
|
||||
|
||||
====================================================================
|
||||
STEP 3 in the constructor FnById(3): fix its header
|
||||
====================================================================
|
||||
|
||||
DETECT, abstractly:
|
||||
|
||||
a constructor (name "<null>") that captures <class_fields_init>
|
||||
|
||||
which in the example is:
|
||||
|
||||
name: "<null>",
|
||||
captures: [ Capture { name: "<class_fields_init>", ... } ],
|
||||
|
||||
REPLACE with:
|
||||
|
||||
name: "Counter",
|
||||
captures: [],
|
||||
|
||||
name becomes the class name from step 1.
|
||||
Remove the <class_fields_init> capture.
|
||||
If other captures remain after it, subtract 1 from every VarRef(n)
|
||||
whose n is greater than the removed capture index.
|
||||
|
||||
|
||||
====================================================================
|
||||
ELIMINATED (opcodes this pass removes from live code)
|
||||
====================================================================
|
||||
|
||||
Step 1 removes these from the class build:
|
||||
|
||||
DefineClass replaced by Fclosure8
|
||||
DefineMethod replaced by GetField + PutField
|
||||
SetHomeObject deleted
|
||||
PushConst8 the class-template one, folded into Fclosure8
|
||||
Fclosure8 the fields-init one, deleted
|
||||
PutLoc1 the SetHomeObject leftover store, deleted
|
||||
Drop the SetHomeObject leftover drop, deleted
|
||||
|
||||
Step 2 removes these from the constructor (the field-init call block):
|
||||
|
||||
GetVarRefCheck of the <class_fields_init> capture
|
||||
Dup
|
||||
IfFalse8
|
||||
GetLoc0 the one feeding the call
|
||||
Swap
|
||||
CallMethod
|
||||
|
||||
After the pass, these three class-only opcodes never appear again anywhere:
|
||||
|
||||
DefineClass
|
||||
DefineMethod
|
||||
SetHomeObject
|
||||
|
||||
The rest (PushConst8, Fclosure8, GetVarRefCheck, Dup, IfFalse8, GetLoc0,
|
||||
Swap, CallMethod, PutLoc1, Drop) still occur elsewhere for ordinary reasons.
|
||||
Only the specific class-pattern instances listed above are removed.
|
||||
|
||||
|
||||
====================================================================
|
||||
NOT TOUCHED (passed through unchanged)
|
||||
====================================================================
|
||||
|
||||
DefineClass AtomU8 { arg: 1 } derived class
|
||||
DefineMethod AtomU8 { arg: 1 } getter
|
||||
DefineMethod AtomU8 { arg: 2 } setter
|
||||
DefineMethodComputed ... computed method name
|
||||
DefineClassComputed ... computed class name
|
||||
|
||||
|
||||
====================================================================
|
||||
LEFT BEHIND (removed by a later pass)
|
||||
====================================================================
|
||||
|
||||
FnById(4), the fields function. Nothing references it anymore.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,210 @@
|
||||
demutate: object create + in-place mutation become function calls
|
||||
|
||||
Plain text, same dump format as class_form.txt / proto_form.txt.
|
||||
|
||||
Goal: every point that builds or mutates an object becomes a call to a function
|
||||
that RETURNS a fresh object. A mutation function copies the input's fields with
|
||||
a spread and re-applies only the writes it was given; it never names the rest.
|
||||
|
||||
|
||||
====================================================================
|
||||
THE KEY IDEA
|
||||
====================================================================
|
||||
|
||||
Find a maximal contiguous RUN of static-key field writes to one object -- the
|
||||
Object that creates it, the DefineField / PutField writes with an atom key, and
|
||||
the value computations they consume -- and lift the whole run into one function.
|
||||
|
||||
creation run Object ; ( <value> ; DefineField @F )+
|
||||
mutation run ( <object> ; <value> ; PutField @F )+ (same object)
|
||||
|
||||
The function returns the finished object. The call site replaces the run with:
|
||||
load the parameters, make the closure, call it, and put the result where the
|
||||
run's object went (the escaping value for a creation, a store-back for a
|
||||
mutation).
|
||||
|
||||
Creation and mutation are ONE code path. The only difference is whether the
|
||||
run's object is made inside it (creation -> start from an empty Object) or
|
||||
loaded from outside (mutation -> the object is a parameter, spread in first).
|
||||
Creation is just mutation of a freshly-made empty object; PutField and
|
||||
DefineField both become DefineField on the fresh copy the function owns.
|
||||
|
||||
|
||||
====================================================================
|
||||
PARAMETERS -- the rule that keeps this general
|
||||
====================================================================
|
||||
|
||||
A value the run USES but does not itself PRODUCE becomes a parameter, in
|
||||
first-use order. Everything produced inside the run (Object, constants,
|
||||
arithmetic that is wholly inside) moves into the function unchanged.
|
||||
|
||||
the object being mutated is used-but-not-produced -> it is a parameter.
|
||||
a constant field value (Push1) is produced inside -> it moves in.
|
||||
a field value from the enclosing frame (GetArg p) -> a parameter.
|
||||
|
||||
So a pure literal of constants lifts to a 0-parameter function; a mutation of
|
||||
`a` lifts to a 1-parameter function; `a.x = p + 1` lifts to a 2-parameter one.
|
||||
|
||||
|
||||
====================================================================
|
||||
FUNCTION BODY -- how the run is rebuilt
|
||||
====================================================================
|
||||
|
||||
The lifted function always builds ONE fresh object and returns it.
|
||||
|
||||
if the run has an incoming object (a parameter), start as a spread copy:
|
||||
|
||||
[0] = Object None [],
|
||||
[1] = GetArg0 None [], // the incoming object
|
||||
[2] = Null None [],
|
||||
[3, 4, 5] = CopyDataProperties U8(6) [2, 1, 0], // copy ALL its fields
|
||||
[] = Drop None [5],
|
||||
[] = Drop None [4],
|
||||
|
||||
otherwise (a creation), start empty:
|
||||
|
||||
[0] = Object None [],
|
||||
|
||||
then, for each field write in the run, in order:
|
||||
|
||||
<value> // moved in, or GetArgK for a parameter
|
||||
DefineField Atom(@F) [value, obj]
|
||||
|
||||
and finally:
|
||||
|
||||
[] = Return None [obj],
|
||||
|
||||
Each generated function is appended to the flat function list, gets the next fn
|
||||
id N, and FnById(N) is added to the caller's cpool at some slot K; the call site
|
||||
loads it with Fclosure8 Const(K).
|
||||
|
||||
|
||||
====================================================================
|
||||
DETECTION (one linear walk)
|
||||
====================================================================
|
||||
|
||||
start a run at an Object, or at the first static-key write to an object.
|
||||
extend it while the next instruction is one of:
|
||||
Object (a nested object is its own run)
|
||||
a value computation feeding a write in this run
|
||||
DefineField, imm == Atom(F) (same object)
|
||||
PutField, imm == Atom(F) (same object)
|
||||
a (re)load of the same object
|
||||
END the run at any of:
|
||||
the object is READ for anything but a field write -- passed to a call,
|
||||
used in an expression, branched on.
|
||||
a static-key write to a DIFFERENT object.
|
||||
|
||||
"Same object": a mutation re-loads the object before each write (each
|
||||
GetArg0 / GetLocCheck is a fresh value), so sameness is judged by the load
|
||||
SOURCE -- the same Arg or Loc -- not the value id. quickjs keeps writes in
|
||||
source order and does not group by object, so interleaved writes to two
|
||||
objects (a.x; b.y; a.z) split into three runs, not two.
|
||||
|
||||
Ignore computed-key writes (DefineArrayEl / PutArrayEl) -- see extensions.
|
||||
|
||||
|
||||
====================================================================
|
||||
WORKED EXAMPLE (test), real instructions
|
||||
====================================================================
|
||||
|
||||
--- the creation run ---
|
||||
|
||||
DETECT:
|
||||
|
||||
[1] = Object None [],
|
||||
[2] = Push1 None [],
|
||||
[3] = DefineField Atom(file_atom[7]) [2, 1],
|
||||
[4] = Push0 None [],
|
||||
[5] = DefineField Atom(file_atom[8]) [4, 3],
|
||||
[] = PutLoc0 None [5],
|
||||
|
||||
nothing is used from outside, so the function takes no parameters.
|
||||
|
||||
REPLACE with (the run's result stays value [5], so PutLoc0 is untouched):
|
||||
|
||||
[20] = Fclosure8 Const(Kc) [],
|
||||
[5] = Call0 None [20],
|
||||
[] = PutLoc0 None [5],
|
||||
|
||||
new FnById(Nc), name "<create>", arg_count 0:
|
||||
|
||||
[0] = Object None [],
|
||||
[1] = Push1 None [],
|
||||
[2] = DefineField Atom(file_atom[7]) [1, 0],
|
||||
[3] = Push0 None [],
|
||||
[4] = DefineField Atom(file_atom[8]) [3, 2],
|
||||
[] = Return None [4],
|
||||
|
||||
--- the mutation run (a.field1 = 2 ; a.other = 3) ---
|
||||
|
||||
DETECT:
|
||||
|
||||
[6] = GetLocCheck Loc(0) [],
|
||||
[7] = Push2 None [],
|
||||
[] = PutField Atom(file_atom[7]) [7, 6],
|
||||
[8] = GetLocCheck Loc(0) [],
|
||||
[9] = Push3 None [],
|
||||
[] = PutField Atom(file_atom[9]) [9, 8],
|
||||
|
||||
the object (Loc 0) is used-but-not-produced -> the one parameter; 2 and 3 are
|
||||
produced inside -> they move in.
|
||||
|
||||
REPLACE with (store the returned object back):
|
||||
|
||||
[21] = Fclosure8 Const(Km) [],
|
||||
[22] = GetLocCheck Loc(0) [], // the incoming object
|
||||
[23] = Call1 None [22, 21], // a = mutate(a); callee is last
|
||||
[] = PutLoc0 None [23],
|
||||
|
||||
new FnById(Nm), name "<mutate>", arg_count 1:
|
||||
|
||||
[0] = Object None [],
|
||||
[1] = GetArg0 None [],
|
||||
[2] = Null None [],
|
||||
[3, 4, 5] = CopyDataProperties U8(6) [2, 1, 0],
|
||||
[] = Drop None [5],
|
||||
[] = Drop None [4],
|
||||
[6] = Push2 None [],
|
||||
[7] = DefineField Atom(file_atom[7]) [6, 3],
|
||||
[8] = Push3 None [],
|
||||
[9] = DefineField Atom(file_atom[9]) [8, 7],
|
||||
[] = Return None [9],
|
||||
|
||||
|
||||
====================================================================
|
||||
FIELD VALUE FORMS -- all handled the same way
|
||||
====================================================================
|
||||
|
||||
constant a.x = 1 Push1 moves into the function
|
||||
argument { x: p } GetArg becomes a parameter
|
||||
computed a.x = p + 1 p is a parameter; the Add moves in
|
||||
field read a.x = b.y b is a parameter; the GetField moves in
|
||||
nested object { x: { y: 1 } } the inner Object is its own run, lifted
|
||||
first; its result is a value the outer
|
||||
run uses -> a parameter of the outer
|
||||
|
||||
|
||||
====================================================================
|
||||
NEEDS A SEPARATE HELPER (extensions)
|
||||
====================================================================
|
||||
|
||||
spread in a literal { ...b, x: 1 } CopyDataProperties of a source
|
||||
object, not just the input copy
|
||||
computed key { [k]: 1 }, a[k] = 1 DefineArrayEl / PutArrayEl,
|
||||
a[0] = 1 keyed by a value, not an atom
|
||||
|
||||
|
||||
====================================================================
|
||||
GAPS (left for later)
|
||||
====================================================================
|
||||
|
||||
A mutation run's object must live in a local, so the returned object can be
|
||||
written back and the next read sees it. A bare param or a sub-expression
|
||||
has nowhere to write back.
|
||||
Aliasing / identity: the run returns a NEW object, so two names for the same
|
||||
object diverge, and === changes.
|
||||
Ordering: a run must stay in program order; it is cut wherever the object is
|
||||
read for anything but a field write.
|
||||
Methods ({ m() {} } -> DefineMethod), getters/setters, delete,
|
||||
defineProperty: not plain field writes.
|
||||
Reference in New Issue
Block a user