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 run is handed anything it does not make for itself. Everything else moves
into the function unchanged. There are two kinds of parameter:

    FRAME. GetArg, GetLoc, GetVarRef and their numbered forms are frame
        reads. Each becomes a parameter, and the load stays at the call site.
    RUN. A nested build inside this run. That run is lifted first, and its
        call result is passed in. See NESTED BUILDS.

Everything else -- constants, arithmetic, field reads off a parameter -- is
made by the run and moves in.

Parameters are taken in first-use order. A frame slot appears once however
many times it is loaded, so a mutation's repeated loads of its object fold
into the one parameter, and the object is always parameter 0.

So a 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)
====================================================================

    A run cannot be matched opcode by opcode, the way a class build can,
    because <value> is any expression at all. FOLLOW THE OBJECT instead:

        start a run at an Object (creation), or at a load of a slot that a
          static-key write then writes to (mutation).
        skip forward to the next instruction that USES the object.
        carry on only while that use is a static-key write to it.
        everything passed over on the way is the value computation, and it
          joins the run.

    A creation and a mutation differ only in how the object carries from one
    write to the next:

        DefineField hands the object back as a NEW value, so a creation run
          is one value chain.
        PutField hands nothing back, so a mutation re-loads the object before
          every write.

    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.
        a jump, or an instruction some jump lands on. A run is ONE straight
            line of code, so both sides of a branch are cut off. Without this
            an `if` in the middle of a sequence of writes joins the two sides
            into one run.

    "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.

    NESTED BUILDS: a run's own instructions are walked AGAIN, to find the
    object builds inside it. Every Object in the program ends up in exactly
    one lifted function, so one function describes one object shape.

        { x: { y: 1 }, z: { w: 2 } }   lifts to THREE functions, not one.

    An inner run is held INSIDE its parent's parameter list, as the Run
    parameter it becomes. So lifting is depth-first: lift a run's Run
    parameters, then the run itself, which needs their results.

    The inner walk looks for BUILDS ONLY. A mutation writes its object over
    and over, so a walk that looked for anything would match the run's own
    second write as a run of its own:

        a.x = 1 ; a.y = 2      one mutation run, NOT a run with one nested

    ORDER IN THE PASS: demutate runs BEFORE declosure. declosure builds its
    closure objects out of Object and DefineField Atom(int(k)), which a run
    cannot tell apart from a user object. Run it after, and every closure gets
    lifted as if it were an object literal.


====================================================================
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
====================================================================

One rule covers them all: a frame read (GetArg, GetLoc, GetVarRef) becomes a
parameter and its load stays at the call site; everything else moves in.

    a.x = 1             constant
        Push1 is produced inside the run, so it moves in.

    { x: p }            argument
        GetArg is a frame read, so p becomes a parameter.

    a.x = p + 1         computed
        p is a parameter; the Add is produced inside, so it moves in.

    a.x = b.y           field read
        b is a parameter; the GetField is produced inside, so it moves in.

    { x: { y: 1 } }     nested object
        The inner Object is a run of its OWN. It is lifted first, and the
        outer run takes its result as a parameter. See NESTED BUILDS.


====================================================================
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.
