js-confuser-vm 0.0.5 → 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 (47) hide show
  1. package/CHANGELOG.md +112 -2
  2. package/README.MD +249 -106
  3. package/dist/build-runtime.js +22 -3
  4. package/dist/compiler.js +864 -801
  5. package/dist/runtime.js +414 -333
  6. package/dist/transforms/bytecode/aliasedOpcodes.js +134 -0
  7. package/dist/transforms/bytecode/concealConstants.js +31 -0
  8. package/dist/transforms/bytecode/macroOpcodes.js +37 -23
  9. package/dist/transforms/bytecode/microOpcodes.js +236 -0
  10. package/dist/transforms/bytecode/resolveContants.js +69 -12
  11. package/dist/transforms/bytecode/resolveLabels.js +5 -3
  12. package/dist/transforms/bytecode/selfModifying.js +3 -2
  13. package/dist/transforms/bytecode/specializedOpcodes.js +54 -39
  14. package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
  15. package/dist/transforms/runtime/internalVariables.js +202 -0
  16. package/dist/transforms/runtime/macroOpcodes.js +30 -18
  17. package/dist/transforms/runtime/microOpcodes.js +76 -0
  18. package/dist/transforms/runtime/shuffleOpcodes.js +1 -1
  19. package/dist/transforms/runtime/specializedOpcodes.js +36 -29
  20. package/dist/utils/op-utils.js +36 -0
  21. package/dist/utils/random-utils.js +27 -0
  22. package/index.ts +11 -8
  23. package/jest.config.js +12 -0
  24. package/package.json +1 -1
  25. package/src/build-runtime.ts +25 -4
  26. package/src/compiler.ts +2482 -2069
  27. package/src/options.ts +3 -0
  28. package/src/runtime.ts +842 -771
  29. package/src/transforms/bytecode/aliasedOpcodes.ts +148 -0
  30. package/src/transforms/bytecode/concealConstants.ts +52 -0
  31. package/src/transforms/bytecode/macroOpcodes.ts +49 -33
  32. package/src/transforms/bytecode/microOpcodes.ts +291 -0
  33. package/src/transforms/bytecode/resolveContants.ts +82 -18
  34. package/src/transforms/bytecode/resolveLabels.ts +5 -4
  35. package/src/transforms/bytecode/selfModifying.ts +3 -3
  36. package/src/transforms/bytecode/specializedOpcodes.ts +85 -46
  37. package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
  38. package/src/transforms/runtime/internalVariables.ts +270 -0
  39. package/src/transforms/runtime/macroOpcodes.ts +47 -20
  40. package/src/transforms/runtime/microOpcodes.ts +93 -0
  41. package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
  42. package/src/transforms/runtime/specializedOpcodes.ts +56 -46
  43. package/src/types.ts +1 -1
  44. package/src/utils/op-utils.ts +46 -0
  45. package/src/transforms/utils/op-utils.ts +0 -26
  46. package/src/utilts.ts +0 -3
  47. /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
@@ -1,42 +1,56 @@
1
1
  import { SOURCE_NODE_SYM } from "../../compiler.js";
2
- import { 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].
6
7
  // Only instructions with *exactly one numeric operand* are considered.
7
- // MAKE_CLOSURE and any instruction with zero / multiple operands are skipped.
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
- // ── Collect used opcodes exactly as specified ─────────────────────────────
11
- const usedOpcodes = new Set(Object.keys(compiler.OP_NAME).map(k => parseInt(k, 10)).filter(v => !isNaN(v)));
12
- if (usedOpcodes.size > U16_MAX) return {
13
- bytecode: bc
14
- };
11
+ const disallowedOps = new Set(nSizedOps.map(name => compiler.OP[name]));
15
12
 
16
13
  // ── Step 1: count frequency of eligible (op, operand) pairs ───────────────
17
14
  const freqMap = new Map();
