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.
