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.
- package/CHANGELOG.md +55 -0
- package/README.MD +101 -37
- package/dist/build-runtime.js +15 -2
- package/dist/compiler.js +98 -51
- package/dist/runtime.js +5 -1
- package/dist/transforms/bytecode/aliasedOpcodes.js +2 -8
- package/dist/transforms/bytecode/macroOpcodes.js +21 -19
- package/dist/transforms/bytecode/microOpcodes.js +236 -0
- package/dist/transforms/bytecode/resolveContants.js +5 -11
- package/dist/transforms/bytecode/resolveLabels.js +5 -3
- package/dist/transforms/bytecode/specializedOpcodes.js +21 -16
- package/dist/transforms/runtime/internalVariables.js +202 -0
- package/dist/transforms/runtime/macroOpcodes.js +30 -18
- package/dist/transforms/runtime/microOpcodes.js +76 -0
- package/dist/transforms/runtime/specializedOpcodes.js +20 -18
- package/dist/utils/op-utils.js +15 -8
- package/index.ts +3 -2
- package/jest.config.js +2 -0
- package/package.json +1 -1
- package/src/build-runtime.ts +18 -3
- package/src/compiler.ts +152 -65
- package/src/options.ts +1 -0
- package/src/runtime.ts +5 -1
- package/src/transforms/bytecode/aliasedOpcodes.ts +2 -12
- package/src/transforms/bytecode/macroOpcodes.ts +28 -29
- package/src/transforms/bytecode/microOpcodes.ts +291 -0
- package/src/transforms/bytecode/resolveContants.ts +6 -13
- package/src/transforms/bytecode/resolveLabels.ts +5 -4
- package/src/transforms/bytecode/specializedOpcodes.ts +38 -28
- package/src/transforms/runtime/internalVariables.ts +270 -0
- package/src/transforms/runtime/macroOpcodes.ts +47 -20
- package/src/transforms/runtime/microOpcodes.ts +93 -0
- package/src/transforms/runtime/specializedOpcodes.ts +27 -32
- package/src/types.ts +1 -1
- package/src/utils/op-utils.ts +21 -8
- package/src/utilts.ts +0 -3
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as t from "@babel/types";
|
|
|
2
2
|
import traverseImport from "@babel/traverse";
|
|
3
3
|
import { ok } from "assert";
|
|
4
4
|
import { Compiler } from "../../compiler.ts";
|
|
5
|
+
import generate from "@babel/generator";
|
|
5
6
|
const traverse = (traverseImport.default ||
|
|
6
7
|
traverseImport) as typeof traverseImport.default;
|
|
7
8
|
|
|
@@ -23,6 +24,43 @@ function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
|
23
24
|
return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export function getOpcodeToCaseMap(
|
|
28
|
+
switchStatement: t.SwitchStatement,
|
|
29
|
+
compiler: Compiler,
|
|
30
|
+
): Map<number, t.SwitchCase> {
|
|
31
|
+
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
32
|
+
const opcodeToCaseMap = new Map<number, t.SwitchCase>();
|
|
33
|
+
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
34
|
+
const test = sc.test;
|
|
35
|
+
if (!test) continue;
|
|
36
|
+
|
|
37
|
+
let opcode;
|
|
38
|
+
let opName;
|
|
39
|
+
if (
|
|
40
|
+
t.isMemberExpression(test) &&
|
|
41
|
+
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
42
|
+
t.isIdentifier(test.property)
|
|
43
|
+
) {
|
|
44
|
+
opName = test.property.name;
|
|
45
|
+
opcode = +Object.keys(compiler.OP_NAME).find(
|
|
46
|
+
(key) => compiler.OP_NAME[key] == opName,
|
|
47
|
+
);
|
|
48
|
+
} else if (t.isNumericLiteral(test)) {
|
|
49
|
+
opcode = test.value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ok(
|
|
53
|
+
typeof opcode === "number" && !Number.isNaN(opcode),
|
|
54
|
+
`Failed to parse ${opcode} from ${opName}`,
|
|
55
|
+
);
|
|
56
|
+
if (opcode !== undefined) {
|
|
57
|
+
opcodeToCaseMap.set(opcode, sc);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return opcodeToCaseMap;
|
|
62
|
+
}
|
|
63
|
+
|
|
26
64
|
// Append a generated switch case for every entry in compiler.MACRO_OPS.
|
|
27
65
|
// Each case inlines the constituent case bodies directly — no operand stack,
|
|
28
66
|
// no substitution needed. Because every opcode handler now reads its own
|
|
@@ -42,19 +80,7 @@ export function applyMacroOpcodes(ast: t.File, compiler: Compiler): void {
|
|
|
42
80
|
|
|
43
81
|
ok(switchStatement, "Could not find @SWITCH statement for macro opcodes");
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
const nameToCaseMap = new Map<string, t.SwitchCase>();
|
|
47
|
-
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
48
|
-
const test = sc.test;
|
|
49
|
-
if (
|
|
50
|
-
test &&
|
|
51
|
-
t.isMemberExpression(test) &&
|
|
52
|
-
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
53
|
-
t.isIdentifier(test.property)
|
|
54
|
-
) {
|
|
55
|
-
nameToCaseMap.set((test.property as t.Identifier).name, sc);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
83
|
+
const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
|
|
58
84
|
|
|
59
85
|
for (const [macroOpStr, constituentOps] of Object.entries(
|
|
60
86
|
compiler.MACRO_OPS,
|
|
@@ -66,21 +92,22 @@ export function applyMacroOpcodes(ast: t.File, compiler: Compiler): void {
|
|
|
66
92
|
const constituentCases: t.SwitchCase[] = [];
|
|
67
93
|
let allFound = true;
|
|
68
94
|
for (const opVal of constituentOps) {
|
|
69
|
-
const
|
|
70
|
-
if (!opName) {
|
|
71
|
-
allFound = false;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
const found = nameToCaseMap.get(opName);
|
|
95
|
+
const found = opcodeToCaseMap.get(opVal);
|
|
75
96
|
if (!found) {
|
|
76
97
|
allFound = false;
|
|
77
98
|
break;
|
|
78
99
|
}
|
|
79
100
|
constituentCases.push(found);
|
|
80
101
|
}
|
|
81
|
-
if (!allFound)
|
|
102
|
+
if (!allFound) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Could not find all constituent ops for macro op ${macroOpCode}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
82
107
|
|
|
83
108
|
const opNames = constituentOps.map((v) => compiler.OP_NAME[v] ?? `OP_${v}`);
|
|
109
|
+
let newName = opNames.join(",");
|
|
110
|
+
compiler.OP_NAME[macroOpCode] = newName;
|
|
84
111
|
|
|
85
112
|
// ── Build the macro case body ──────────────────────────────────────────
|
|
86
113
|
// Clone and inline each sub-instruction's case body directly.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
import { Compiler } from "../../compiler.ts";
|
|
5
|
+
import { applyInternalVariablesToSwitchCase } from "./internalVariables.ts";
|
|
6
|
+
|
|
7
|
+
const traverse = (traverseImport.default ||
|
|
8
|
+
traverseImport) as typeof traverseImport.default;
|
|
9
|
+
|
|
10
|
+
// Extract the real statement list from a SwitchCase consequent.
|
|
11
|
+
function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
12
|
+
let stmts: t.Statement[];
|
|
13
|
+
if (
|
|
14
|
+
switchCase.consequent.length === 1 &&
|
|
15
|
+
t.isBlockStatement(switchCase.consequent[0])
|
|
16
|
+
) {
|
|
17
|
+
stmts = (switchCase.consequent[0] as t.BlockStatement).body;
|
|
18
|
+
} else {
|
|
19
|
+
stmts = switchCase.consequent as t.Statement[];
|
|
20
|
+
}
|
|
21
|
+
return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Append a generated switch case for every entry in compiler.MICRO_OPS.
|
|
25
|
+
// applyInteralVariablesToRuntime must run before this so that the source
|
|
26
|
+
// case bodies are already using this._internals[index] instead of local vars.
|
|
27
|
+
// Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
|
|
28
|
+
export function applyMicroOpcodes(ast: t.File, compiler: Compiler): void {
|
|
29
|
+
if (!compiler.MICRO_OPS || Object.keys(compiler.MICRO_OPS).length === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let switchStatement: t.SwitchStatement | null = null;
|
|
34
|
+
traverse(ast, {
|
|
35
|
+
SwitchStatement(path) {
|
|
36
|
+
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
37
|
+
switchStatement = path.node;
|
|
38
|
+
path.stop();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
ok(switchStatement, "Could not find @SWITCH statement for micro opcodes");
|
|
44
|
+
|
|
45
|
+
// Build opName → SwitchCase from existing cases.
|
|
46
|
+
const nameToCaseMap = new Map<string, t.SwitchCase>();
|
|
47
|
+
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
48
|
+
const test = sc.test;
|
|
49
|
+
if (
|
|
50
|
+
test &&
|
|
51
|
+
t.isMemberExpression(test) &&
|
|
52
|
+
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
53
|
+
t.isIdentifier(test.property)
|
|
54
|
+
) {
|
|
55
|
+
nameToCaseMap.set((test.property as t.Identifier).name, sc);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const [microOpStr, info] of Object.entries(compiler.MICRO_OPS)) {
|
|
60
|
+
const microOpCode = Number(microOpStr);
|
|
61
|
+
const { originalOp, stmtIndex } = info;
|
|
62
|
+
|
|
63
|
+
const originalName = compiler.OP_NAME[originalOp];
|
|
64
|
+
if (!originalName) continue;
|
|
65
|
+
|
|
66
|
+
const originalCase = nameToCaseMap.get(originalName);
|
|
67
|
+
if (!originalCase) continue;
|
|
68
|
+
|
|
69
|
+
// Extract and clone all non-break statements from the original case body.
|
|
70
|
+
const allStmts = extractCaseBody(originalCase);
|
|
71
|
+
if (stmtIndex >= allStmts.length) continue;
|
|
72
|
+
|
|
73
|
+
const rawStmt = t.cloneNode(allStmts[stmtIndex], true) as t.Statement;
|
|
74
|
+
|
|
75
|
+
const newCase = t.switchCase(t.numericLiteral(microOpCode), [
|
|
76
|
+
t.blockStatement([rawStmt, t.breakStatement()]),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
// Apply internal-variable substitution — this may replace rawStmt in the
|
|
80
|
+
// block body (var decl → assignment), so add the comment afterwards on
|
|
81
|
+
// whatever the first statement of the block actually is.
|
|
82
|
+
applyInternalVariablesToSwitchCase(newCase, compiler, microOpCode);
|
|
83
|
+
|
|
84
|
+
const blockBody = (newCase.consequent[0] as t.BlockStatement).body;
|
|
85
|
+
const firstStmt = blockBody[0];
|
|
86
|
+
if (firstStmt) {
|
|
87
|
+
const microName = compiler.OP_NAME[microOpCode] ?? `MICRO_${microOpCode}`;
|
|
88
|
+
t.addComment(firstStmt, "leading", ` ${microName}`, true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
(switchStatement as t.SwitchStatement).cases.push(newCase);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -2,7 +2,7 @@ import * as t from "@babel/types";
|
|
|
2
2
|
import traverseImport from "@babel/traverse";
|
|
3
3
|
import { ok } from "assert";
|
|
4
4
|
import { Compiler } from "../../compiler.ts";
|
|
5
|
-
import
|
|
5
|
+
import { getOpcodeToCaseMap } from "./macroOpcodes.ts";
|
|
6
6
|
|
|
7
7
|
const traverse = (traverseImport.default ||
|
|
8
8
|
traverseImport) as typeof traverseImport.default;
|
|
@@ -33,6 +33,14 @@ function inlineFixedOperands(
|
|
|
33
33
|
// Wrap the statements in a temporary BlockStatement so traverse has a root.
|
|
34
34
|
// The replacement mutates the original statement objects in place.
|
|
35
35
|
var replaced = 0;
|
|
36
|
+
function consumeOperand() {
|
|
37
|
+
const resolvedValue = resolvedValues[replaced++];
|
|
38
|
+
ok(
|
|
39
|
+
typeof resolvedValue === "number",
|
|
40
|
+
`Expected a numeric operand value, got ${resolvedValue}`,
|
|
41
|
+
);
|
|
42
|
+
return t.numericLiteral(resolvedValue);
|
|
43
|
+
}
|
|
36
44
|
|
|
37
45
|
traverse(t.blockStatement(bodyStmts), {
|
|
38
46
|
noScope: true,
|
|
@@ -49,14 +57,11 @@ function inlineFixedOperands(
|
|
|
49
57
|
};
|
|
50
58
|
|
|
51
59
|
if (isMethodCall("_operand")) {
|
|
52
|
-
path.replaceWith(
|
|
60
|
+
path.replaceWith(consumeOperand());
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
if (isMethodCall("_constant")) {
|
|
56
|
-
path.node.arguments = [
|
|
57
|
-
t.numericLiteral(resolvedValues[replaced++]),
|
|
58
|
-
t.numericLiteral(resolvedValues[replaced++]),
|
|
59
|
-
];
|
|
64
|
+
path.node.arguments = [consumeOperand(), consumeOperand()];
|
|
60
65
|
}
|
|
61
66
|
},
|
|
62
67
|
});
|
|
@@ -72,11 +77,7 @@ function inlineFixedOperands(
|
|
|
72
77
|
// replaced by the constant integer that was captured at compile time.
|
|
73
78
|
// Must be called AFTER applyMacroOpcodes (so the original cases exist) but
|
|
74
79
|
// BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
|
|
75
|
-
export function applySpecializedOpcodes(
|
|
76
|
-
ast: t.File,
|
|
77
|
-
bytecode: b.Bytecode,
|
|
78
|
-
compiler: Compiler,
|
|
79
|
-
): void {
|
|
80
|
+
export function applySpecializedOpcodes(ast: t.File, compiler: Compiler): void {
|
|
80
81
|
let switchStatement: t.SwitchStatement | null = null;
|
|
81
82
|
traverse(ast, {
|
|
82
83
|
SwitchStatement(path) {
|
|
@@ -93,18 +94,7 @@ export function applySpecializedOpcodes(
|
|
|
93
94
|
);
|
|
94
95
|
|
|
95
96
|
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
96
|
-
const
|
|
97
|
-
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
98
|
-
const test = sc.test;
|
|
99
|
-
if (
|
|
100
|
-
test &&
|
|
101
|
-
t.isMemberExpression(test) &&
|
|
102
|
-
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
103
|
-
t.isIdentifier(test.property)
|
|
104
|
-
) {
|
|
105
|
-
nameToCaseMap.set((test.property as t.Identifier).name, sc);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
97
|
+
const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
|
|
108
98
|
|
|
109
99
|
if (!compiler.SPECIALIZED_OPS) return;
|
|
110
100
|
|
|
@@ -112,29 +102,34 @@ export function applySpecializedOpcodes(
|
|
|
112
102
|
const specialOpCode = Number(specialOpStr);
|
|
113
103
|
const { originalOp, operands } = info;
|
|
114
104
|
|
|
115
|
-
|
|
105
|
+
let newName = compiler.OP_NAME[specialOpCode];
|
|
116
106
|
const originalName = compiler.OP_NAME[originalOp];
|
|
117
|
-
|
|
107
|
+
const originalCase = opcodeToCaseMap.get(originalOp);
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
ok(
|
|
110
|
+
originalCase,
|
|
111
|
+
`Could not find original case for opcode ${originalName} (${originalOp})`,
|
|
112
|
+
);
|
|
121
113
|
|
|
122
114
|
// Clone the original handler body
|
|
123
115
|
const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
|
|
124
116
|
(s) => t.cloneNode(s, true) as t.Statement,
|
|
125
117
|
);
|
|
126
118
|
|
|
127
|
-
const placedOperands = info.
|
|
119
|
+
const placedOperands = info.operands;
|
|
128
120
|
ok(placedOperands, `Could not find operand for original opcode ${newName}`);
|
|
129
121
|
|
|
130
122
|
const resolvedValues = placedOperands.map((placedOperand) => {
|
|
131
123
|
return (placedOperand as any)?.resolvedValue ?? placedOperand;
|
|
132
124
|
});
|
|
133
125
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"Expected
|
|
137
|
-
|
|
126
|
+
if (resolvedValues.find((v) => typeof v !== "number")) {
|
|
127
|
+
console.error(info);
|
|
128
|
+
throw new Error("Expected all resolved operand values to be numbers");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
newName = `${originalName}_${resolvedValues.join("_")}`;
|
|
132
|
+
compiler.OP_NAME[specialOpCode] = newName;
|
|
138
133
|
|
|
139
134
|
// Replace this._operand() with the baked-in constant
|
|
140
135
|
inlineFixedOperands(bodyStmts, resolvedValues);
|
package/src/types.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// each operand as a separate u16 slot in the bytecode array.
|
|
9
9
|
export type InstrOperand =
|
|
10
10
|
| number
|
|
11
|
-
| Op<{ type: "number"; value
|
|
11
|
+
| Op<{ type: "number"; value?: number }>
|
|
12
12
|
| Op<{ type: "label"; label: string; offset?: number }>
|
|
13
13
|
| Op<{ type: "defineLabel"; label: string }>
|
|
14
14
|
| Op<{ type: "constant"; value: any }>;
|
package/src/utils/op-utils.ts
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { getRandomInt } from "./random-utils.ts";
|
|
2
2
|
import * as b from "../types.ts";
|
|
3
|
+
import { Compiler } from "../compiler.ts";
|
|
3
4
|
|
|
4
5
|
export const U16_MAX = 0xffff; // bytecode operands are u16
|
|
5
6
|
|
|
6
7
|
/** Returns the next free opcode slot, or -1 when the space is exhausted. */
|
|
7
|
-
export function nextFreeSlot(
|
|
8
|
+
export function nextFreeSlot(compiler: Compiler): number {
|
|
9
|
+
// ── Collect used opcodes exactly as specified ─────────────────────────────
|
|
10
|
+
const usedOpcodes = new Set<number>(
|
|
11
|
+
Object.keys(compiler.OP_NAME)
|
|
12
|
+
.map((k) => parseInt(k, 10))
|
|
13
|
+
.filter((v) => !isNaN(v)) as number[],
|
|
14
|
+
);
|
|
15
|
+
|
|
8
16
|
if (usedOpcodes.size > U16_MAX) return -1;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
|
|
18
|
+
// Random opcode
|
|
19
|
+
if (compiler.options.randomizeOpcodes) {
|
|
20
|
+
let attempts = 0;
|
|
21
|
+
while (attempts++ < 512) {
|
|
22
|
+
const candidate = getRandomInt(0, U16_MAX);
|
|
23
|
+
if (!usedOpcodes.has(candidate)) {
|
|
24
|
+
usedOpcodes.add(candidate);
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
}
|
|
29
|
+
|
|
17
30
|
// Fallback: linear scan from a random start
|
|
18
|
-
const start =
|
|
31
|
+
const start = Object.keys(compiler.OP_NAME).length;
|
|
19
32
|
for (let i = 0; i <= U16_MAX; i++) {
|
|
20
33
|
const v = (start + i) & U16_MAX;
|
|
21
34
|
if (!usedOpcodes.has(v)) {
|
package/src/utilts.ts
DELETED