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 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 through the next call_method): get_var_ref_check ; 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 "") that captures which in the example is: name: "", captures: [ Capture { name: "", ... } ], REPLACE with: name: "Counter", captures: [], name becomes the class name from step 1. Remove the 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 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.