js-confuser-vm 0.0.6 → 0.0.7

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 (36) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.MD +101 -37
  3. package/dist/build-runtime.js +15 -2
  4. package/dist/compiler.js +98 -51
  5. package/dist/runtime.js +5 -1
  6. package/dist/transforms/bytecode/aliasedOpcodes.js +2 -8
  7. package/dist/transforms/bytecode/macroOpcodes.js +21 -19
  8. package/dist/transforms/bytecode/microOpcodes.js +236 -0
  9. package/dist/transforms/bytecode/resolveContants.js +5 -11
  10. package/dist/transforms/bytecode/resolveLabels.js +5 -3
  11. package/dist/transforms/bytecode/specializedOpcodes.js +21 -16
  12. package/dist/transforms/runtime/internalVariables.js +202 -0
  13. package/dist/transforms/runtime/macroOpcodes.js +30 -18
  14. package/dist/transforms/runtime/microOpcodes.js +76 -0
  15. package/dist/transforms/runtime/specializedOpcodes.js +20 -18
  16. package/dist/utils/op-utils.js +15 -8
  17. package/index.ts +3 -2
  18. package/jest.config.js +2 -0
  19. package/package.json +1 -1
  20. package/src/build-runtime.ts +18 -3
  21. package/src/compiler.ts +152 -65
  22. package/src/options.ts +1 -0
  23. package/src/runtime.ts +5 -1
  24. package/src/transforms/bytecode/aliasedOpcodes.ts +2 -12
  25. package/src/transforms/bytecode/macroOpcodes.ts +28 -29
  26. package/src/transforms/bytecode/microOpcodes.ts +291 -0
  27. package/src/transforms/bytecode/resolveContants.ts +6 -13
  28. package/src/transforms/bytecode/resolveLabels.ts +5 -4
  29. package/src/transforms/bytecode/specializedOpcodes.ts +38 -28
  30. package/src/transforms/runtime/internalVariables.ts +270 -0
  31. package/src/transforms/runtime/macroOpcodes.ts +47 -20
  32. package/src/transforms/runtime/microOpcodes.ts +93 -0
  33. package/src/transforms/runtime/specializedOpcodes.ts +27 -32
  34. package/src/types.ts +1 -1
  35. package/src/utils/op-utils.ts +21 -8
  36. package/src/utilts.ts +0 -3
