js-confuser-vm 0.0.9 → 0.1.1

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 (64) hide show
  1. package/.gitmodules +4 -0
  2. package/CHANGELOG.md +125 -2
  3. package/README.md +128 -53
  4. package/bench.ts +146 -0
  5. package/disassemble.ts +12 -0
  6. package/dist/build-runtime.js +41 -15
  7. package/dist/compiler.js +328 -181
  8. package/dist/disassembler.js +317 -0
  9. package/dist/index.js +7 -2
  10. package/dist/runtime.js +255 -176
  11. package/dist/template.js +258 -0
  12. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  13. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  14. package/dist/transforms/bytecode/dispatcher.js +266 -0
  15. package/dist/transforms/bytecode/macroOpcodes.js +3 -3
  16. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  17. package/dist/transforms/bytecode/resolveLabels.js +21 -18
  18. package/dist/transforms/bytecode/resolveRegisters.js +216 -0
  19. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  20. package/dist/transforms/bytecode/specializedOpcodes.js +22 -12
  21. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  22. package/dist/transforms/runtime/classObfuscation.js +43 -0
  23. package/dist/transforms/runtime/handlerTable.js +91 -0
  24. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  25. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  26. package/dist/types.js +42 -1
  27. package/dist/utils/ast-utils.js +14 -0
  28. package/dist/utils/op-utils.js +1 -2
  29. package/dist/utils/pass-utils.js +100 -0
  30. package/dist/utils/profile-utils.js +3 -0
  31. package/index.ts +22 -16
  32. package/jest.config.js +19 -2
  33. package/output.disassembled.js +41 -0
  34. package/package.json +2 -1
  35. package/src/build-runtime.ts +113 -78
  36. package/src/compiler.ts +2703 -2482
  37. package/src/disassembler.ts +329 -0
  38. package/src/index.ts +12 -2
  39. package/src/options.ts +8 -1
  40. package/src/runtime.ts +294 -180
  41. package/src/template.ts +265 -0
  42. package/src/transforms/bytecode/aliasedOpcodes.ts +5 -2
  43. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  44. package/src/transforms/bytecode/dispatcher.ts +292 -0
  45. package/src/transforms/bytecode/macroOpcodes.ts +4 -4
  46. package/src/transforms/bytecode/resolveLabels.ts +31 -27
  47. package/src/transforms/bytecode/resolveRegisters.ts +226 -0
  48. package/src/transforms/bytecode/specializedOpcodes.ts +27 -20
  49. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  50. package/src/transforms/runtime/classObfuscation.ts +59 -0
  51. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  52. package/src/types.ts +106 -5
  53. package/src/utils/ast-utils.ts +19 -0
  54. package/src/utils/op-utils.ts +2 -2
  55. package/src/utils/pass-utils.ts +126 -0
  56. package/src/utils/profile-utils.ts +3 -0
  57. package/tsconfig.json +1 -1
  58. package/dist/transforms/utils/op-utils.js +0 -25
  59. package/dist/transforms/utils/random-utils.js +0 -27
  60. package/dist/utilts.js +0 -3
  61. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  62. package/src/transforms/runtime/internalVariables.ts +0 -270
  63. package/src/transforms/runtime/microOpcodes.ts +0 -93
  64. /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
