244 lines
8.3 KiB
Plaintext
244 lines
8.3 KiB
Plaintext
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.
|