Files
jsrt/docs/declass_algo.txt
2026-09-07 11:44:42 +00:00

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.