Files
jsrt/docs/demutate_algo.txt
T
2026-09-03 20:20:58 +00:00

211 lines
8.3 KiB
Plaintext

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.