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) [],
    [18] = GetField Atom(atom("prototype")) [6],
    [8] = Fclosure8 Const(0) [],
    [] = PutField Atom(file_atom[5]) [8, 18],
    [] = Nop None [],
    [] = Nop None [],
    [] = Nop None [],
    [] = Nop None [],

INSTRUCTION BY INSTRUCTION:

    PushConst8 Const(1) ; DefineClass [5,4]
        -> Fclosure8 Const(1), which keeps the DefineClass dst [6].

    GetField "prototype" [6]
        -> emitted once for the whole class, into a fresh value id [18].
           It is NOT emitted per method: the prototype cannot change
           between the PutFields below. A class with no methods does not
           emit it at all.

    Fclosure8 Const(0)
        -> unchanged. It is the method closure.

    DefineMethod atom=A [8,7]
        -> PutField atom=A [8, 18], reading the [18] from above.
           Repeat the Fclosure8/PutField pair for each method.

    Fclosure8 Const(2) ; SetHomeObject ; PutLoc1 [12] ; Drop [11]
        -> deleted. This is the field-initialiser tail.

    Nop
        -> padding, described below.

LENGTH, and why the rewrite is written in place:

    matched  = 2 + 2n instructions, plus 4 more if the field-initialiser
               tail is there (n = the number of methods).
    emitted  = 2 + 2n instructions, or 1 when n is 0.

    So the emission is never longer than what it replaces. It is written
    straight over the matched range and the slack is padded with Nop,
    which pops and pushes nothing. Every instruction outside a class
    build keeps its position, so no other range has to be adjusted.

    In the example: 8 matched, 4 emitted, 4 Nop.

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.


====================================================================
UNSUPPORTED (the pass panics)
====================================================================

    DefineClass AtomU8 { arg: 1 }        derived class (extends)

    Prototype form would have to flatten the parent's prototype into the
    child's and lower `super`. Neither is implemented, so declass panics
    rather than emit a class that has silently lost its parent.


====================================================================
NOT TOUCHED (passed through unchanged)
====================================================================

    DefineMethodComputed ...             computed method name
    DefineClassComputed ...              computed class name

    Known gap: a getter or setter (DefineMethod with a non-zero kind) is
    NOT yet passed through. It is wrongly lowered as an ordinary method,
    to a put_field, giving a data property instead of an accessor. It
    should instead make the whole class pass through untouched.


====================================================================
LEFT BEHIND (an empty function)
====================================================================

    FnById(4), the fields function. Step 1 deleted the fclosure8 that
    built it, and step 2 moved its writes into the constructor, so
    nothing references it anymore.

    Step 2 empties it:

        code: [],

    It is NOT removed from the function list. A function id is an index
    into that list, so removing one would renumber every function after
    it and break every FnById that points past it.
