js-confuser-vm 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +242 -89
  2. package/dist/compiler.js +583 -208
  3. package/dist/disassembler.js +58 -8
  4. package/dist/runtime.js +93 -74
  5. package/dist/template.js +81 -76
  6. package/dist/transforms/bytecode/concealConstants.js +2 -2
  7. package/dist/transforms/bytecode/controlFlowFlattening.js +143 -25
  8. package/dist/transforms/bytecode/dispatcher.js +3 -3
  9. package/dist/transforms/bytecode/resolveRegisters.js +19 -4
  10. package/dist/transforms/bytecode/selfModifying.js +88 -21
  11. package/dist/transforms/bytecode/specializedOpcodes.js +6 -3
  12. package/dist/transforms/bytecode/stringConcealing.js +253 -75
  13. package/dist/utils/ast-utils.js +61 -0
  14. package/dist/utils/op-utils.js +1 -0
  15. package/package.json +7 -1
  16. package/.gitmodules +0 -4
  17. package/.prettierignore +0 -1
  18. package/CHANGELOG.md +0 -358
  19. package/babel-plugin-inline-runtime.cjs +0 -34
  20. package/babel.config.json +0 -23
  21. package/bench.ts +0 -146
  22. package/disassemble.ts +0 -12
  23. package/index.ts +0 -43
  24. package/jest-strip-types.js +0 -10
  25. package/jest.config.js +0 -64
  26. package/output.disassembled.js +0 -41
  27. package/src/build-runtime.ts +0 -113
  28. package/src/compiler.ts +0 -2703
  29. package/src/disassembler.ts +0 -329
  30. package/src/index.ts +0 -24
  31. package/src/minify.ts +0 -21
  32. package/src/options.ts +0 -24
  33. package/src/runtime.ts +0 -956
  34. package/src/template.ts +0 -265
  35. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -151
  36. package/src/transforms/bytecode/concealConstants.ts +0 -52
  37. package/src/transforms/bytecode/controlFlowFlattening.ts +0 -566
  38. package/src/transforms/bytecode/dispatcher.ts +0 -292
  39. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  40. package/src/transforms/bytecode/resolveConstants.ts +0 -126
  41. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  42. package/src/transforms/bytecode/resolveRegisters.ts +0 -226
  43. package/src/transforms/bytecode/selfModifying.ts +0 -121
  44. package/src/transforms/bytecode/specializedOpcodes.ts +0 -164
  45. package/src/transforms/bytecode/stringConcealing.ts +0 -130
  46. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  47. package/src/transforms/runtime/classObfuscation.ts +0 -59
  48. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  49. package/src/transforms/runtime/minify.ts +0 -1
  50. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  51. package/src/transforms/runtime/specializedOpcodes.ts +0 -161
  52. package/src/types.ts +0 -134
  53. package/src/utils/ast-utils.ts +0 -19
  54. package/src/utils/op-utils.ts +0 -46
  55. package/src/utils/pass-utils.ts +0 -126
  56. package/src/utils/profile-utils.ts +0 -3
  57. package/src/utils/random-utils.ts +0 -31
  58. package/tsconfig.json +0 -12