18
15
  for (const instr of bc) {
19
16
  const op = instr[0];
20
- if (op === null || op === compiler.OP.MAKE_CLOSURE) continue;
17
+ if (op === null || disallowedOps.has(op)) continue;
18
+
19
+ // Only supports between 1-6 operands
20
+ const operandCount = getInstructionSize(instr) - 1;
21
+ if (operandCount < 1 || operandCount > 6) continue;
21
22
 
22
- // Must have exactly one operand and it must be a plain number
23
- if (instr.length !== 2) continue;
24
- const operand = instr[1];
25
- const key = `${op},${operand}`;
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);
37
+ const operandsKey = JSON.stringify(operands);
38
+ const key = `${op},${operandsKey}`;
26
39
  const entry = freqMap.get(key);
27
40
  if (entry) {
28
- entry.count++;
41
+ entry.occurences++;
29
42
  } else {
30
43
  freqMap.set(key, {
31
44
  op,
32
- operand,
33
- count: 1
45
+ operands,
46
+ operandsKey,
47
+ occurences: 1
34
48
  });
35
49
  }
36
50
  }
37
51
 
38
52
  // ── Step 2: keep combinations that appear >= 2 times, sort by frequency ───
39
- const candidates = Array.from(freqMap.values()).filter(e => e.count >= 1).sort((a, b) => b.count - a.count);
53
+ const candidates = Array.from(freqMap.values()).filter(e => e.occurences >= 1).sort((a, b) => b.occurences - a.occurences);
40
54
  if (candidates.length === 0) return {
41
55
  bytecode: bc
42
56
  };
