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 ; ( ; DefineField @F )+ mutation run ( ; ; 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: // 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 "", 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 "", 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.