@@ -0,0 +1,236 @@
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 { VM_RUNTIME, SOURCE_NODE_SYM } from "../../compiler.js";
6
+ import { nextFreeSlot } from "../../utils/op-utils.js";
7
+ import { nSizedOps } from "./specializedOpcodes.js";
8
+ const traverse = traverseImport.default || traverseImport;
9
+
10
+ // Extract the real statement list from a SwitchCase consequent.
11
+ function extractCaseBody(switchCase) {
12
+ let stmts;
13
+ if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
14
+ stmts = switchCase.consequent[0].body;
15
+ } else {
16
+ stmts = switchCase.consequent;
17
+ }
18
+ return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
19
+ }
20
+
21
+ // Count how many IR-level operands a single statement consumes.
22
+ // Returns null if the statement is ineligible (contains a loop, or has
23
+ // _operand()/_constant() calls inside a conditional branch).
24
+ function countStatementOperands(stmt) {
25
+ let count = 0;
26
+ let ineligible = false;
27
+ const file = t.file(t.program([t.cloneNode(stmt, true)]));
28
+ traverse(file, {
29
+ enter(path) {
30
+ if (ineligible) {
31
+ path.stop();
32
+ return;
33
+ }
34
+ const nodeType = path.node.type;
35
+
36
+ // Don't traverse into nested functions
37
+ if (nodeType === "FunctionDeclaration" || nodeType === "FunctionExpression" || nodeType === "ArrowFunctionExpression") {
38
+ path.skip();
39
+ return;
40
+ }
41
+
42
+ // Count _operand() and _constant() calls
43
+ if (nodeType === "CallExpression") {
44
+ const call = path.node;
45
+ const callee = call.callee;
46
+ if (t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property)) {
47
+ const name = callee.property.name;
48
+ const operandsConsumed = name === "_operand" ? 1 : name === "_constant" ? 2 : null;
49
+ if (operandsConsumed) {
50
+ // You are not allowed to use _operand() in loops or branches
51
+ const ancestors = path.getAncestry();
52
+ if (ancestors.find(t => t.isLoop() || t.isIfStatement() || t.isSwitchStatement() || t.isConditionalExpression() || t.isLogicalExpression())) {
53
+ ineligible = true;
54
+ path.stop();
55
+ return;
56
+ }
57
+ count += operandsConsumed;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ });
63
+ return ineligible ? null : count;
64
+ }
65
+
66
+ // Analyse the VM runtime's @SWITCH statement to build a per-opcode map of
67
+ // { stmtIndex → irOperandCount } for every case that can be split.
68
+ // Returns a map: opValue → array of per-statement operand counts (null if ineligible).
69
+ function analyzeRuntimeCases(compiler) {
70
+ // Parse the runtime source
71
+ const ast = parse(VM_RUNTIME, {
72
+ sourceType: "unambiguous"
73
+ });
74
+
75
+ // Build reverse name→opValue map from original OPs only
76
+ const nameToOp = new Map();
77
+ for (const [name, val] of Object.entries(compiler.OP)) {
78
+ if (val !== undefined) nameToOp.set(name, val);
79
+ }
80
+ let switchStatement = null;
81
+ traverse(ast, {
82
+ SwitchStatement(path) {
83
+ if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
84
+ switchStatement = path.node;
85
+ path.stop();
86
+ }
87
+ }
88
+ });
89
+ ok(switchStatement, "Could not find @SWITCH statement for micro opcodes");
90
+ const result = new Map();
91
+ for (const sc of switchStatement.cases) {
92
+ const test = sc.test;
93
+ if (!test || !t.isMemberExpression(test) || !t.isIdentifier(test.object, {
94
+ name: "OP"
95
+ }) || !t.isIdentifier(test.property)) {
96
+ continue;
97
+ }
98
+ const opName = test.property.name;
99
+ const opVal = nameToOp.get(opName);
100
+ if (opVal === undefined) continue;
101
+ const stmts = extractCaseBody(sc);
102
+ if (stmts.length < 2) continue; // need at least 2 statements to split
103
+
104
+ const counts = [];
105
+ let allEligible = true;
106
+
107
+ // Banned patterns:
108
+ // Return statements (Control flow isn't remembered)
109
+ traverse(t.file(t.program(stmts)), {
110
+ ReturnStatement(path) {
111
+ path.stop();
112
+ allEligible = false;
113
+ }
114
+ });
115
+ for (const stmt of stmts) {
116
+ const c = countStatementOperands(stmt);
117
+ if (c === null) {
118
+ allEligible = false;
119
+ break;
120
+ }
121
+ if (t.isDebuggerStatement(stmt) || t.isThrowStatement(stmt)) {
122
+ allEligible = false;
123
+ break;
124
+ }
125
+ counts.push(c);
126
+ }
127
+ if (!allEligible) continue;
128
+
129
+ // Verify that the total operand count matches the instruction size expectation
130
+ // (just store for now; bytecode pass validates operands match)
131
+ result.set(opVal, counts);
132
+ }
133
+ return result;
134
+ }
135
+
136
+ // Main bytecode transform: split frequently-used opcodes into per-statement
137
+ // micro-opcodes so each sub-instruction is as small as possible.
138
+ export function microOpcodes(bc, compiler) {
139
+ // ── Step 1: analyse runtime to discover splittable opcodes ──────────────────
140
+ const opAnalysis = analyzeRuntimeCases(compiler);
141
+ if (opAnalysis.size === 0) return {
142
+ bytecode: bc
143
+ };
144
+
145
+ // ── Step 2: count opcode frequency in bytecode ────────────────────────────
146
+ const disallowedOps = new Set(nSizedOps.map(name => compiler.OP[name]));
147
+ disallowedOps.add(compiler.OP.RETURN);
148
+ const freqMap = new Map();
149
+ for (const instr of bc) {
150
+ const op = instr[0];
151
+ if (op === null || !opAnalysis.has(op) || disallowedOps.has(op)) continue;
152
+ freqMap.set(op, (freqMap.get(op) ?? 0) + 1);
153
+ }
154
+
155
+ // ── Step 3: sort by frequency, keep opcodes that actually appear ─────────
156
+ const candidates = Array.from(freqMap.entries()).filter(([, count]) => count >= 1).sort(([, a], [, b]) => b - a).map(([op]) => op);
157
+ if (candidates.length === 0) return {
158
+ bytecode: bc
159
+ };
160
+
161
+ // ── Step 4: assign free opcode slots for each sub-statement ─────────────
162
+ // Build: originalOp → [{ microOp, irOperandCount }, ...]
163
+ const originalToSubOps = new Map();
164
+ for (const origOp of candidates) {
165
+ const stmtCounts = opAnalysis.get(origOp);
166
+
167
+ // Pre-allocate all needed slots; if any slot is unavailable, skip this op.
168
+ const slots = [];
169
+ for (let si = 0; si < stmtCounts.length; si++) {
170
+ const slot = nextFreeSlot(compiler);
171
+ if (slot === -1) break;
172
+ compiler.OP_NAME[slot] = `MICRO_${origOp}_${si}`;
173
+ slots.push(slot);
174
+ }
175
+ if (slots.length !== stmtCounts.length) continue;
176
+ const subOps = [];
177
+ const origName = compiler.OP_NAME[origOp] ?? `OP_${origOp}`;
178
+ for (let si = 0; si < stmtCounts.length; si++) {
179
+ const microOp = slots[si];
180
+ const irOperandCount = stmtCounts[si];
181
+ subOps.push({
182
+ microOp,
183
+ irOperandCount
184
+ });
185
+ compiler.OP_NAME[microOp] = `MICRO_${origName}_${si}`;
186
+ compiler.MICRO_OPS[microOp] = {
187
+ originalOp: origOp,
188
+ stmtIndex: si,
189
+ irOperandCount
190
+ };
191
+ }
192
+ originalToSubOps.set(origOp, subOps);
193
+ }
194
+ if (originalToSubOps.size === 0) return {
195
+ bytecode: bc
196
+ };
197
+
198
+ // ── Step 5: replace each matched instruction with sub-instructions ────────
199
+ const result = [];
200
+ for (const instr of bc) {
201
+ const op = instr[0];
202
+ if (op === null || !originalToSubOps.has(op)) {
203
+ result.push(instr);
204
+ continue;
205
+ }
206
+ const subOps = originalToSubOps.get(op);
207
+ const operands = instr.slice(1); // all operands of the original instruction
208
+
209
+ // Verify total operand count matches sum of sub-op IR operand counts
210
+ const expectedTotal = subOps.reduce((s, {
211
+ irOperandCount
212
+ }) => s + irOperandCount, 0);
213
+ if (operands.length !== expectedTotal) {
214
+ throw new Error(`Operand count mismatch for opcode ${compiler.OP_NAME[op]}`);
215
+ }
216
+
217
+ // Split operands among sub-instructions
218
+ let offset = 0;
219
+ for (const {
220
+ microOp,
221
+ irOperandCount
222
+ } of subOps) {
223
+ const subOperands = operands.slice(offset, offset + irOperandCount);
224
+ offset += irOperandCount;
225
+ const newInstr = [microOp, ...subOperands];
226
+ // Carry source-node info on the first sub-instruction
227
+ if (offset === irOperandCount) {
228
+ newInstr[SOURCE_NODE_SYM] = instr[SOURCE_NODE_SYM];
229
+ }
230
+ result.push(newInstr);
231
+ }
232
+ }
233
+ return {
234
+ bytecode: result
235
+ };
236
+ }
@@ -18,8 +18,7 @@ function concealString(s, key) {
18
18
  return Buffer.from(bytes).toString("base64");
19
19
  }
20
20
 
21
- // Resolve all {type:"constant", value} operands to a PAIR of integer operands:
22
- // [constPoolIndex, concealKey]
21
+ // Resolve all {type:"constant", value} (index) and {type:"constant", value, key: true} (key) operands
23
22
  //
24
23
  // constPoolIndex — index into the constants array (as before).
25
24
  // concealKey — XOR key used to conceal this constant.
@@ -36,7 +35,6 @@ export function resolveConstants(bc, compiler) {
36
35
  const keyMap = new Map(); // pool index → conceal key
37
36
 
38
37
  function intern(operand) {
39
- const operandAsObject = typeof operand === "object" && operand ? operand : {};
40
38
  const value = operand.value;
41
39
  let idx = constantsMap.get(value);
42
40
  let key = 0;
@@ -62,12 +60,10 @@ export function resolveConstants(bc, compiler) {
62
60
  key = keyMap.get(idx);
63
61
  }
64
62
  const idxOperand = {
65
- ...operandAsObject,
66
63
  type: "number",
67
64
  resolvedValue: idx
68
65
  };
69
66
  const keyOperand = {
70
- ...operandAsObject,
71
67
  type: "number",
72
68
  resolvedValue: key
73
69
  };
@@ -81,17 +77,15 @@ export function resolveConstants(bc, compiler) {
81
77
  const hasConstant = operands.some(o => o !== undefined && o !== null && typeof o === "object" && o.type === "constant");
82
78
  if (hasConstant) {
83
79
  // 1-to-2 expansion: each {type:"constant"} becomes [constIdx, concealKey].
84
- const newOperands = [];
85
- for (const operand of operands) {
80
+ const newOperands = operands.map(operand => {
86
81
  if (operand?.type === "constant") {
87
82
  const [idxOperand, key] = intern(operand);
88
83
  const newOperand = operand?.key ? key : idxOperand;
89
- newOperands.push(newOperand);
90
- // newOperands.push(key); // plain number — serialized as a regular u16 slot
84
+ return Object.assign(operand, newOperand);
91
85
  } else {
92
- newOperands.push(operand);
86
+ return operand;
93
87
  }
94
- }
88
+ });
95
89
  const newInstr = [op, ...newOperands];
96
90
  newInstr[SOURCE_NODE_SYM] = instr[SOURCE_NODE_SYM];
97
91
  resolved.push(newInstr);
@@ -54,13 +54,15 @@ export function resolveLabels(bc, compiler) {
54
54
  if (operand !== undefined && operand !== null && typeof operand === "object" && operand.type === "label") {
55
55
  const pc = labelToPc.get(operand.label);
56
56
  if (pc === undefined) throw new Error(`Undefined label: ${operand.label}`);
57
- var operandAsObject = typeof operand === "object" && operand ? operand : {};
58
57
  const newOperand = {
59
- ...operandAsObject,
60
- // Preverse original operand properties
61
58
  type: "number",
62
59
  resolvedValue: pc + (operand.offset ?? 0)
63
60
  };
61
+
62
+ // Mutate original object so that references are also updated
63
+ if (typeof operand === "object" && operand !== null) {
64
+ return Object.assign(operand, newOperand);
65
+ }
64
66
  return newOperand;
65
67
  }
66
68
  return operand;
@@ -1,5 +1,6 @@
1
1
  import { SOURCE_NODE_SYM } from "../../compiler.js";
2
- import { getInstructionSize, nextFreeSlot, U16_MAX } from "../../utils/op-utils.js";
2
+ import { getInstructionSize, nextFreeSlot } from "../../utils/op-utils.js";
3
+ export const nSizedOps = ["MAKE_CLOSURE", "BUILD_ARRAY", "BUILD_OBJECT", "CALL", "CALL_METHOD", "NEW"];
3
4
 
4
5
  // Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
5
6
  // Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
@@ -7,13 +8,7 @@ import { getInstructionSize, nextFreeSlot, U16_MAX } from "../../utils/op-utils.
7
8
  // MAKE_CLOSURE and other N-sized instructions cannot be specialized
8
9
  // Runs after selfModifying but before resolveLabels (operands stay plain numbers).
9
10
  export function specializedOpcodes(bc, compiler) {
10
- const disallowedOps = new Set([compiler.OP.MAKE_CLOSURE, compiler.OP.BUILD_ARRAY, compiler.OP.BUILD_OBJECT, compiler.OP.CALL, compiler.OP.CALL_METHOD, compiler.OP.NEW]);
11
-
12
- // ── Collect used opcodes exactly as specified ─────────────────────────────
13
- const usedOpcodes = new Set(Object.keys(compiler.OP_NAME).map(k => parseInt(k, 10)).filter(v => !isNaN(v)));
14
- if (usedOpcodes.size > U16_MAX) return {
15
- bytecode: bc
16
- };
11
+ const disallowedOps = new Set(nSizedOps.map(name => compiler.OP[name]));
17
12
 
18
13
  // ── Step 1: count frequency of eligible (op, operand) pairs ───────────────
19
14
  const freqMap = new Map();
@@ -24,7 +19,21 @@ export function specializedOpcodes(bc, compiler) {
24
19
  // Only supports between 1-6 operands
25
20
  const operandCount = getInstructionSize(instr) - 1;
26
21
  if (operandCount < 1 || operandCount > 6) continue;
27
- const operands = instr.slice(1);
22
+
23
+ // Convert numbers into operand objects so they can be modified elsewhere and preserved
24
+ const oldOperands = instr.slice(1);
25
+ const operands = oldOperands.map(operand => {
26
+ if (typeof operand === "number") {
27
+ return {
28
+ type: "number",
29
+ value: operand,
30
+ resolvedValue: operand
31
+ };
32
+ }
33
+ return operand;
34
+ });
35
+ instr.length = 1;
36
+ instr.push(...operands);
28
37
  const operandsKey = JSON.stringify(operands);
29
38
  const key = `${op},${operandsKey}`;
30
39
  const entry = freqMap.get(key);
@@ -50,7 +59,7 @@ export function specializedOpcodes(bc, compiler) {
50
59
  const sigToSpecial = new Map();
51
60
  const specializedOps = {};
52
61
  for (let i = 0; i < candidates.length; i++) {
53
- const specialOp = nextFreeSlot(usedOpcodes);
62
+ const specialOp = nextFreeSlot(compiler);
54
63
  if (specialOp === -1) break;
55
64
  const {
56
65
  op: originalOp,
@@ -92,14 +101,10 @@ export function specializedOpcodes(bc, compiler) {
92
101
  const newOperands = operands.map(operand => {
93
102
  const operandAsObject = typeof operand === "object" && operand ? operand : {
94
103
  type: "number",
95
- value: operand,
96
104
  resolvedValue: operand
97
105
  };
98
- const newOperand = {
99
- ...operandAsObject,
100
- placeholder: true
101
- };
102
- return newOperand;
106
+ operandAsObject.placeholder = true;
107
+ return operandAsObject;
103
108
  });
104
109
  const newInstr = [specialOpCode, ...newOperands];
105
110
 
@@ -0,0 +1,202 @@
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 * as t from "@babel/types";
21
+ import traverseImport from "@babel/traverse";
22
+ import { ok } from "assert";
23
+ import { getRandomInt } from "../../utils/random-utils.js";
24
+ import { U16_MAX } from "../../utils/op-utils.js";
25
+ const traverse = traverseImport.default || traverseImport;
26
+ export function makeInternalsAccess(index) {
27
+ return t.memberExpression(t.memberExpression(t.thisExpression(), t.identifier("_internals")), t.numericLiteral(index), true // computed
28
+ );
29
+ }
30
+ function collectUsedIndices(compiler) {
31
+ const used = new Set();
32
+ for (const v of compiler._internals.globally.values()) used.add(v);
33
+ for (const opMap of compiler._internals.opcodes.values()) {
34
+ for (const v of opMap.values()) used.add(v);
35
+ }
36
+ return used;
37
+ }
38
+
39
+ // Assign or look up the _internals slot index for a variable name within a
40
+ // specific opcode handler.
41
+ //
42
+ // _internals.opcodes[currentOpcode] is the source of truth for this opcode.
43
+ // _internals.globally holds the shared pool written on first sight.
44
+ //
45
+ // randomizeOpcodes OFF → always reuse / create in globally, mirror to opcodes.
46
+ // randomizeOpcodes ON → first time a name is seen: create global slot.
47
+ // subsequent opcodes: 50% reuse global, 50% create an
48
+ // opcode-specific random slot (NOT written to globally).
49
+ function assignInternalsIndex(name, compiler, currentOpcode) {
50
+ // Ensure per-opcode map exists
51
+ let opcodeMap = compiler._internals.opcodes.get(currentOpcode);
52
+ if (!opcodeMap) {
53
+ opcodeMap = new Map();
54
+ compiler._internals.opcodes.set(currentOpcode, opcodeMap);
55
+ }
56
+
57
+ // Already registered for this opcode — return immediately
58
+ const existing = opcodeMap.get(name);
59
+ if (existing !== undefined) return existing;
60
+ const globalIndex = compiler._internals.globally.get(name);
61
+ let index;
62
+ if (!compiler.options.randomizeOpcodes) {
63
+ // Non-random: always share the global sequential slot
64
+ if (globalIndex === undefined) {
65
+ index = compiler._internals.globally.size;
66
+ compiler._internals.globally.set(name, index);
67
+ } else {
68
+ index = globalIndex;
69
+ }
70
+ } else if (globalIndex === undefined) {
71
+ // First opcode to declare this variable — establish the global slot
72
+ const used = collectUsedIndices(compiler);
73
+ let candidate;
74
+ do {
75
+ candidate = getRandomInt(0, U16_MAX);
76
+ } while (used.has(candidate));
77
+ index = candidate;
78
+ compiler._internals.globally.set(name, index);
79
+ } else {
80
+ // Already in global: 50% chance to reuse, 50% opcode-specific new slot
81
+ if (Math.random() < 0.5) {
82
+ index = globalIndex;
83
+ } else {
84
+ const used = collectUsedIndices(compiler);
85
+ let candidate;
86
+ do {
87
+ candidate = getRandomInt(0, U16_MAX);
88
+ } while (used.has(candidate));
89
+ index = candidate;
90
+ // Intentionally NOT written to globally — this slot is opcode-specific
91
+ }
92
+ }
93
+ opcodeMap.set(name, index);
94
+ return index;
95
+ }
96
+ export function applyInternalVariablesToSwitchCase(node, compiler, currentOpcode) {
97
+ // Work with the actual body array (block body or flat consequent)
98
+ let bodyArr;
99
+ if (node.consequent.length === 1 && t.isBlockStatement(node.consequent[0])) {
100
+ bodyArr = node.consequent[0].body;
101
+ } else {
102
+ bodyArr = node.consequent;
103
+ }
104
+
105
+ // Single traversal: declarations and references handled in one pass.
106
+ //
107
+ // Declaration (Identifier is VariableDeclarator.id):
108
+ // → register/look-up slot, replace entire VariableDeclaration with
109
+ // AssignmentExpression (bare for ForStatement.init, else ExpressionStatement).
110
+ //
111
+ // Reference (any other Identifier):
112
+ // → look up opcodes[currentOpcode] (source of truth) and replace if found.
113
+ // This handles cross-statement refs produced by micro-opcode splitting.
114
+ const syntheticFile = t.file(t.program(bodyArr));
115
+ const illegalNames = new Set(); // Nested closure names are skipped
116
+
117
+ traverse(syntheticFile, {
118
+ Identifier(path) {
119
+ const name = path.node.name;
120
+ if (illegalNames.has(name)) return;
121
+
122
+ // Skip non-computed property names: obj.name
123
+ if (t.isMemberExpression(path.parent) && !path.parent.computed && path.parent.property === path.node) {
124
+ return;
125
+ }
126
+
127
+ // Skip non-computed object-property keys: { name: value }
128
+ if (t.isObjectProperty(path.parent) && !path.parent.computed && path.parent.key === path.node) {
129
+ return;
130
+ }
131
+
132
+ // Don't descend into nested function scopes
133
+ if (path.find(p => p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression())) {
134
+ return;
135
+ }
136
+
137
+ // ── Declaration binding ──────────────────────────────────────────────
138
+ if (t.isVariableDeclarator(path.parent) && path.parent.id === path.node) {
139
+ // Verify it's not referenced in nested closure (illegal)
140
+ const binding = path.scope.getBinding(name);
141
+ if (binding?.referencePaths.some(rp => rp.findParent(p => p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression()))) {
142
+ illegalNames.add(name);
143
+ return;
144
+ }
145
+ const index = assignInternalsIndex(name, compiler, currentOpcode);
146
+ const init = path.parent.init;
147
+ const assignment = t.assignmentExpression("=", makeInternalsAccess(index), init ?? t.identifier("undefined"));
148
+
149
+ // Two levels up: VariableDeclarator → VariableDeclaration
150
+ const varDeclPath = path.parentPath.parentPath;
151
+ if (t.isForStatement(varDeclPath.parent) && varDeclPath.parent.init === varDeclPath.node) {
152
+ // ForStatement.init accepts an Expression directly
153
+ varDeclPath.replaceWith(assignment);
154
+ } else {
155
+ varDeclPath.replaceWith(t.expressionStatement(assignment));
156
+ }
157
+ return;
158
+ }
159
+
160
+ // ── Reference ───────────────────────────────────────────────────────
161
+ // Source of truth for this opcode is its own per-opcode map
162
+ const opcodeMap = compiler._internals.opcodes.get(currentOpcode);
163
+ const index = opcodeMap?.get(name);
164
+ if (index !== undefined) {
165
+ path.replaceWith(makeInternalsAccess(index));
166
+ path.skip();
167
+ }
168
+ }
169
+ });
170
+ }
171
+
172
+ // This takes the AST and finds the runtime switch statement via the leading
173
+ // comment "@SWITCH" then applies the above transformation to each switch case.
174
+ export function applyInteralVariablesToRuntime(ast, compiler) {
175
+ let switchStatement = null;
176
+ traverse(ast, {
177
+ SwitchStatement(path) {
178
+ if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
179
+ switchStatement = path.node;
180
+ path.stop();
181
+ }
182
+ }
183
+ });
184
+ ok(switchStatement, "Could not find @SWITCH statement for internal variables");
185
+ for (const sc of switchStatement.cases) {
186
+ const test = sc.test;
187
+ let currentOpcode = null;
188
+ if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
189
+ name: "OP"
190
+ }) && t.isIdentifier(test.property)) {
191
+ // case OP.LOAD_CONST: → resolve via compiler.OP
192
+ const opName = test.property.name;
193
+ const val = compiler.OP[opName];
194
+ if (val !== undefined) currentOpcode = val;
195
+ } else if (test && t.isNumericLiteral(test)) {
196
+ // Already a numeric literal (e.g. generated micro-opcode cases)
197
+ currentOpcode = test.value;
198
+ }
199
+ if (currentOpcode === null) continue;
200
+ applyInternalVariablesToSwitchCase(sc, compiler, currentOpcode);
201
+ }
202
+ }
@@ -17,6 +17,29 @@ function extractCaseBody(switchCase) {
17
17
  }
18
18
  return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
19
19
  }
20
+ export function getOpcodeToCaseMap(switchStatement, compiler) {
21
+ // Build a map opName → SwitchCase from the existing OP.xxx case tests.
22
+ const opcodeToCaseMap = new Map();
23
+ for (const sc of switchStatement.cases) {
24
+ const test = sc.test;
25
+ if (!test) continue;
26
+ let opcode;
27
+ let opName;
28
+ if (t.isMemberExpression(test) && t.isIdentifier(test.object, {
29
+ name: "OP"
30
+ }) && t.isIdentifier(test.property)) {
31
+ opName = test.property.name;
32
+ opcode = +Object.keys(compiler.OP_NAME).find(key => compiler.OP_NAME[key] == opName);
33
+ } else if (t.isNumericLiteral(test)) {
34
+ opcode = test.value;
35
+ }
36
+ ok(typeof opcode === "number" && !Number.isNaN(opcode), `Failed to parse ${opcode} from ${opName}`);
37
+ if (opcode !== undefined) {
38
+ opcodeToCaseMap.set(opcode, sc);
39
+ }
40
+ }
41
+ return opcodeToCaseMap;
42
+ }
20
43
 
21
44
  // Append a generated switch case for every entry in compiler.MACRO_OPS.
22
45
  // Each case inlines the constituent case bodies directly — no operand stack,
@@ -35,17 +58,7 @@ export function applyMacroOpcodes(ast, compiler) {
35
58
  }
36
59
  });
37
60
  ok(switchStatement, "Could not find @SWITCH statement for macro opcodes");
38
-
39
- // Build a map opName → SwitchCase from the existing OP.xxx case tests.
40
- const nameToCaseMap = new Map();
41
- for (const sc of switchStatement.cases) {
42
- const test = sc.test;
43
- if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
44
- name: "OP"
45
- }) && t.isIdentifier(test.property)) {
46
- nameToCaseMap.set(test.property.name, sc);
47
- }
48
- }
61
+ const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
49
62
  for (const [macroOpStr, constituentOps] of Object.entries(compiler.MACRO_OPS)) {
50
63
  const macroOpCode = Number(macroOpStr);
51
64
  const N = constituentOps.length;
@@ -54,20 +67,19 @@ export function applyMacroOpcodes(ast, compiler) {
54
67
  const constituentCases = [];
55
68
  let allFound = true;
56
69
  for (const opVal of constituentOps) {
57
- const opName = compiler.OP_NAME[opVal];
58
- if (!opName) {
59
- allFound = false;
60
- break;
61
- }
62
- const found = nameToCaseMap.get(opName);
70
+ const found = opcodeToCaseMap.get(opVal);
63
71
  if (!found) {
64
72
  allFound = false;
65
73
  break;
66
74
  }
67
75
  constituentCases.push(found);
68
76
  }
69
- if (!allFound) continue;
77
+ if (!allFound) {
78
+ throw new Error(`Could not find all constituent ops for macro op ${macroOpCode}`);
79
+ }
70
80
  const opNames = constituentOps.map(v => compiler.OP_NAME[v] ?? `OP_${v}`);
81
+ let newName = opNames.join(",");
82
+ compiler.OP_NAME[macroOpCode] = newName;
71
83
 
72
84
  // ── Build the macro case body ──────────────────────────────────────────
73
85
  // Clone and inline each sub-instruction's case body directly.