@@ -45,22 +59,23 @@ export function specializedOpcodes(bc, compiler) {
45
59
  const sigToSpecial = new Map();
46
60
  const specializedOps = {};
47
61
  for (let i = 0; i < candidates.length; i++) {
48
- const specialOp = nextFreeSlot(usedOpcodes);
62
+ const specialOp = nextFreeSlot(compiler);
49
63
  if (specialOp === -1) break;
50
64
  const {
51
65
  op: originalOp,
52
- operand
66
+ operands,
67
+ operandsKey
53
68
  } = candidates[i];
54
- const key = `${originalOp},${JSON.stringify(operand)}`;
69
+ const key = `${originalOp},${operandsKey}`;
55
70
  sigToSpecial.set(key, specialOp);
56
71
  specializedOps[specialOp] = {
57
72
  originalOp,
58
- operand
73
+ operands
59
74
  };
60
75
 
61
76
  // Register a human-readable name for disassembly / debugging
62
77
  const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
63
- compiler.OP_NAME[specialOp] = `${originalName}_${JSON.stringify(operand)}`;
78
+ compiler.OP_NAME[specialOp] = `${originalName}_${JSON.stringify(operandsKey)}`;
64
79
  }
65
80
 
66
81
  // Store mapping so the interpreter knows how to dispatch the specialized op
@@ -70,32 +85,32 @@ export function specializedOpcodes(bc, compiler) {
70
85
  const result = [];
71
86
  for (const instr of bc) {
72
87
  const op = instr[0];
73
- // Only consider instructions with exactly one numeric operand
74
- if (op === null || instr.length !== 2 || op === compiler.OP.MAKE_CLOSURE) {
88
+ // Only consider instructions with one or more operands
89
+ if (op === null || instr.length <= 1 || op === compiler.OP.MAKE_CLOSURE) {
90
+ result.push(instr);
91
+ continue;
92
+ }
93
+ const operands = instr.slice(1);
94
+ const operandsKey = JSON.stringify(operands);
95
+ const key = `${op},${operandsKey}`;
96
+ const specialOpCode = sigToSpecial.get(key);
97
+ if (!specialOpCode) {
75
98
  result.push(instr);
76
99
  continue;
77
100
  }
78
- const operand = instr[1];
79
- const key = `${op},${JSON.stringify(operand)}`;
80
- if (sigToSpecial.has(key)) {
81
- const specialOpCode = sigToSpecial.get(key);
101
+ const newOperands = operands.map(operand => {
82
102
  const operandAsObject = typeof operand === "object" && operand ? operand : {
83
103
  type: "number",
84
- value: operand,
85
104
  resolvedValue: operand
86
105
  };
87
- const newOperand = {
88
- ...operandAsObject,
89
- placeholder: true
90
- };
91
- const newInstr = [specialOpCode, newOperand];
106
+ operandAsObject.placeholder = true;
107
+ return operandAsObject;
108
+ });
109
+ const newInstr = [specialOpCode, ...newOperands];
92
110
 
93
- // Preserve source-node information for error reporting
94
- newInstr[SOURCE_NODE_SYM] = instr[SOURCE_NODE_SYM];
95
- result.push(newInstr);
96
- } else {
97
- result.push(instr);
98
- }
111
+ // Preserve source-node information for error reporting
112
+ newInstr[SOURCE_NODE_SYM] = instr[SOURCE_NODE_SYM];
113
+ result.push(newInstr);
99
114
  }
100
115
  return {
101
116
  bytecode: result
@@ -0,0 +1,134 @@
1
+ import * as t from "@babel/types";
2
+ import traverseImport from "@babel/traverse";
3
+ import { ok } from "assert";
4
+ const traverse = traverseImport.default || traverseImport;
5
+
6
+ // Extract the real statement list from a SwitchCase consequent.
7
+ function extractCaseBody(switchCase) {
8
+ let stmts;
9
+ if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
10
+ stmts = switchCase.consequent[0].body;
11
+ } else {
12
+ stmts = switchCase.consequent;
13
+ }
14
+ return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
15
+ }
16
+
17
+ // Replace every `this._operand()` call in bodyStmts with `_operands[i]`
18
+ // where i is the call's sequential index (0-based).
19
+ // Returns the number of replacements performed.
20
+ function replaceOperandCalls(bodyStmts) {
21
+ let replaced = 0;
22
+ traverse(t.blockStatement(bodyStmts), {
23
+ noScope: true,
24
+ CallExpression(path) {
25
+ const callee = path.node.callee;
26
+ const isMethodCall = methodName => {
27
+ return t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
28
+ name: methodName
29
+ }) && path.node.arguments.length === 0;
30
+ };
31
+
32
+ // Replace with _operands[i]
33
+ const createOperandAccess = () => {
34
+ return t.memberExpression(t.identifier("_operands"), t.numericLiteral(replaced++), true // computed
35
+ );
36
+ };
37
+ if (isMethodCall("_operand")) {
38
+ path.replaceWith(createOperandAccess());
39
+ }
40
+ if (isMethodCall("_constant")) {
41
+ path.node.arguments = [createOperandAccess(), createOperandAccess()];
42
+ }
43
+ }
44
+ });
45
+ return replaced;
46
+ }
47
+
48
+ // Appends a generated switch case for every entry in compiler.ALIASED_OPS.
49
+ // Each alias case:
50
+ // 1. Reads all operands eagerly into `_unsortedOperands` (in the shuffled
51
+ // bytecode order) via sequential this._operand() calls.
52
+ // 2. Restores the original operand order into `_operands` using the INVERSE
53
+ // of the stored `order` permutation:
54
+ // inverseOrder[order[i]] = i
55
+ // _operands[j] = _unsortedOperands[inverseOrder[j]]
56
+ // This is necessary because the bytecode stored originalOperands[order[i]]
57
+ // at slot i, so recovering originalOperands[j] requires the inverse lookup.
58
+ // 3. Executes a clone of the original handler body where every
59
+ // this._operand() has been replaced by the corresponding `_operands[i]`.
60
+ //
61
+ // Must run AFTER applyMacroOpcodes / applySpecializedOpcodes (so original
62
+ // cases already exist) but BEFORE applyShuffleOpcodes (so the new alias
63
+ // cases are also shuffled into the handler order).
64
+ export function applyAliasedOpcodes(ast, compiler) {
65
+ if (!compiler.ALIASED_OPS || Object.keys(compiler.ALIASED_OPS).length === 0) return;
66
+ let switchStatement = null;
67
+ traverse(ast, {
68
+ SwitchStatement(path) {
69
+ if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
70
+ switchStatement = path.node;
71
+ path.stop();
72
+ }
73
+ }
74
+ });
75
+ ok(switchStatement, "Could not find @SWITCH statement for aliased opcodes");
76
+
77
+ // Build opName → SwitchCase map from existing OP.xxx case tests.
78
+ const nameToCaseMap = new Map();
79
+ for (const sc of switchStatement.cases) {
80
+ const test = sc.test;
81
+ if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
82
+ name: "OP"
83
+ }) && t.isIdentifier(test.property)) {
84
+ nameToCaseMap.set(test.property.name, sc);
85
+ }
86
+ }
87
+ for (const [aliasOpStr, info] of Object.entries(compiler.ALIASED_OPS)) {
88
+ const aliasOpCode = Number(aliasOpStr);
89
+ const {
90
+ originalOp,
91
+ order
92
+ } = info;
93
+ const arity = order.length;
94
+ const originalName = compiler.OP_NAME[originalOp];
95
+ if (!originalName) continue;
96
+ const originalCase = nameToCaseMap.get(originalName);
97
+ if (!originalCase) continue;
98
+
99
+ // Clone the original handler body (deep clone so we don't mutate the source)
100
+ const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
101
+
102
+ // Replace this._operand() calls with _operands[i]
103
+ const replaced = replaceOperandCalls(bodyStmts);
104
+
105
+ // If the handler has a different number of _operand() calls than our
106
+ // recorded arity, skip this alias (variable-operand handler guard).
107
+ if (replaced !== arity) continue;
108
+
109
+ // Build: var _unsortedOperands = [this._operand(), this._operand(), ...]
110
+ // Reads operands in the NEW (shuffled) bytecode order.
111
+ const unsortedInit = t.variableDeclaration("let", [t.variableDeclarator(t.identifier("_unsortedOperands"), t.arrayExpression(Array.from({
112
+ length: arity
113
+ }, () => t.callExpression(t.memberExpression(t.thisExpression(), t.identifier("_operand")), []))))]);
114
+
115
+ // The inverse permutation maps original position j → unsorted index i,
116
+ // because the bytecode stored originalOperands[order[i]] at slot i.
117
+ // inverseOrder[j] = i means: original operand j lives at _unsortedOperands[i]
118
+ const inverseOrder = new Array(arity);
119
+ for (let i = 0; i < arity; i++) {
120
+ inverseOrder[order[i]] = i;
121
+ }
122
+
123
+ // Build: var _operands = [_unsortedOperands[inverseOrder[0]], ...]
124
+ // Restores the original operand order expected by the handler body.
125
+ const operandsInit = t.variableDeclaration("let", [t.variableDeclarator(t.identifier("_operands"), t.arrayExpression(inverseOrder.map(idx => t.memberExpression(t.identifier("_unsortedOperands"), t.numericLiteral(idx), true // computed
126
+ ))))]);
127
+ const allStmts = [unsortedInit, operandsInit, ...bodyStmts];
128
+
129
+ // Add a leading comment for readability in non-minified output
130
+ t.addComment(allStmts[0], "leading", ` ${compiler.OP_NAME[aliasOpCode]} (order: [${order.join(",")}])`, true);
131
+ allStmts.push(t.breakStatement());
132
+ switchStatement.cases.push(t.switchCase(t.numericLiteral(aliasOpCode), [t.blockStatement(allStmts)]));
133
+ }
134
+ }
@@ -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.
@@ -0,0 +1,76 @@
1
+ import * as t from "@babel/types";
2
+ import traverseImport from "@babel/traverse";
3
+ import { ok } from "assert";
4
+ import { applyInternalVariablesToSwitchCase } from "./internalVariables.js";
5
+ const traverse = traverseImport.default || traverseImport;
6
+
7
+ // Extract the real statement list from a SwitchCase consequent.
8
+ function extractCaseBody(switchCase) {
9
+ let stmts;
10
+ if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
11
+ stmts = switchCase.consequent[0].body;
12
+ } else {
13
+ stmts = switchCase.consequent;
14
+ }
15
+ return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
16
+ }
17
+
18
+ // Append a generated switch case for every entry in compiler.MICRO_OPS.
19
+ // applyInteralVariablesToRuntime must run before this so that the source
20
+ // case bodies are already using this._internals[index] instead of local vars.
21
+ // Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
22
+ export function applyMicroOpcodes(ast, compiler) {
23
+ if (!compiler.MICRO_OPS || Object.keys(compiler.MICRO_OPS).length === 0) {
24
+ return;
25
+ }
26
+ let switchStatement = null;
27
+ traverse(ast, {
28
+ SwitchStatement(path) {
29
+ if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
30
+ switchStatement = path.node;
31
+ path.stop();
32
+ }
33
+ }
34
+ });
35
+ ok(switchStatement, "Could not find @SWITCH statement for micro opcodes");
36
+
37
+ // Build opName → SwitchCase from existing cases.
38
+ const nameToCaseMap = new Map();
39
+ for (const sc of switchStatement.cases) {
40
+ const test = sc.test;
41
+ if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
42
+ name: "OP"
43
+ }) && t.isIdentifier(test.property)) {
44
+ nameToCaseMap.set(test.property.name, sc);
45
+ }
46
+ }
47
+ for (const [microOpStr, info] of Object.entries(compiler.MICRO_OPS)) {
48
+ const microOpCode = Number(microOpStr);
49
+ const {
50
+ originalOp,
51
+ stmtIndex
52
+ } = info;
53
+ const originalName = compiler.OP_NAME[originalOp];
54
+ if (!originalName) continue;
55
+ const originalCase = nameToCaseMap.get(originalName);
56
+ if (!originalCase) continue;
57
+
58
+ // Extract and clone all non-break statements from the original case body.
59
+ const allStmts = extractCaseBody(originalCase);
60
+ if (stmtIndex >= allStmts.length) continue;
61
+ const rawStmt = t.cloneNode(allStmts[stmtIndex], true);
62
+ const newCase = t.switchCase(t.numericLiteral(microOpCode), [t.blockStatement([rawStmt, t.breakStatement()])]);
63
+
64
+ // Apply internal-variable substitution — this may replace rawStmt in the
65
+ // block body (var decl → assignment), so add the comment afterwards on
66
+ // whatever the first statement of the block actually is.
67
+ applyInternalVariablesToSwitchCase(newCase, compiler, microOpCode);
68
+ const blockBody = newCase.consequent[0].body;
69
+ const firstStmt = blockBody[0];
70
+ if (firstStmt) {
71
+ const microName = compiler.OP_NAME[microOpCode] ?? `MICRO_${microOpCode}`;
72
+ t.addComment(firstStmt, "leading", ` ${microName}`, true);
73
+ }
74
+ switchStatement.cases.push(newCase);
75
+ }
76
+ }
@@ -1,6 +1,6 @@
1
1
  import traverseImport from "@babel/traverse";
2
2
  import { ok } from "assert";
3
- import { shuffle } from "../utils/random-utils.js";
3
+ import { shuffle } from "../../utils/random-utils.js";
4
4
  const traverse = traverseImport.default || traverseImport;
5
5
 
6
6
  // Randomly reorder the switch cases inside the @SWITCH statement so the