@@ -1,112 +0,0 @@
1
- // --- Label IR ---
2
- // During compilation, jump targets are symbolic labels instead of hard-coded
3
- // PC numbers. Two IR "pseudo operands" carry the label information:
4
- //
5
- // defineLabel operand : [null, {type:"defineLabel", label:"FN_ENTRY_1"}]
6
- // Marks a position in the bytecode array.
7
- // resolveLabels() strips these out entirely.
8
- //
9
- // label ref operand : [OP.JUMP, {type:"label", label:"FN_ENTRY_1"}]
10
- // Used as the operand of any jump instruction. resolveLabels() replaces
11
- // it with the integer PC that the corresponding defineLabel resolves to.
12
- //
13
- // The output bytecode is still a nested array of instructions.
14
- // Flattening (one u16 slot per op, one per operand) happens in the Serializer.
15
- // PC values computed here reflect the FLAT slot index so that jump targets,
16
- // startPc, and LOAD_INT label operands are all correct after flattening.
17
-
18
- import type { Instruction } from "../../types.ts";
19
- import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
20
-
21
- // Resolve symbolic labels to absolute flat-PC indices within a bytecode array.
22
- // defineLabel pseudo-instructions are stripped; label-ref operands become ints.
23
- // Each instruction [op, ...operands] occupies (1 + operands.length) flat slots,
24
- // so realPc advances by instr.length for every non-pseudo instruction.
25
- export function resolveLabels(
26
- bc: Instruction[],
27
- compiler: Compiler,
28
- ): {
29
- bytecode: Instruction[];
30
- } {
31
- // Pass 1 – walk the array and record each label's flat PC, counting
32
- // real instructions by their full flat width (1 op + N operands).
33
- const labelToPc = new Map<string, number>();
34
- let realPc = 0;
35
- for (const instr of bc) {
36
- const op = instr[0];
37
- const operand = instr[1];
38
-
39
- if (
40
- op === null &&
41
- operand !== null &&
42
- typeof operand === "object" &&
43
- (operand as any).type === "defineLabel"
44
- ) {
45
- labelToPc.set((operand as any).label, realPc);
46
- continue;
47
- }
48
-
49
- if (op === null) continue; // "null" opcodes are never emitted
50
-
51
- // Each instruction occupies 1 slot for the opcode + 1 per operand.
52
- // IMPORTANT: 'placeholder' operands are not counted
53
- realPc += instr.filter((x) => (x as any)?.placeholder !== true).length;
54
- }
55
-
56
- // Pass 2 – build the resolved instruction list.
57
- // Label refs may appear at any operand position, so scan all of them.
58
- const resolved: any[] = [];
59
- for (const instr of bc) {
60
- const [op, ...operands] = instr;
61
-
62
- // Replace label-ref and encodedLabel operands with resolved flat PCs.
63
- // encodedLabel applies an encoding to the PC before emission so that raw
64
- // jump targets are hidden; the dispatcher block reverses it at runtime.
65
- // To change the encoding scheme, update both here and in jumpDispatcher.ts.
66
- const newOperands = operands.map((operand) => {
67
- if (
68
- operand === undefined ||
69
- operand === null ||
70
- typeof operand !== "object"
71
- )
72
- return operand;
73
-
74
- const type = (operand as any).type;
75
-
76
- if (type === "label") {
77
- const pc = labelToPc.get((operand as any).label);
78
- if (pc === undefined)
79
- throw new Error(`Undefined label: ${(operand as any).label}`);
80
-
81
- let resolvedValue = pc + ((operand as any).offset ?? 0);
82
- if ((operand as any).transform) {
83
- resolvedValue = (operand as any).transform(resolvedValue);
84
- }
85
-
86
- const newOperand = {
87
- type: "number",
88
- resolvedValue: resolvedValue,
89
- };
90
- return Object.assign(operand, newOperand);
91
- }
92
-
93
- return operand;
94
- });
95
-
96
- const newInstr = [op, ...newOperands];
97
- (newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
98
-
99
- // Pseudo-op "defineLabel"s are kept within this bytecode as the Serializer is responsible for dropping it, and its useful information for comment generation
100
- resolved.push(newInstr);
101
- }
102
-
103
- // Patch each function descriptor's startPc now that labels are resolved.
104
- for (const desc of compiler.fnDescriptors) {
105
- desc.startPc =
106
- labelToPc.get(desc.startLabel) ?? labelToPc.get(desc.entryLabel);
107
- }
108
-
109
- return {
110
- bytecode: resolved,
111
- };
112
- }
@@ -1,226 +0,0 @@
1
- // resolveRegisters
2
- // Converts virtual RegisterOperand objects into concrete slot indices and sets
3
- // each FnDescriptor's regCount.
4
- //
5
- // Two-tier slot assignment:
6
- //
7
- // "local::" pool (params, `arguments`, hoisted vars, upvalue-captured vars)
8
- // ─────────────────────────────────────────────────────────────────────────
9
- // Sorted by virtual-id, slots assigned sequentially with NO reuse.
10
- // This is required because:
11
- // • The runtime writes args[i] to regs[base + i] at call time, so params
12
- // MUST occupy slots 0..paramCount-1 in virtual-id order.
13
- // • Open upvalues hold an absolute slot index and read regs[base+slot] for
14
- // the lifetime of the outer frame — reusing a captured slot corrupts reads.
15
- //
16
- // All other pools (e.g. "temp::", "canary::", pass-introduced pools)
17
- // ─────────────────────────────────────────────────────────────────────────
18
- // Linear-scan with a free list: registers are sorted by firstUse, and any
19
- // slot whose previous occupant's lastUse < current register's firstUse is
20
- // recycled. An explicit [null, freeRegOperand(reg)] pseudo-instruction clamps
21
- // lastUse early, enabling reuse before the natural end of the live range.
22
- //
23
- // Pools are processed in priority order: "local::" always first (slots
24
- // 0..N), then remaining pools alphabetically. This keeps temp slots above
25
- // the reserved param/local region.
26
- //
27
- // regCount = max concrete slot used across all pools + 1.
28
- //
29
- // Run AFTER all IR-level passes but BEFORE resolveLabels / resolveConstants.
30
-
31
- import type { Bytecode } from "../../types.ts";
32
- import { Compiler } from "../../compiler.ts";
33
-
34
- export function resolveRegisters(
35
- bc: Bytecode,
36
- compiler: Compiler,
37
- ): { bytecode: Bytecode } {
38
- function registerPoolKey(op: {
39
- kind?: string;
40
- scopeId?: string | number;
41
- pinned?: boolean;
42
- }): string {
43
- // Pinned registers must never share a slot with anything else.
44
- // Passes set this on registers whose live range crosses a CFF back-edge
45
- // that the linear-scan liveness analysis cannot see.
46
- if (op.pinned) return "local::";
47
- return `${op.kind ?? "local"}::${op.scopeId ?? ""}`;
48
- }
49
-
50
- // ── Pass 1: collect live ranges ───────────────────────────────────────────
51
- // For each (fnId, virtId) record the first and last instruction index where
52
- // the register appears as a real operand. A freeReg marker clamps lastUse.
53
- type RegInfo = {
54
- firstUse: number;
55
- lastUse: number;
56
- poolKey: string;
57
- freed: boolean; // true once a freeReg has been seen; prevents further extension
58
- };
59
- // fnId -> virtId -> RegInfo
60
- const fnRegInfo = new Map<number, Map<number, RegInfo>>();
61
-
62
- for (let i = 0; i < bc.length; i++) {
63
- const instr = bc[i];
64
- for (let j = 1; j < instr.length; j++) {
65
- const op = instr[j] as any;
66
- if (!op || typeof op !== "object") continue;
67
-
68
- if (op.type === "register") {
69
- const { fnId, id } = op;
70
- const poolKey = registerPoolKey(op);
71
- let fnMap = fnRegInfo.get(fnId);
72
- if (!fnMap) {
73
- fnMap = new Map();
74
- fnRegInfo.set(fnId, fnMap);
75
- }
76
- const existing = fnMap.get(id);
77
- if (!existing) {
78
- fnMap.set(id, { firstUse: i, lastUse: i, poolKey, freed: false });
79
- } else if (!existing.freed) {
80
- // Only extend lastUse if no explicit freeReg has clamped it yet.
81
- existing.lastUse = i;
82
- }
83
- } else if (op.type === "freeReg") {
84
- // Explicit end-of-life marker: clamp lastUse and prevent extension.
85
- const { fnId, id } = op;
86
- const fnMap = fnRegInfo.get(fnId);
87
- if (fnMap) {
88
- const info = fnMap.get(id);
89
- if (info && !info.freed) {
90
- info.lastUse = i;
91
- info.freed = true;
92
- }
93
- }
94
- }
95
- }
96
- }
97
-
98
- // ── Pass 2: slot assignment per function ──────────────────────────────────
99
- // fnId -> virtId -> concrete slot
100
- const fnSlotMaps = new Map<number, Map<number, number>>();
101
-
102
- // Pool ordering: "local::" always first; all other keys sorted alphabetically.
103
- function poolSortKey(key: string): [number, string] {
104
- return key === "local::" ? [0, ""] : [1, key];
105
- }
106
-
107
- for (const [fnId, regMap] of fnRegInfo) {
108
- // Group by pool key.
109
- const pools = new Map<
110
- string,
111
- Array<{ id: number; firstUse: number; lastUse: number }>
112
- >();
113
- for (const [id, info] of regMap) {
114
- let pool = pools.get(info.poolKey);
115
- if (!pool) {
116
- pool = [];
117
- pools.set(info.poolKey, pool);
118
- }
119
- pool.push({ id, firstUse: info.firstUse, lastUse: info.lastUse });
120
- }
121
-
122
- const sortedPoolKeys = Array.from(pools.keys()).sort((a, b) => {
123
- const [pa, sa] = poolSortKey(a);
124
- const [pb, sb] = poolSortKey(b);
125
- if (pa !== pb) return pa - pb;
126
- return sa < sb ? -1 : sa > sb ? 1 : 0;
127
- });
128
-
129
- const slotMap = new Map<number, number>(); // virtId -> slot
130
- fnSlotMaps.set(fnId, slotMap);
131
-
132
- // nextSlot is the high-water mark: the next fresh slot to allocate.
133
- // It is shared across all pools so each pool's slots start above the
134
- // previous pool's maximum slot.
135
- let nextSlot = 0;
136
-
137
- for (const poolKey of sortedPoolKeys) {
138
- const regs = pools.get(poolKey)!;
139
-
140
- if (poolKey === "local::") {
141
- // ── Local pool: virtual-id order, no reuse ────────────────────────
142
- // Params must be at the lowest slots (written by the runtime at call
143
- // time); upvalue captures must keep their slot for the frame's lifetime.
144
- regs.sort((a, b) => a.id - b.id);
145
- for (const reg of regs) {
146
- slotMap.set(reg.id, nextSlot++);
147
- }
148
- } else {
149
- // ── Non-local pool: firstUse order, linear-scan reuse ─────────────
150
- regs.sort((a, b) => a.firstUse - b.firstUse);
151
-
152
- // freeList entries: { slot, freeAt } where freeAt = lastUse of current
153
- // occupant. A slot becomes available when freeAt < next reg's firstUse.
154
- const freeList: Array<{ slot: number; freeAt: number }> = [];
155
-
156
- for (const reg of regs) {
157
- // Find the lowest-numbered slot whose last occupant has ended.
158
- let bestSlot = -1;
159
- let bestIdx = -1;
160
- for (let k = 0; k < freeList.length; k++) {
161
- if (freeList[k].freeAt < reg.firstUse) {
162
- if (bestSlot === -1 || freeList[k].slot < bestSlot) {
163
- bestSlot = freeList[k].slot;
164
- bestIdx = k;
165
- }
166
- }
167
- }
168
-
169
- let assignedSlot: number;
170
- if (bestIdx !== -1) {
171
- assignedSlot = bestSlot;
172
- freeList.splice(bestIdx, 1);
173
- } else {
174
- assignedSlot = nextSlot++;
175
- }
176
-
177
- slotMap.set(reg.id, assignedSlot);
178
- freeList.push({ slot: assignedSlot, freeAt: reg.lastUse });
179
- }
180
- // nextSlot already reflects the high-water mark; reused slots are
181
- // always < nextSlot by construction.
182
- }
183
- }
184
- }
185
-
186
- // ── Pass 3: patch register operands ──────────────────────────────────────
187
- for (const instr of bc) {
188
- for (let i = 1; i < instr.length; i++) {
189
- const op = instr[i] as any;
190
- if (!op || typeof op !== "object") continue;
191
- if (op.type === "register") {
192
- op.resolvedValue = fnSlotMaps.get(op.fnId)?.get(op.id);
193
- }
194
- }
195
- }
196
-
197
- // ── Pass 4: set regCount on each FnDescriptor ─────────────────────────────
198
- // regCount = max concrete slot used + 1 (not sum of virtual-register counts).
199
- for (const desc of compiler.fnDescriptors) {
200
- const fnId = desc._fnIdx!;
201
- const slotMap = fnSlotMaps.get(fnId);
202
- let regCount = 0;
203
- if (slotMap) {
204
- for (const slot of slotMap.values()) {
205
- if (slot + 1 > regCount) regCount = slot + 1;
206
- }
207
- }
208
- desc.regCount = regCount;
209
- }
210
-
211
- compiler.mainRegCount = compiler.mainFn?.regCount ?? 0;
212
-
213
- // ── Pass 5: patch fnRegCount operands ────────────────────────────────────
214
- for (const instr of bc) {
215
- for (let i = 1; i < instr.length; i++) {
216
- const op = instr[i] as any;
217
- if (!op || typeof op !== "object") continue;
218
- if (op.type === "fnRegCount") {
219
- const desc = compiler.fnDescriptors[op.fnId];
220
- op.resolvedValue = desc?.regCount ?? 0;
221
- }
222
- }
223
- }
224
-
225
- return { bytecode: bc };
226
- }
@@ -1,121 +0,0 @@
1
- import type { Bytecode, Instruction } from "../../types.ts";
2
- import { Compiler } from "../../compiler.ts";
3
- import { choice } from "../../utils/random-utils.ts";
4
- import { getInstructionSize } from "../../utils/op-utils.ts";
5
-
6
- export function selfModifying(
7
- bc: Bytecode,
8
- compiler: Compiler,
9
- ): { bytecode: Bytecode } {
10
- // Walk the bytecode looking for "defineLabel" pseudo-ops, which start basic
11
- // blocks. For each block we collect the body (instructions between the label
12
- // and the next label/jump terminator), move it to the end of the bytecode
13
- // under a fresh "patch_LXX" label, and replace it in-place with:
14
- //
15
- // defineLabel ("originalLabel") ← kept as-is (pseudo-op)
16
- // PATCH destPc sliceStart sliceEnd ← 4 flat slots total
17
- // Garbage Opcodes × bodyFlatSize ← placeholder slots
18
- //
19
- // PATCH reads three inline operands via _operand():
20
- // destPc = originalLabel + 4 (first slot after PATCH's own 4 slots)
21
- // sliceStart = patchLabel (flat PC of appended body)
22
- // sliceEnd = patchLabel + bodyFlatSize
23
- //
24
- // On first execution PATCH copies bytecode[sliceStart..sliceEnd) over the
25
- // placeholder region starting at destPc. Execution then falls through into
26
- // the freshly-patched body. Subsequent calls are idempotent.
27
-
28
- const { OP, JUMP_OPS } = compiler;
29
-
30
- const result: Bytecode = [];
31
- const appended: Bytecode = [];
32
- let patchCount = 0;
33
-
34
- let i = 0;
35
- while (i < bc.length) {
36
- const instr = bc[i];
37
- const [op, operand] = instr;
38
-
39
- // Detect a defineLabel pseudo-op — start of a new basic block.
40
- if (
41
- op === null &&
42
- operand !== null &&
43
- typeof operand === "object" &&
44
- (operand as any).type === "defineLabel"
45
- ) {
46
- const originalLabel = (operand as any).label as string;
47
- result.push(instr); // keep the defineLabel marker
48
- i++;
49
-
50
- // Collect body: everything after the label until the next terminator.
51
- let j = i;
52
- while (j < bc.length) {
53
- const [nextOp, nextOperand] = bc[j];
54
-
55
- // Another defineLabel = boundary of the next block.
56
- if (
57
- nextOp === null &&
58
- typeof nextOperand === "object" &&
59
- (nextOperand as any)?.type === "defineLabel"
60
- ) {
61
- break;
62
- }
63
-
64
- // Jump instructions, RETURN all terminate the body.
65
- if (nextOp !== null && (JUMP_OPS.has(nextOp) || nextOp === OP.RETURN)) {
66
- break;
67
- }
68
-
69
- j++;
70
- }
71
-
72
- const body = bc.slice(i, j);
73
- const N = body.length;
74
-
75
- if (N === 0) {
76
- // Nothing to transform — label is immediately followed by a terminator.
77
- continue;
78
- }
79
-
80
- const patchLabel = `patch_${originalLabel}_${patchCount++}`;
81
-
82
- // Flat size of the body (each instruction occupies instr.length slots).
83
- const bodyFlatSize = body.reduce(
84
- (acc, instr) => acc + getInstructionSize(instr),
85
- 0,
86
- );
87
-
88
- // ── PATCH instruction (4 flat slots: opcode + 3 operands) ───────────
89
- // destPc = originalLabel + 4 (slot right after PATCH's 4 slots)
90
- // sliceStart = patchLabel
91
- // sliceEnd = patchLabel + bodyFlatSize
92
- result.push([
93
- OP.PATCH as number,
94
- { type: "label", label: originalLabel, offset: 4 },
95
- { type: "label", label: patchLabel },
96
- { type: "label", label: patchLabel, offset: bodyFlatSize },
97
- ] as unknown as Instruction);
98
-
99
- // ── Placeholders (Garbage Opcodes * bodyFlatSize, each 1 flat slot) ────────────
100
- // These are overwritten by PATCH on first execution.
101
- for (let p = 0; p < bodyFlatSize; p++) {
102
- const randomOpcode = choice(Object.values(compiler.OP));
103
- result.push([+randomOpcode]);
104
- }
105
-
106
- // ── Append real body at end ─────────────────────────────────────────
107
- appended.push([null, { type: "defineLabel", label: patchLabel }]);
108
- for (const bodyInstr of body) {
109
- appended.push(bodyInstr);
110
- }
111
-
112
- i = j; // skip over the original body in the input array
113
- continue;
114
- }
115
-
116
- result.push(instr);
117
- i++;
118
- }
119
-
120
- return { bytecode: [...result, ...appended] };
121
- }
@@ -1,164 +0,0 @@
1
- import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
2
- import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
3
- import { getInstructionSize, nextFreeSlot } from "../../utils/op-utils.ts";
4
-
5
- export const nSizedOps = [
6
- "MAKE_CLOSURE",
7
- "BUILD_ARRAY",
8
- "BUILD_OBJECT",
9
- "CALL",
10
- "CALL_METHOD",
11
- "NEW",
12
- ];
13
-
14
- // Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
15
- // Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
16
- // Only instructions that are fixed-sized are considered.
17
- // MAKE_CLOSURE and other N-sized instructions cannot be specialized
18
- // Operands are converted into objects and marked as 'placeholder' - other passes can mutate and the reference stays intact
19
- // We need a reference throughout the pipeline so that final AST generation can place the actual value
20
- // The 'placeholder' flag drops the operand from the final bytecode - any size calculation must not count these
21
- export function specializedOpcodes(
22
- bc: Bytecode,
23
- compiler: Compiler,
24
- ): { bytecode: Bytecode } {
25
- const disallowedOps = new Set(nSizedOps.map((name) => compiler.OP[name]));
26
-
27
- // ── Step 1: count frequency of eligible (op, operand) pairs ───────────────
28
- const freqMap = new Map<
29
- string,
30
- {
31
- op: number;
32
- operands: InstrOperand[];
33
- operandsKey: string;
34
- occurences: number;
35
- }
36
- >();
37
-
38
- const instrToOperandKey = new WeakMap<Instruction, string>();
39
-
40
- for (const instr of bc) {
41
- const op = instr[0];
42
- if (op === null || disallowedOps.has(op)) continue;
43
-
44
- // Only supports between 1-6 operands
45
- const operandCount = getInstructionSize(instr) - 1;
46
- if (operandCount < 1 || operandCount > 6) continue;
47
-
48
- // Convert numbers into operand objects so they can be modified elsewhere and preserved
49
- const oldOperands = instr.slice(1);
50
-
51
- let operands = [];
52
-
53
- for (const operand of oldOperands) {
54
- if (typeof operand === "number") {
55
- operands.push({
56
- type: "number",
57
- value: operand,
58
- resolvedValue: operand,
59
- } as InstrOperand);
60
- } else {
61
- operands.push(operand as InstrOperand);
62
- }
63
- }
64
-
65
- instr.length = 1;
66
- instr.push(...operands);
67
-
68
- const operandsKey = JSON.stringify(operands);
69
- instrToOperandKey.set(instr, operandsKey);
70
-
71
- const key = `${op},${operandsKey}`;
72
- const entry = freqMap.get(key);
73
- if (entry) {
74
- entry.occurences++;
75
- } else {
76
- freqMap.set(key, {
77
- op,
78
- operands,
79
- operandsKey,
80
- occurences: 1,
81
- });
82
- }
83
- }
84
-
85
- // ── Step 2: keep combinations that appear >= 2 times, sort by frequency ───
86
- const candidates = Array.from(freqMap.values())
87
- .filter((e) => e.occurences >= 1)
88
- .sort((a, b) => b.occurences - a.occurences)
89
- .slice(0, 1000);
90
-
91
- if (candidates.length === 0) return { bytecode: bc };
92
-
93
- // ── Step 3: assign free opcode slots to the best candidates ───────────────
94
- const sigToSpecial = new Map<string, number>();
95
- const specializedOps: Compiler["SPECIALIZED_OPS"] = {};
96
-
97
- for (const candidate of candidates) {
98
- const specialOp = nextFreeSlot(compiler);
99
- if (specialOp === -1) break;
100
- const { op: originalOp, operands, operandsKey } = candidate;
101
-
102
- const key = `${originalOp},${operandsKey}`;
103
- sigToSpecial.set(key, specialOp);
104
-
105
- specializedOps[specialOp] = { originalOp, operands };
106
-
107
- // Register a human-readable name for disassembly / debugging
108
- const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
109
- compiler.OP_NAME[specialOp] = `${originalName}_${operandsKey}`;
110
- }
111
-
112
- // Store mapping so the interpreter knows how to dispatch the specialized op
113
- compiler.SPECIALIZED_OPS = specializedOps;
114
-
115
- // ── Step 4: replace matching instructions with the new single-byte opcode ─
116
- const result: Bytecode = [];
117
-
118
- for (const instr of bc) {
119
- const op = instr[0];
120
- // Only consider instructions with one or more operands
121
- if (op === null || instr.length <= 1 || op === compiler.OP.MAKE_CLOSURE) {
122
- result.push(instr);
123
- continue;
124
- }
125
-
126
- const operands = instr.slice(1);
127
- const operandsKey = instrToOperandKey.get(instr);
128
- if (!operandsKey) {
129
- result.push(instr);
130
- continue;
131
- }
132
-
133
- const key = `${op},${operandsKey}`;
134
-
135
- const specialOpCode = sigToSpecial.get(key)!;
136
-
137
- if (!specialOpCode) {
138
- result.push(instr);
139
- continue;
140
- }
141
-
142
- const newOperands = operands.map((operand) => {
143
- const operandAsObject: any =
144
- typeof operand === "object" && operand
145
- ? operand
146
- : {
147
- type: "number",
148
- resolvedValue: operand,
149
- };
150
-
151
- operandAsObject.placeholder = true;
152
- return operandAsObject;
153
- });
154
-
155
- const newInstr: Instruction = [specialOpCode, ...newOperands];
156
-
157
- // Preserve source-node information for error reporting
158
- (newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
159
-
160
- result.push(newInstr);
161
- }
162
-
163
- return { bytecode: result };
164
- }