@@ -1,25 +0,0 @@
1
- import { getRandomInt } from "./random-utils.js";
2
- export const U16_MAX = 0xffff; // bytecode operands are u16
3
-
4
- /** Returns the next free opcode slot, or -1 when the space is exhausted. */
5
- export function nextFreeSlot(usedOpcodes) {
6
- if (usedOpcodes.size > U16_MAX) return -1;
7
- let attempts = 0;
8
- while (attempts++ < 512) {
9
- const candidate = getRandomInt(0, U16_MAX);
10
- if (!usedOpcodes.has(candidate)) {
11
- usedOpcodes.add(candidate);
12
- return candidate;
13
- }
14
- }
15
- // Fallback: linear scan from a random start
16
- const start = getRandomInt(0, U16_MAX);
17
- for (let i = 0; i <= U16_MAX; i++) {
18
- const v = start + i & U16_MAX;
19
- if (!usedOpcodes.has(v)) {
20
- usedOpcodes.add(v);
21
- return v;
22
- }
23
- }
24
- return -1;
25
- }
@@ -1,27 +0,0 @@
1
- import { ok } from "assert";
2
- export function getPlaceholder() {
3
- return Math.random().toString(36).substring(2, 15);
4
- }
5
- export function choice(elements) {
6
- ok(elements.length > 0, "choice() called on empty sequence");
7
- return elements[Math.floor(Math.random() * elements.length)];
8
- }
9
- export function getRandom() {
10
- return Math.random();
11
- }
12
- export function getRandomInt(min, max) {
13
- ok(min <= max, "min must be <= max");
14
- return Math.floor(Math.random() * (max - min + 1)) + min;
15
- }
16
-
17
- /**
18
- * Shuffles an array in-place using the Fisher-Yates algorithm.
19
- * @param array - The array to shuffle (mutated)
20
- */
21
- export function shuffle(array) {
22
- for (let i = array.length - 1; i > 0; i--) {
23
- const j = Math.floor(Math.random() * (i + 1));
24
- [array[i], array[j]] = [array[j], array[i]];
25
- }
26
- return array;
27
- }
package/dist/utilts.js DELETED
@@ -1,3 +0,0 @@
1
- export function escapeRegex(s) {
2
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
- }
@@ -1,291 +0,0 @@
1
- import { parse } from "@babel/parser";
2
- import traverseImport from "@babel/traverse";
3
- import * as t from "@babel/types";
4
- import { ok } from "assert";
5
- import { Compiler, VM_RUNTIME, SOURCE_NODE_SYM } from "../../compiler.ts";
6
- import type { Bytecode, Instruction } from "../../types.ts";
7
- import { nextFreeSlot } from "../../utils/op-utils.ts";
8
- import { nSizedOps } from "./specializedOpcodes.ts";
9
- import generate from "@babel/generator";
10
-
11
- const traverse = (traverseImport.default ||
12
- traverseImport) as typeof traverseImport.default;
13
-
14
- // Extract the real statement list from a SwitchCase consequent.
15
- function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
16
- let stmts: t.Statement[];
17
- if (
18
- switchCase.consequent.length === 1 &&
19
- t.isBlockStatement(switchCase.consequent[0])
20
- ) {
21
- stmts = (switchCase.consequent[0] as t.BlockStatement).body;
22
- } else {
23
- stmts = switchCase.consequent as t.Statement[];
24
- }
25
- return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
26
- }
27
-
28
- // Count how many IR-level operands a single statement consumes.
29
- // Returns null if the statement is ineligible (contains a loop, or has
30
- // _operand()/_constant() calls inside a conditional branch).
31
- function countStatementOperands(stmt: t.Statement): number | null {
32
- let count = 0;
33
- let ineligible = false;
34
-
35
- const file = t.file(t.program([t.cloneNode(stmt, true) as t.Statement]));
36
-
37
- traverse(file, {
38
- enter(path) {
39
- if (ineligible) {
40
- path.stop();
41
- return;
42
- }
43
-
44
- const nodeType = path.node.type;
45
-
46
- // Don't traverse into nested functions
47
- if (
48
- nodeType === "FunctionDeclaration" ||
49
- nodeType === "FunctionExpression" ||
50
- nodeType === "ArrowFunctionExpression"
51
- ) {
52
- path.skip();
53
- return;
54
- }
55
-
56
- // Count _operand() and _constant() calls
57
- if (nodeType === "CallExpression") {
58
- const call = path.node as t.CallExpression;
59
- const callee = call.callee;
60
- if (
61
- t.isMemberExpression(callee) &&
62
- t.isThisExpression(callee.object) &&
63
- t.isIdentifier(callee.property)
64
- ) {
65
- const name = (callee.property as t.Identifier).name;
66
- const operandsConsumed =
67
- name === "_operand" ? 1 : name === "_constant" ? 2 : null;
68
-
69
- if (operandsConsumed) {
70
- // You are not allowed to use _operand() in loops or branches
71
- const ancestors = path.getAncestry();
72
-
73
- if (
74
- ancestors.find(
75
- (t) =>
76
- t.isLoop() ||
77
- t.isIfStatement() ||
78
- t.isSwitchStatement() ||
79
- t.isConditionalExpression() ||
80
- t.isLogicalExpression(),
81
- )
82
- ) {
83
- ineligible = true;
84
- path.stop();
85
- return;
86
- }
87
-
88
- count += operandsConsumed;
89
- }
90
- }
91
- }
92
- },
93
- });
94
-
95
- return ineligible ? null : count;
96
- }
97
-
98
- // Analyse the VM runtime's @SWITCH statement to build a per-opcode map of
99
- // { stmtIndex → irOperandCount } for every case that can be split.
100
- // Returns a map: opValue → array of per-statement operand counts (null if ineligible).
101
- function analyzeRuntimeCases(compiler: Compiler): Map<number, number[]> {
102
- // Parse the runtime source
103
- const ast = parse(VM_RUNTIME, { sourceType: "unambiguous" });
104
-
105
- // Build reverse name→opValue map from original OPs only
106
- const nameToOp = new Map<string, number>();
107
- for (const [name, val] of Object.entries(compiler.OP)) {
108
- if (val !== undefined) nameToOp.set(name, val as number);
109
- }
110
-
111
- let switchStatement: t.SwitchStatement | null = null;
112
- traverse(ast, {
113
- SwitchStatement(path) {
114
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
115
- switchStatement = path.node;
116
- path.stop();
117
- }
118
- },
119
- });
120
-
121
- ok(switchStatement, "Could not find @SWITCH statement for micro opcodes");
122
-
123
- const result = new Map<number, number[]>();
124
-
125
- for (const sc of (switchStatement as t.SwitchStatement).cases) {
126
- const test = sc.test;
127
- if (
128
- !test ||
129
- !t.isMemberExpression(test) ||
130
- !t.isIdentifier(test.object, { name: "OP" }) ||
131
- !t.isIdentifier(test.property)
132
- ) {
133
- continue;
134
- }
135
-
136
- const opName = (test.property as t.Identifier).name;
137
- const opVal = nameToOp.get(opName);
138
- if (opVal === undefined) continue;
139
-
140
- const stmts = extractCaseBody(sc);
141
- if (stmts.length < 2) continue; // need at least 2 statements to split
142
-
143
- const counts: number[] = [];
144
- let allEligible = true;
145
-
146
- // Banned patterns:
147
- // Return statements (Control flow isn't remembered)
148
- traverse(t.file(t.program(stmts)), {
149
- ReturnStatement(path) {
150
- path.stop();
151
- allEligible = false;
152
- },
153
- });
154
-
155
- for (const stmt of stmts) {
156
- const c = countStatementOperands(stmt);
157
- if (c === null) {
158
- allEligible = false;
159
- break;
160
- }
161
- if (t.isDebuggerStatement(stmt) || t.isThrowStatement(stmt)) {
162
- allEligible = false;
163
- break;
164
- }
165
- counts.push(c);
166
- }
167
-
168
- if (!allEligible) continue;
169
-
170
- // Verify that the total operand count matches the instruction size expectation
171
- // (just store for now; bytecode pass validates operands match)
172
- result.set(opVal, counts);
173
- }
174
-
175
- return result;
176
- }
177
-
178
- // Main bytecode transform: split frequently-used opcodes into per-statement
179
- // micro-opcodes so each sub-instruction is as small as possible.
180
- export function microOpcodes(
181
- bc: Bytecode,
182
- compiler: Compiler,
183
- ): { bytecode: Bytecode } {
184
- // ── Step 1: analyse runtime to discover splittable opcodes ──────────────────
185
- const opAnalysis = analyzeRuntimeCases(compiler);
186
- if (opAnalysis.size === 0) return { bytecode: bc };
187
-
188
- // ── Step 2: count opcode frequency in bytecode ────────────────────────────
189
- const disallowedOps = new Set(nSizedOps.map((name) => compiler.OP[name]));
190
-
191
- disallowedOps.add(compiler.OP.RETURN);
192
-
193
- const freqMap = new Map<number, number>();
194
- for (const instr of bc) {
195
- const op = instr[0];
196
- if (op === null || !opAnalysis.has(op) || disallowedOps.has(op)) continue;
197
- freqMap.set(op, (freqMap.get(op) ?? 0) + 1);
198
- }
199
-
200
- // ── Step 3: sort by frequency, keep opcodes that actually appear ─────────
201
- const candidates = Array.from(freqMap.entries())
202
- .filter(([, count]) => count >= 1)
203
- .sort(([, a], [, b]) => b - a)
204
- .map(([op]) => op);
205
-
206
- if (candidates.length === 0) return { bytecode: bc };
207
-
208
- // ── Step 4: assign free opcode slots for each sub-statement ─────────────
209
- // Build: originalOp → [{ microOp, irOperandCount }, ...]
210
- const originalToSubOps = new Map<
211
- number,
212
- { microOp: number; irOperandCount: number }[]
213
- >();
214
-
215
- for (const origOp of candidates) {
216
- const stmtCounts = opAnalysis.get(origOp)!;
217
-
218
- // Pre-allocate all needed slots; if any slot is unavailable, skip this op.
219
- const slots: number[] = [];
220
- for (let si = 0; si < stmtCounts.length; si++) {
221
- const slot = nextFreeSlot(compiler);
222
- if (slot === -1) break;
223
-
224
- compiler.OP_NAME[slot] = `MICRO_${origOp}_${si}`;
225
- slots.push(slot);
226
- }
227
- if (slots.length !== stmtCounts.length) continue;
228
-
229
- const subOps: { microOp: number; irOperandCount: number }[] = [];
230
- const origName = compiler.OP_NAME[origOp] ?? `OP_${origOp}`;
231
-
232
- for (let si = 0; si < stmtCounts.length; si++) {
233
- const microOp = slots[si];
234
- const irOperandCount = stmtCounts[si];
235
- subOps.push({ microOp, irOperandCount });
236
-
237
- compiler.OP_NAME[microOp] = `MICRO_${origName}_${si}`;
238
- compiler.MICRO_OPS[microOp] = {
239
- originalOp: origOp,
240
- stmtIndex: si,
241
- irOperandCount,
242
- };
243
- }
244
-
245
- originalToSubOps.set(origOp, subOps);
246
- }
247
-
248
- if (originalToSubOps.size === 0) return { bytecode: bc };
249
-
250
- // ── Step 5: replace each matched instruction with sub-instructions ────────
251
- const result: Bytecode = [];
252
-
253
- for (const instr of bc) {
254
- const op = instr[0];
255
- if (op === null || !originalToSubOps.has(op)) {
256
- result.push(instr);
257
- continue;
258
- }
259
-
260
- const subOps = originalToSubOps.get(op)!;
261
- const operands = instr.slice(1); // all operands of the original instruction
262
-
263
- // Verify total operand count matches sum of sub-op IR operand counts
264
- const expectedTotal = subOps.reduce(
265
- (s, { irOperandCount }) => s + irOperandCount,
266
- 0,
267
- );
268
- if (operands.length !== expectedTotal) {
269
- throw new Error(
270
- `Operand count mismatch for opcode ${compiler.OP_NAME[op]}`,
271
- );
272
- }
273
-
274
- // Split operands among sub-instructions
275
- let offset = 0;
276
- for (const { microOp, irOperandCount } of subOps) {
277
- const subOperands = operands.slice(offset, offset + irOperandCount);
278
- offset += irOperandCount;
279
-
280
- const newInstr: Instruction = [microOp, ...subOperands];
281
- // Carry source-node info on the first sub-instruction
282
- if (offset === irOperandCount) {
283
- (newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
284
- }
285
-
286
- result.push(newInstr);
287
- }
288
- }
289
-
290
- return { bytecode: result };
291
- }
@@ -1,270 +0,0 @@
1
- // Goes through the switch case for defined identifiers on the statement level
2
- // Example:
3
- // case OP.LOAD_CONST: {
4
- // var dst = this._operand();
5
- // frame.regs[dst] = this._constant();
6
- // break;
7
- // }
8
- // You find "dst" is defined in this scope.
9
- // You first check the compiler to see if it's already assigned an index in compiler._internals mapping varName=>index
10
- // If not found, use compiler._internals.globally.size as the new index (when options.randomizeOpcodes is off, when on, choose random between 0 and 65535), and add varName=>index to compiler._internals
11
- // Then replace the VariableDeclaration to an AssignmentExpression setting left this._internals[index] = init;
12
- // Then replace all identifiers of "dst" to this._internals[index] as well (Updates references)
13
- // Final output:
14
- // case OP.LOAD_CONST: {
15
- // this._internals[index] = this._operand();
16
- // frame.regs[this._internals[index]] = this._constant();
17
- // break;
18
- // }
19
-
20
- import { Compiler } from "../../compiler.ts";
21
- import * as t from "@babel/types";
22
- import traverseImport from "@babel/traverse";
23
- import { ok } from "assert";
24
- import { getRandomInt } from "../../utils/random-utils.ts";
25
- import { U16_MAX } from "../../utils/op-utils.ts";
26
-
27
- const traverse = (traverseImport.default ||
28
- traverseImport) as typeof traverseImport.default;
29
-
30
- export function makeInternalsAccess(index: number): t.MemberExpression {
31
- return t.memberExpression(
32
- t.memberExpression(t.thisExpression(), t.identifier("_internals")),
33
- t.numericLiteral(index),
34
- true, // computed
35
- );
36
- }
37
-
38
- function collectUsedIndices(compiler: Compiler): Set<number> {
39
- const used = new Set<number>();
40
- for (const v of compiler._internals.globally.values()) used.add(v);
41
- for (const opMap of compiler._internals.opcodes.values()) {
42
- for (const v of opMap.values()) used.add(v);
43
- }
44
- return used;
45
- }
46
-
47
- // Assign or look up the _internals slot index for a variable name within a
48
- // specific opcode handler.
49
- //
50
- // _internals.opcodes[currentOpcode] is the source of truth for this opcode.
51
- // _internals.globally holds the shared pool written on first sight.
52
- //
53
- // randomizeOpcodes OFF → always reuse / create in globally, mirror to opcodes.
54
- // randomizeOpcodes ON → first time a name is seen: create global slot.
55
- // subsequent opcodes: 50% reuse global, 50% create an
56
- // opcode-specific random slot (NOT written to globally).
57
- function assignInternalsIndex(
58
- name: string,
59
- compiler: Compiler,
60
- currentOpcode: number,
61
- ): number {
62
- // Ensure per-opcode map exists
63
- let opcodeMap = compiler._internals.opcodes.get(currentOpcode);
64
- if (!opcodeMap) {
65
- opcodeMap = new Map();
66
- compiler._internals.opcodes.set(currentOpcode, opcodeMap);
67
- }
68
-
69
- // Already registered for this opcode — return immediately
70
- const existing = opcodeMap.get(name);
71
- if (existing !== undefined) return existing;
72
-
73
- const globalIndex = compiler._internals.globally.get(name);
74
- let index: number;
75
-
76
- if (!compiler.options.randomizeOpcodes) {
77
- // Non-random: always share the global sequential slot
78
- if (globalIndex === undefined) {
79
- index = compiler._internals.globally.size;
80
- compiler._internals.globally.set(name, index);
81
- } else {
82
- index = globalIndex;
83
- }
84
- } else if (globalIndex === undefined) {
85
- // First opcode to declare this variable — establish the global slot
86
- const used = collectUsedIndices(compiler);
87
- let candidate: number;
88
- do {
89
- candidate = getRandomInt(0, U16_MAX);
90
- } while (used.has(candidate));
91
- index = candidate;
92
- compiler._internals.globally.set(name, index);
93
- } else {
94
- // Already in global: 50% chance to reuse, 50% opcode-specific new slot
95
- if (Math.random() < 0.5) {
96
- index = globalIndex;
97
- } else {
98
- const used = collectUsedIndices(compiler);
99
- let candidate: number;
100
- do {
101
- candidate = getRandomInt(0, U16_MAX);
102
- } while (used.has(candidate));
103
- index = candidate;
104
- // Intentionally NOT written to globally — this slot is opcode-specific
105
- }
106
- }
107
-
108
- opcodeMap.set(name, index);
109
- return index;
110
- }
111
-
112
- export function applyInternalVariablesToSwitchCase(
113
- node: t.SwitchCase,
114
- compiler: Compiler,
115
- currentOpcode: number,
116
- ) {
117
- // Work with the actual body array (block body or flat consequent)
118
- let bodyArr: t.Statement[];
119
- if (node.consequent.length === 1 && t.isBlockStatement(node.consequent[0])) {
120
- bodyArr = (node.consequent[0] as t.BlockStatement).body;
121
- } else {
122
- bodyArr = node.consequent as t.Statement[];
123
- }
124
-
125
- // Single traversal: declarations and references handled in one pass.
126
- //
127
- // Declaration (Identifier is VariableDeclarator.id):
128
- // → register/look-up slot, replace entire VariableDeclaration with
129
- // AssignmentExpression (bare for ForStatement.init, else ExpressionStatement).
130
- //
131
- // Reference (any other Identifier):
132
- // → look up opcodes[currentOpcode] (source of truth) and replace if found.
133
- // This handles cross-statement refs produced by micro-opcode splitting.
134
- const syntheticFile = t.file(t.program(bodyArr as t.Statement[]));
135
- const illegalNames = new Set<string>(); // Nested closure names are skipped
136
-
137
- traverse(syntheticFile, {
138
- Identifier(path) {
139
- const name = path.node.name;
140
- if (illegalNames.has(name)) return;
141
-
142
- // Skip non-computed property names: obj.name
143
- if (
144
- t.isMemberExpression(path.parent) &&
145
- !path.parent.computed &&
146
- path.parent.property === path.node
147
- ) {
148
- return;
149
- }
150
-
151
- // Skip non-computed object-property keys: { name: value }
152
- if (
153
- t.isObjectProperty(path.parent) &&
154
- !path.parent.computed &&
155
- path.parent.key === path.node
156
- ) {
157
- return;
158
- }
159
-
160
- // Don't descend into nested function scopes
161
- if (
162
- path.find(
163
- (p) =>
164
- p.isFunctionDeclaration() ||
165
- p.isFunctionExpression() ||
166
- p.isArrowFunctionExpression(),
167
- )
168
- ) {
169
- return;
170
- }
171
-
172
- // ── Declaration binding ──────────────────────────────────────────────
173
- if (t.isVariableDeclarator(path.parent) && path.parent.id === path.node) {
174
- // Verify it's not referenced in nested closure (illegal)
175
- const binding = path.scope.getBinding(name);
176
- if (
177
- binding?.referencePaths.some((rp) =>
178
- rp.findParent(
179
- (p) =>
180
- p.isFunctionDeclaration() ||
181
- p.isFunctionExpression() ||
182
- p.isArrowFunctionExpression(),
183
- ),
184
- )
185
- ) {
186
- illegalNames.add(name);
187
- return;
188
- }
189
-
190
- const index = assignInternalsIndex(name, compiler, currentOpcode);
191
- const init = (path.parent as t.VariableDeclarator).init;
192
-
193
- const assignment = t.assignmentExpression(
194
- "=",
195
- makeInternalsAccess(index),
196
- init ?? t.identifier("undefined"),
197
- );
198
-
199
- // Two levels up: VariableDeclarator → VariableDeclaration
200
- const varDeclPath = path.parentPath!.parentPath!;
201
-
202
- if (
203
- t.isForStatement(varDeclPath.parent) &&
204
- varDeclPath.parent.init === varDeclPath.node
205
- ) {
206
- // ForStatement.init accepts an Expression directly
207
- varDeclPath.replaceWith(assignment);
208
- } else {
209
- varDeclPath.replaceWith(t.expressionStatement(assignment));
210
- }
211
- return;
212
- }
213
-
214
- // ── Reference ───────────────────────────────────────────────────────
215
- // Source of truth for this opcode is its own per-opcode map
216
- const opcodeMap = compiler._internals.opcodes.get(currentOpcode);
217
- const index = opcodeMap?.get(name);
218
- if (index !== undefined) {
219
- path.replaceWith(makeInternalsAccess(index));
220
- path.skip();
221
- }
222
- },
223
- });
224
- }
225
-
226
- // This takes the AST and finds the runtime switch statement via the leading
227
- // comment "@SWITCH" then applies the above transformation to each switch case.
228
- export function applyInteralVariablesToRuntime(
229
- ast: t.File,
230
- compiler: Compiler,
231
- ) {
232
- let switchStatement: t.SwitchStatement | null = null;
233
- traverse(ast, {
234
- SwitchStatement(path) {
235
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
236
- switchStatement = path.node;
237
- path.stop();
238
- }
239
- },
240
- });
241
-
242
- ok(
243
- switchStatement,
244
- "Could not find @SWITCH statement for internal variables",
245
- );
246
-
247
- for (const sc of (switchStatement as t.SwitchStatement).cases) {
248
- const test = sc.test;
249
- let currentOpcode: number | null = null;
250
-
251
- if (
252
- test &&
253
- t.isMemberExpression(test) &&
254
- t.isIdentifier(test.object, { name: "OP" }) &&
255
- t.isIdentifier(test.property)
256
- ) {
257
- // case OP.LOAD_CONST: → resolve via compiler.OP
258
- const opName = (test.property as t.Identifier).name;
259
- const val = compiler.OP[opName as keyof typeof compiler.OP];
260
- if (val !== undefined) currentOpcode = val as number;
261
- } else if (test && t.isNumericLiteral(test)) {
262
- // Already a numeric literal (e.g. generated micro-opcode cases)
263
- currentOpcode = test.value;
264
- }
265
-
266
- if (currentOpcode === null) continue;
267
-
268
- applyInternalVariablesToSwitchCase(sc, compiler, currentOpcode);
269
- }
270
- }