js-confuser-vm 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -89
- package/dist/compiler.js +583 -208
- package/dist/disassembler.js +58 -8
- package/dist/runtime.js +93 -74
- package/dist/template.js +81 -76
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +143 -25
- package/dist/transforms/bytecode/dispatcher.js +3 -3
- package/dist/transforms/bytecode/resolveRegisters.js +19 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/specializedOpcodes.js +6 -3
- package/dist/transforms/bytecode/stringConcealing.js +253 -75
- package/dist/utils/ast-utils.js +61 -0
- package/dist/utils/op-utils.js +1 -0
- package/package.json +7 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -358
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/bench.ts +0 -146
- package/disassemble.ts +0 -12
- package/index.ts +0 -43
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -64
- package/output.disassembled.js +0 -41
- package/src/build-runtime.ts +0 -113
- package/src/compiler.ts +0 -2703
- package/src/disassembler.ts +0 -329
- package/src/index.ts +0 -24
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -24
- package/src/runtime.ts +0 -956
- package/src/template.ts +0 -265
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -151
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/controlFlowFlattening.ts +0 -566
- package/src/transforms/bytecode/dispatcher.ts +0 -292
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/resolveConstants.ts +0 -126
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -226
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -164
- package/src/transforms/bytecode/stringConcealing.ts +0 -130
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/classObfuscation.ts +0 -59
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -161
- package/src/types.ts +0 -134
- package/src/utils/ast-utils.ts +0 -19
- package/src/utils/op-utils.ts +0 -46
- package/src/utils/pass-utils.ts +0 -126
- package/src/utils/profile-utils.ts +0 -3
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// String Concealing
|
|
2
|
-
//
|
|
3
|
-
// Encodes every string constant in each function with base64, then inserts a
|
|
4
|
-
// decode closure (atob) that is called immediately after each LOAD_CONST to
|
|
5
|
-
// recover the original value at runtime.
|
|
6
|
-
//
|
|
7
|
-
// ── How it works ─────────────────────────────────────────────────────────────
|
|
8
|
-
//
|
|
9
|
-
// Each function that contains at least one string LOAD_CONST gets:
|
|
10
|
-
//
|
|
11
|
-
// rClosure — a register holding the decode closure, created ONCE at function
|
|
12
|
-
// entry (hoisted). All decode calls within the function reuse it.
|
|
13
|
-
//
|
|
14
|
-
// The decode function is compiled ONCE (shared across all functions) from a
|
|
15
|
-
// Template:
|
|
16
|
-
//
|
|
17
|
-
// function decode(encoded) { return atob(encoded); }
|
|
18
|
-
//
|
|
19
|
-
// String constant transformations:
|
|
20
|
-
//
|
|
21
|
-
// Original: LOAD_CONST rDst, "hello"
|
|
22
|
-
// Becomes: LOAD_CONST rDst, "aGVsbG8=" (base64-encoded)
|
|
23
|
-
// CALL rDst, rClosure, 1, rDst (decode in-place)
|
|
24
|
-
//
|
|
25
|
-
// ── Pipeline position ─────────────────────────────────────────────────────────
|
|
26
|
-
// Runs BEFORE resolveRegisters and resolveLabels (same slot as Dispatcher/CFF).
|
|
27
|
-
|
|
28
|
-
import { Compiler } from "../../compiler.ts";
|
|
29
|
-
import { Template } from "../../template.ts";
|
|
30
|
-
import type { Bytecode, RegisterOperand } from "../../types.ts";
|
|
31
|
-
import * as b from "../../types.ts";
|
|
32
|
-
import { ref, buildMaxIdMap, allocReg, forEachFunction } from "../../utils/pass-utils.ts";
|
|
33
|
-
|
|
34
|
-
// ── Per-function transformation ──────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
function processFunctionBlock(
|
|
37
|
-
instrs: Bytecode,
|
|
38
|
-
fnId: number,
|
|
39
|
-
compiler: Compiler,
|
|
40
|
-
maxId: Map<number, number>,
|
|
41
|
-
decodeDesc: any,
|
|
42
|
-
): { instrs: Bytecode } {
|
|
43
|
-
const OP = compiler.OP;
|
|
44
|
-
|
|
45
|
-
// Only transform functions that contain string constants.
|
|
46
|
-
const hasStringConst = instrs.some((instr) => {
|
|
47
|
-
if (instr[0] !== OP.LOAD_CONST) return false;
|
|
48
|
-
const operands = instr.slice(1);
|
|
49
|
-
return (
|
|
50
|
-
operands.length === 2 &&
|
|
51
|
-
(operands[1] as any)?.type === "constant" &&
|
|
52
|
-
typeof (operands[1] as any).value === "string"
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
if (!hasStringConst) return { instrs };
|
|
56
|
-
|
|
57
|
-
const rClosure = allocReg(fnId, maxId);
|
|
58
|
-
const out: Bytecode = [];
|
|
59
|
-
|
|
60
|
-
// Hoist: create the decode closure once at function entry.
|
|
61
|
-
out.push([
|
|
62
|
-
OP.MAKE_CLOSURE!,
|
|
63
|
-
ref(rClosure),
|
|
64
|
-
{ type: "label", label: decodeDesc.entryLabel },
|
|
65
|
-
decodeDesc.paramCount, // 1 (encoded)
|
|
66
|
-
b.fnRegCountOperand(decodeDesc._fnIdx),
|
|
67
|
-
0, // no upvalues
|
|
68
|
-
0, // hasRest = false
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
// Transform each instruction.
|
|
72
|
-
for (const instr of instrs) {
|
|
73
|
-
if (
|
|
74
|
-
instr[0] === OP.LOAD_CONST &&
|
|
75
|
-
instr.length === 3 &&
|
|
76
|
-
(instr[2] as any)?.type === "constant" &&
|
|
77
|
-
typeof (instr[2] as any).value === "string"
|
|
78
|
-
) {
|
|
79
|
-
const dst = instr[1] as RegisterOperand;
|
|
80
|
-
const constOp = instr[2] as any;
|
|
81
|
-
|
|
82
|
-
// Encode the string in-place.
|
|
83
|
-
constOp.value = Buffer.from(constOp.value as string, "utf-8").toString(
|
|
84
|
-
"base64",
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
out.push(instr);
|
|
88
|
-
|
|
89
|
-
// Decode: rDst = decode(rDst)
|
|
90
|
-
out.push([
|
|
91
|
-
OP.CALL!,
|
|
92
|
-
ref(dst), // dst — receives decoded string
|
|
93
|
-
ref(rClosure), // the hoisted decode closure
|
|
94
|
-
1, // argc
|
|
95
|
-
ref(dst), // arg[0] = encoded value
|
|
96
|
-
]);
|
|
97
|
-
} else {
|
|
98
|
-
out.push(instr);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { instrs: out };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── Pass entry point ──────────────────────────────────────────────────────────
|
|
106
|
-
export function stringConcealing(
|
|
107
|
-
bc: Bytecode,
|
|
108
|
-
compiler: Compiler,
|
|
109
|
-
): { bytecode: Bytecode } {
|
|
110
|
-
const maxId = buildMaxIdMap(bc);
|
|
111
|
-
|
|
112
|
-
// Compile the decode function ONCE — all functions share the same closure body.
|
|
113
|
-
const decodeTemplate = new Template(`
|
|
114
|
-
function decode(encoded) {
|
|
115
|
-
return atob(encoded);
|
|
116
|
-
}
|
|
117
|
-
`).compile({}, compiler);
|
|
118
|
-
const decodeDesc = decodeTemplate.functions[0];
|
|
119
|
-
|
|
120
|
-
const { bytecode } = forEachFunction(bc, compiler, (fnInstrs, fnId) =>
|
|
121
|
-
processFunctionBlock(fnInstrs, fnId, compiler, maxId, decodeDesc),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// Append the decode function's bytecode at the end (defines its entryLabel).
|
|
125
|
-
// This is a single shared closure, not per-function, so it lives outside
|
|
126
|
-
// forEachFunction's tail mechanism.
|
|
127
|
-
bytecode.push(...decodeTemplate.bytecode);
|
|
128
|
-
|
|
129
|
-
return { bytecode };
|
|
130
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
const traverse = (traverseImport.default ||
|
|
7
|
-
traverseImport) as typeof traverseImport.default;
|
|
8
|
-
|
|
9
|
-
// Extract the real statement list from a SwitchCase consequent.
|
|
10
|
-
function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
11
|
-
let stmts: t.Statement[];
|
|
12
|
-
if (
|
|
13
|
-
switchCase.consequent.length === 1 &&
|
|
14
|
-
t.isBlockStatement(switchCase.consequent[0])
|
|
15
|
-
) {
|
|
16
|
-
stmts = (switchCase.consequent[0] as t.BlockStatement).body;
|
|
17
|
-
} else {
|
|
18
|
-
stmts = switchCase.consequent as t.Statement[];
|
|
19
|
-
}
|
|
20
|
-
return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Replace every `this._operand()` call in bodyStmts with `_operands[i]`
|
|
24
|
-
// where i is the call's sequential index (0-based).
|
|
25
|
-
// Returns the number of replacements performed.
|
|
26
|
-
function replaceOperandCalls(bodyStmts: t.Statement[]): number {
|
|
27
|
-
let replaced = 0;
|
|
28
|
-
|
|
29
|
-
traverse(t.blockStatement(bodyStmts), {
|
|
30
|
-
noScope: true,
|
|
31
|
-
CallExpression(path) {
|
|
32
|
-
const callee = path.node.callee;
|
|
33
|
-
|
|
34
|
-
const isMethodCall = (methodName) => {
|
|
35
|
-
return (
|
|
36
|
-
t.isMemberExpression(callee) &&
|
|
37
|
-
t.isThisExpression(callee.object) &&
|
|
38
|
-
t.isIdentifier(callee.property, { name: methodName }) &&
|
|
39
|
-
path.node.arguments.length === 0
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Replace with _operands[i]
|
|
44
|
-
const createOperandAccess = () => {
|
|
45
|
-
return t.memberExpression(
|
|
46
|
-
t.identifier("_operands"),
|
|
47
|
-
t.numericLiteral(replaced++),
|
|
48
|
-
true, // computed
|
|
49
|
-
);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
if (isMethodCall("_operand")) {
|
|
53
|
-
path.replaceWith(createOperandAccess());
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (isMethodCall("_constant")) {
|
|
57
|
-
path.node.arguments = [createOperandAccess(), createOperandAccess()];
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return replaced;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Appends a generated switch case for every entry in compiler.ALIASED_OPS.
|
|
66
|
-
// Each alias case:
|
|
67
|
-
// 1. Reads all operands eagerly into `_unsortedOperands` (in the shuffled
|
|
68
|
-
// bytecode order) via sequential this._operand() calls.
|
|
69
|
-
// 2. Restores the original operand order into `_operands` using the INVERSE
|
|
70
|
-
// of the stored `order` permutation:
|
|
71
|
-
// inverseOrder[order[i]] = i
|
|
72
|
-
// _operands[j] = _unsortedOperands[inverseOrder[j]]
|
|
73
|
-
// This is necessary because the bytecode stored originalOperands[order[i]]
|
|
74
|
-
// at slot i, so recovering originalOperands[j] requires the inverse lookup.
|
|
75
|
-
// 3. Executes a clone of the original handler body where every
|
|
76
|
-
// this._operand() has been replaced by the corresponding `_operands[i]`.
|
|
77
|
-
//
|
|
78
|
-
// Must run AFTER applyMacroOpcodes / applySpecializedOpcodes (so original
|
|
79
|
-
// cases already exist) but BEFORE applyShuffleOpcodes (so the new alias
|
|
80
|
-
// cases are also shuffled into the handler order).
|
|
81
|
-
export function applyAliasedOpcodes(ast: t.File, compiler: Compiler): void {
|
|
82
|
-
if (!compiler.ALIASED_OPS || Object.keys(compiler.ALIASED_OPS).length === 0)
|
|
83
|
-
return;
|
|
84
|
-
|
|
85
|
-
let switchStatement: t.SwitchStatement | null = null;
|
|
86
|
-
traverse(ast, {
|
|
87
|
-
SwitchStatement(path) {
|
|
88
|
-
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
89
|
-
switchStatement = path.node;
|
|
90
|
-
path.stop();
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
ok(switchStatement, "Could not find @SWITCH statement for aliased opcodes");
|
|
96
|
-
|
|
97
|
-
// Build opName → SwitchCase map from existing OP.xxx case tests.
|
|
98
|
-
const nameToCaseMap = new Map<string, t.SwitchCase>();
|
|
99
|
-
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
100
|
-
const test = sc.test;
|
|
101
|
-
if (
|
|
102
|
-
test &&
|
|
103
|
-
t.isMemberExpression(test) &&
|
|
104
|
-
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
105
|
-
t.isIdentifier(test.property)
|
|
106
|
-
) {
|
|
107
|
-
nameToCaseMap.set((test.property as t.Identifier).name, sc);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const [aliasOpStr, info] of Object.entries(compiler.ALIASED_OPS)) {
|
|
112
|
-
const aliasOpCode = Number(aliasOpStr);
|
|
113
|
-
const { originalOp, order } = info;
|
|
114
|
-
const arity = order.length;
|
|
115
|
-
|
|
116
|
-
const originalName = compiler.OP_NAME[originalOp];
|
|
117
|
-
if (!originalName) continue;
|
|
118
|
-
|
|
119
|
-
const originalCase = nameToCaseMap.get(originalName);
|
|
120
|
-
if (!originalCase) continue;
|
|
121
|
-
|
|
122
|
-
// Clone the original handler body (deep clone so we don't mutate the source)
|
|
123
|
-
const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
|
|
124
|
-
(s) => t.cloneNode(s, true) as t.Statement,
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Replace this._operand() calls with _operands[i]
|
|
128
|
-
const replaced = replaceOperandCalls(bodyStmts);
|
|
129
|
-
|
|
130
|
-
// If the handler has a different number of _operand() calls than our
|
|
131
|
-
// recorded arity, skip this alias (variable-operand handler guard).
|
|
132
|
-
if (replaced !== arity) continue;
|
|
133
|
-
|
|
134
|
-
// Build: var _unsortedOperands = [this._operand(), this._operand(), ...]
|
|
135
|
-
// Reads operands in the NEW (shuffled) bytecode order.
|
|
136
|
-
const unsortedInit = t.variableDeclaration("let", [
|
|
137
|
-
t.variableDeclarator(
|
|
138
|
-
t.identifier("_unsortedOperands"),
|
|
139
|
-
t.arrayExpression(
|
|
140
|
-
Array.from({ length: arity }, () =>
|
|
141
|
-
t.callExpression(
|
|
142
|
-
t.memberExpression(t.thisExpression(), t.identifier("_operand")),
|
|
143
|
-
[],
|
|
144
|
-
),
|
|
145
|
-
),
|
|
146
|
-
),
|
|
147
|
-
),
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
// The inverse permutation maps original position j → unsorted index i,
|
|
151
|
-
// because the bytecode stored originalOperands[order[i]] at slot i.
|
|
152
|
-
// inverseOrder[j] = i means: original operand j lives at _unsortedOperands[i]
|
|
153
|
-
const inverseOrder = new Array<number>(arity);
|
|
154
|
-
for (let i = 0; i < arity; i++) {
|
|
155
|
-
inverseOrder[order[i]] = i;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Build: var _operands = [_unsortedOperands[inverseOrder[0]], ...]
|
|
159
|
-
// Restores the original operand order expected by the handler body.
|
|
160
|
-
const operandsInit = t.variableDeclaration("let", [
|
|
161
|
-
t.variableDeclarator(
|
|
162
|
-
t.identifier("_operands"),
|
|
163
|
-
t.arrayExpression(
|
|
164
|
-
inverseOrder.map((idx) =>
|
|
165
|
-
t.memberExpression(
|
|
166
|
-
t.identifier("_unsortedOperands"),
|
|
167
|
-
t.numericLiteral(idx),
|
|
168
|
-
true, // computed
|
|
169
|
-
),
|
|
170
|
-
),
|
|
171
|
-
),
|
|
172
|
-
),
|
|
173
|
-
]);
|
|
174
|
-
|
|
175
|
-
const allStmts: t.Statement[] = [unsortedInit, operandsInit, ...bodyStmts];
|
|
176
|
-
|
|
177
|
-
// Add a leading comment for readability in non-minified output
|
|
178
|
-
t.addComment(
|
|
179
|
-
allStmts[0],
|
|
180
|
-
"leading",
|
|
181
|
-
` ${compiler.OP_NAME[aliasOpCode]} (order: [${order.join(",")}])`,
|
|
182
|
-
true,
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
allStmts.push(t.breakStatement());
|
|
186
|
-
|
|
187
|
-
(switchStatement as t.SwitchStatement).cases.push(
|
|
188
|
-
t.switchCase(t.numericLiteral(aliasOpCode), [t.blockStatement(allStmts)]),
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { Compiler } from "../../compiler.ts";
|
|
2
|
-
import * as t from "@babel/types";
|
|
3
|
-
import { shuffle } from "../../utils/random-utils.ts";
|
|
4
|
-
|
|
5
|
-
function hasComment(node: t.Node, text: string): boolean {
|
|
6
|
-
const all = [
|
|
7
|
-
...((node as any).leadingComments ?? []),
|
|
8
|
-
...((node as any).innerComments ?? []),
|
|
9
|
-
...((node as any).trailingComments ?? []),
|
|
10
|
-
];
|
|
11
|
-
return all.some((c) => c.value.includes(text));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isPrototypeAssignment(stmt: t.Statement): boolean {
|
|
15
|
-
if (!t.isExpressionStatement(stmt)) return false;
|
|
16
|
-
const expr = stmt.expression;
|
|
17
|
-
if (!t.isAssignmentExpression(expr)) return false;
|
|
18
|
-
const left = expr.left;
|
|
19
|
-
return (
|
|
20
|
-
t.isMemberExpression(left) &&
|
|
21
|
-
t.isMemberExpression(left.object) &&
|
|
22
|
-
t.isIdentifier((left.object as t.MemberExpression).property, {
|
|
23
|
-
name: "prototype",
|
|
24
|
-
})
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function applyClassObfuscation(ast: t.File, _compiler: Compiler): void {
|
|
29
|
-
const body = ast.program.body;
|
|
30
|
-
|
|
31
|
-
// Split at the first statement that carries the @BOOT comment.
|
|
32
|
-
// Everything from that statement onward is the boot section and must stay last.
|
|
33
|
-
let bootIdx = body.findIndex((stmt) => hasComment(stmt, "@BOOT"));
|
|
34
|
-
if (bootIdx === -1) bootIdx = body.length;
|
|
35
|
-
|
|
36
|
-
const shufflable = body.slice(0, bootIdx);
|
|
37
|
-
const boot = body.slice(bootIdx);
|
|
38
|
-
|
|
39
|
-
// Partition the shufflable section into two independent groups.
|
|
40
|
-
// Group A: variable/function declarations (constructors, standalone vars).
|
|
41
|
-
// Group B: prototype method assignments (X.prototype.Y = ...).
|
|
42
|
-
// Both groups are shuffled independently; A always precedes B so that
|
|
43
|
-
// constructors are defined before methods reference them.
|
|
44
|
-
const varDecls: t.Statement[] = [];
|
|
45
|
-
const methodDefs: t.Statement[] = [];
|
|
46
|
-
|
|
47
|
-
for (const stmt of shufflable) {
|
|
48
|
-
if (isPrototypeAssignment(stmt)) {
|
|
49
|
-
methodDefs.push(stmt);
|
|
50
|
-
} else {
|
|
51
|
-
varDecls.push(stmt);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
shuffle(varDecls);
|
|
56
|
-
shuffle(methodDefs);
|
|
57
|
-
|
|
58
|
-
ast.program.body = [...varDecls, ...methodDefs, ...boot];
|
|
59
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
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 generate from "@babel/generator";
|
|
6
|
-
const traverse = (traverseImport.default ||
|
|
7
|
-
traverseImport) as typeof traverseImport.default;
|
|
8
|
-
|
|
9
|
-
// Extract the real statement list from a SwitchCase consequent, normalising
|
|
10
|
-
// the two forms that appear in the runtime:
|
|
11
|
-
// • A single wrapping BlockStatement → use its .body
|
|
12
|
-
// • Statements listed directly → use as-is
|
|
13
|
-
// In both cases trailing BreakStatement / EmptyStatement are filtered out.
|
|
14
|
-
function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
15
|
-
let stmts: t.Statement[];
|
|
16
|
-
if (
|
|
17
|
-
switchCase.consequent.length === 1 &&
|
|
18
|
-
t.isBlockStatement(switchCase.consequent[0])
|
|
19
|
-
) {
|
|
20
|
-
stmts = (switchCase.consequent[0] as t.BlockStatement).body;
|
|
21
|
-
} else {
|
|
22
|
-
stmts = switchCase.consequent as t.Statement[];
|
|
23
|
-
}
|
|
24
|
-
return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
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
|
-
|
|
64
|
-
// Append a generated switch case for every entry in compiler.MACRO_OPS.
|
|
65
|
-
// Each case inlines the constituent case bodies directly — no operand stack,
|
|
66
|
-
// no substitution needed. Because every opcode handler now reads its own
|
|
67
|
-
// operands via this._operand(), those calls naturally consume the inline
|
|
68
|
-
// operands that macroOpcodes.ts embedded on the macro instruction.
|
|
69
|
-
// Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
|
|
70
|
-
export function applyMacroOpcodes(ast: t.File, compiler: Compiler): void {
|
|
71
|
-
let switchStatement: t.SwitchStatement | null = null;
|
|
72
|
-
traverse(ast, {
|
|
73
|
-
SwitchStatement(path) {
|
|
74
|
-
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
75
|
-
switchStatement = path.node;
|
|
76
|
-
path.stop();
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
ok(switchStatement, "Could not find @SWITCH statement for macro opcodes");
|
|
82
|
-
|
|
83
|
-
const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
|
|
84
|
-
|
|
85
|
-
for (const [macroOpStr, constituentOps] of Object.entries(
|
|
86
|
-
compiler.MACRO_OPS,
|
|
87
|
-
)) {
|
|
88
|
-
const macroOpCode = Number(macroOpStr);
|
|
89
|
-
const N = constituentOps.length;
|
|
90
|
-
|
|
91
|
-
// Resolve each constituent op value → case node via OP_NAME lookup.
|
|
92
|
-
const constituentCases: t.SwitchCase[] = [];
|
|
93
|
-
let allFound = true;
|
|
94
|
-
for (const opVal of constituentOps) {
|
|
95
|
-
const found = opcodeToCaseMap.get(opVal);
|
|
96
|
-
if (!found) {
|
|
97
|
-
allFound = false;
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
constituentCases.push(found);
|
|
101
|
-
}
|
|
102
|
-
if (!allFound) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Could not find all constituent ops for macro op ${macroOpCode}`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const opNames = constituentOps.map((v) => compiler.OP_NAME[v] ?? `OP_${v}`);
|
|
109
|
-
let newName = opNames.join(",");
|
|
110
|
-
compiler.OP_NAME[macroOpCode] = newName;
|
|
111
|
-
|
|
112
|
-
// ── Build the macro case body ──────────────────────────────────────────
|
|
113
|
-
// Clone and inline each sub-instruction's case body directly.
|
|
114
|
-
// No operand substitution needed: each body already calls this._operand()
|
|
115
|
-
// to read its own operands, which will consume the inline operands that
|
|
116
|
-
// macroOpcodes.ts embedded on the macro instruction in order.
|
|
117
|
-
const bodyStmts: t.Statement[] = [];
|
|
118
|
-
|
|
119
|
-
for (let i = 0; i < N; i++) {
|
|
120
|
-
const subStmts = extractCaseBody(constituentCases[i]).map(
|
|
121
|
-
(s) => t.cloneNode(s, true) as t.Statement,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (subStmts.length > 0) {
|
|
125
|
-
t.addComment(subStmts[0], "leading", ` ${opNames[i]}`, true);
|
|
126
|
-
bodyStmts.push(...subStmts);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
bodyStmts.push(t.breakStatement());
|
|
131
|
-
|
|
132
|
-
(switchStatement as t.SwitchStatement).cases.push(
|
|
133
|
-
t.switchCase(t.numericLiteral(macroOpCode), [
|
|
134
|
-
t.blockStatement(bodyStmts),
|
|
135
|
-
]),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { minify as applyMinify } from "../../minify.ts";
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import * as t from "@babel/types";
|
|
2
|
-
import traverseImport from "@babel/traverse";
|
|
3
|
-
import { ok } from "assert";
|
|
4
|
-
import { shuffle } from "../../utils/random-utils.ts";
|
|
5
|
-
const traverse = (traverseImport.default ||
|
|
6
|
-
traverseImport) as typeof traverseImport.default;
|
|
7
|
-
|
|
8
|
-
// Randomly reorder the switch cases inside the @SWITCH statement so the
|
|
9
|
-
// opcode handler order varies per build.
|
|
10
|
-
export function applyShuffleOpcodes(ast: t.File): void {
|
|
11
|
-
let switchStatement: t.SwitchStatement | null = null;
|
|
12
|
-
traverse(ast, {
|
|
13
|
-
SwitchStatement(path) {
|
|
14
|
-
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
15
|
-
switchStatement = path.node;
|
|
16
|
-
path.stop();
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
ok(switchStatement, "Could not find opcode handlers switch statement");
|
|
22
|
-
|
|
23
|
-
switchStatement.cases = shuffle(switchStatement.cases);
|
|
24
|
-
}
|