js-confuser-vm 0.1.0 → 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 +281 -147
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +714 -265
- package/dist/disassembler.js +367 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +160 -119
- package/dist/template.js +163 -42
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
- package/dist/transforms/bytecode/dispatcher.js +15 -111
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
- package/dist/transforms/bytecode/resolveRegisters.js +23 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
- package/dist/transforms/bytecode/stringConcealing.js +288 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +75 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/package.json +8 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -335
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/index.ts +0 -38
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -52
- package/src/build-runtime.ts +0 -78
- package/src/compiler.ts +0 -2593
- package/src/index.ts +0 -14
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -18
- package/src/runtime.ts +0 -923
- package/src/template.ts +0 -141
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/dispatcher.ts +0 -398
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -221
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -156
- package/src/types.ts +0 -93
- package/src/utils/op-utils.ts +0 -48
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import type { Bytecode, Instruction } from "../../types.ts";
|
|
2
|
-
import { Compiler } from "../../compiler.ts";
|
|
3
|
-
import { choice } from "../../utils/random-utils.ts";
|
|
4
|
-
import { getInstructionSize } from "../../utils/op-utils.ts";
|
|
5
|
-
|
|
6
|
-
export function selfModifying(
|
|
7
|
-
bc: Bytecode,
|
|
8
|
-
compiler: Compiler,
|
|
9
|
-
): { bytecode: Bytecode } {
|
|
10
|
-
// Walk the bytecode looking for "defineLabel" pseudo-ops, which start basic
|
|
11
|
-
// blocks. For each block we collect the body (instructions between the label
|
|
12
|
-
// and the next label/jump terminator), move it to the end of the bytecode
|
|
13
|
-
// under a fresh "patch_LXX" label, and replace it in-place with:
|
|
14
|
-
//
|
|
15
|
-
// defineLabel ("originalLabel") ← kept as-is (pseudo-op)
|
|
16
|
-
// PATCH destPc sliceStart sliceEnd ← 4 flat slots total
|
|
17
|
-
// Garbage Opcodes × bodyFlatSize ← placeholder slots
|
|
18
|
-
//
|
|
19
|
-
// PATCH reads three inline operands via _operand():
|
|
20
|
-
// destPc = originalLabel + 4 (first slot after PATCH's own 4 slots)
|
|
21
|
-
// sliceStart = patchLabel (flat PC of appended body)
|
|
22
|
-
// sliceEnd = patchLabel + bodyFlatSize
|
|
23
|
-
//
|
|
24
|
-
// On first execution PATCH copies bytecode[sliceStart..sliceEnd) over the
|
|
25
|
-
// placeholder region starting at destPc. Execution then falls through into
|
|
26
|
-
// the freshly-patched body. Subsequent calls are idempotent.
|
|
27
|
-
|
|
28
|
-
const { OP, JUMP_OPS } = compiler;
|
|
29
|
-
|
|
30
|
-
const result: Bytecode = [];
|
|
31
|
-
const appended: Bytecode = [];
|
|
32
|
-
let patchCount = 0;
|
|
33
|
-
|
|
34
|
-
let i = 0;
|
|
35
|
-
while (i < bc.length) {
|
|
36
|
-
const instr = bc[i];
|
|
37
|
-
const [op, operand] = instr;
|
|
38
|
-
|
|
39
|
-
// Detect a defineLabel pseudo-op — start of a new basic block.
|
|
40
|
-
if (
|
|
41
|
-
op === null &&
|
|
42
|
-
operand !== null &&
|
|
43
|
-
typeof operand === "object" &&
|
|
44
|
-
(operand as any).type === "defineLabel"
|
|
45
|
-
) {
|
|
46
|
-
const originalLabel = (operand as any).label as string;
|
|
47
|
-
result.push(instr); // keep the defineLabel marker
|
|
48
|
-
i++;
|
|
49
|
-
|
|
50
|
-
// Collect body: everything after the label until the next terminator.
|
|
51
|
-
let j = i;
|
|
52
|
-
while (j < bc.length) {
|
|
53
|
-
const [nextOp, nextOperand] = bc[j];
|
|
54
|
-
|
|
55
|
-
// Another defineLabel = boundary of the next block.
|
|
56
|
-
if (
|
|
57
|
-
nextOp === null &&
|
|
58
|
-
typeof nextOperand === "object" &&
|
|
59
|
-
(nextOperand as any)?.type === "defineLabel"
|
|
60
|
-
) {
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Jump instructions, RETURN all terminate the body.
|
|
65
|
-
if (nextOp !== null && (JUMP_OPS.has(nextOp) || nextOp === OP.RETURN)) {
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
j++;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const body = bc.slice(i, j);
|
|
73
|
-
const N = body.length;
|
|
74
|
-
|
|
75
|
-
if (N === 0) {
|
|
76
|
-
// Nothing to transform — label is immediately followed by a terminator.
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const patchLabel = `patch_${originalLabel}_${patchCount++}`;
|
|
81
|
-
|
|
82
|
-
// Flat size of the body (each instruction occupies instr.length slots).
|
|
83
|
-
const bodyFlatSize = body.reduce(
|
|
84
|
-
(acc, instr) => acc + getInstructionSize(instr),
|
|
85
|
-
0,
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// ── PATCH instruction (4 flat slots: opcode + 3 operands) ───────────
|
|
89
|
-
// destPc = originalLabel + 4 (slot right after PATCH's 4 slots)
|
|
90
|
-
// sliceStart = patchLabel
|
|
91
|
-
// sliceEnd = patchLabel + bodyFlatSize
|
|
92
|
-
result.push([
|
|
93
|
-
OP.PATCH as number,
|
|
94
|
-
{ type: "label", label: originalLabel, offset: 4 },
|
|
95
|
-
{ type: "label", label: patchLabel },
|
|
96
|
-
{ type: "label", label: patchLabel, offset: bodyFlatSize },
|
|
97
|
-
] as unknown as Instruction);
|
|
98
|
-
|
|
99
|
-
// ── Placeholders (Garbage Opcodes * bodyFlatSize, each 1 flat slot) ────────────
|
|
100
|
-
// These are overwritten by PATCH on first execution.
|
|
101
|
-
for (let p = 0; p < bodyFlatSize; p++) {
|
|
102
|
-
const randomOpcode = choice(Object.values(compiler.OP));
|
|
103
|
-
result.push([+randomOpcode]);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── Append real body at end ─────────────────────────────────────────
|
|
107
|
-
appended.push([null, { type: "defineLabel", label: patchLabel }]);
|
|
108
|
-
for (const bodyInstr of body) {
|
|
109
|
-
appended.push(bodyInstr);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
i = j; // skip over the original body in the input array
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
result.push(instr);
|
|
117
|
-
i++;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return { bytecode: [...result, ...appended] };
|
|
121
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
|
|
2
|
-
import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
|
|
3
|
-
import { getInstructionSize, nextFreeSlot } from "../../utils/op-utils.ts";
|
|
4
|
-
|
|
5
|
-
export const nSizedOps = [
|
|
6
|
-
"MAKE_CLOSURE",
|
|
7
|
-
"BUILD_ARRAY",
|
|
8
|
-
"BUILD_OBJECT",
|
|
9
|
-
"CALL",
|
|
10
|
-
"CALL_METHOD",
|
|
11
|
-
"NEW",
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
// Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
|
|
15
|
-
// Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
|
|
16
|
-
// Only instructions that are fixed-sized are considered.
|
|
17
|
-
// MAKE_CLOSURE and other N-sized instructions cannot be specialized
|
|
18
|
-
// Operands are converted into objects and marked as 'placeholder' - other passes can mutate and the reference stays intact
|
|
19
|
-
// We need a reference throughout the pipeline so that final AST generation can place the actual value
|
|
20
|
-
// The 'placeholder' flag drops the operand from the final bytecode - any size calculation must not count these
|
|
21
|
-
export function specializedOpcodes(
|
|
22
|
-
bc: Bytecode,
|
|
23
|
-
compiler: Compiler,
|
|
24
|
-
): { bytecode: Bytecode } {
|
|
25
|
-
const disallowedOps = new Set(nSizedOps.map((name) => compiler.OP[name]));
|
|
26
|
-
|
|
27
|
-
// ── Step 1: count frequency of eligible (op, operand) pairs ───────────────
|
|
28
|
-
const freqMap = new Map<
|
|
29
|
-
string,
|
|
30
|
-
{
|
|
31
|
-
op: number;
|
|
32
|
-
operands: InstrOperand[];
|
|
33
|
-
operandsKey: string;
|
|
34
|
-
occurences: number;
|
|
35
|
-
}
|
|
36
|
-
>();
|
|
37
|
-
|
|
38
|
-
for (const instr of bc) {
|
|
39
|
-
const op = instr[0];
|
|
40
|
-
if (op === null || disallowedOps.has(op)) continue;
|
|
41
|
-
|
|
42
|
-
// Only supports between 1-6 operands
|
|
43
|
-
const operandCount = getInstructionSize(instr) - 1;
|
|
44
|
-
if (operandCount < 1 || operandCount > 6) continue;
|
|
45
|
-
|
|
46
|
-
// Convert numbers into operand objects so they can be modified elsewhere and preserved
|
|
47
|
-
const oldOperands = instr.slice(1);
|
|
48
|
-
const operands = oldOperands.map((operand) => {
|
|
49
|
-
if (typeof operand === "number") {
|
|
50
|
-
return {
|
|
51
|
-
type: "number",
|
|
52
|
-
value: operand,
|
|
53
|
-
resolvedValue: operand,
|
|
54
|
-
} as InstrOperand;
|
|
55
|
-
}
|
|
56
|
-
return operand;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
instr.length = 1;
|
|
60
|
-
instr.push(...operands);
|
|
61
|
-
|
|
62
|
-
const operandsKey = JSON.stringify(operands);
|
|
63
|
-
|
|
64
|
-
const key = `${op},${operandsKey}`;
|
|
65
|
-
const entry = freqMap.get(key);
|
|
66
|
-
if (entry) {
|
|
67
|
-
entry.occurences++;
|
|
68
|
-
} else {
|
|
69
|
-
freqMap.set(key, {
|
|
70
|
-
op,
|
|
71
|
-
operands,
|
|
72
|
-
operandsKey,
|
|
73
|
-
occurences: 1,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Step 2: keep combinations that appear >= 2 times, sort by frequency ───
|
|
79
|
-
const candidates = Array.from(freqMap.values())
|
|
80
|
-
.filter((e) => e.occurences >= 1)
|
|
81
|
-
.sort((a, b) => b.occurences - a.occurences);
|
|
82
|
-
|
|
83
|
-
if (candidates.length === 0) return { bytecode: bc };
|
|
84
|
-
|
|
85
|
-
// ── Step 3: assign free opcode slots to the best candidates ───────────────
|
|
86
|
-
const sigToSpecial = new Map<string, number>();
|
|
87
|
-
const specializedOps: Compiler["SPECIALIZED_OPS"] = {};
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
90
|
-
const specialOp = nextFreeSlot(compiler);
|
|
91
|
-
if (specialOp === -1) break;
|
|
92
|
-
const { op: originalOp, operands, operandsKey } = candidates[i];
|
|
93
|
-
|
|
94
|
-
const key = `${originalOp},${operandsKey}`;
|
|
95
|
-
sigToSpecial.set(key, specialOp);
|
|
96
|
-
|
|
97
|
-
specializedOps[specialOp] = { originalOp, operands };
|
|
98
|
-
|
|
99
|
-
// Register a human-readable name for disassembly / debugging
|
|
100
|
-
const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
|
|
101
|
-
compiler.OP_NAME[specialOp] =
|
|
102
|
-
`${originalName}_${JSON.stringify(operandsKey)}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Store mapping so the interpreter knows how to dispatch the specialized op
|
|
106
|
-
compiler.SPECIALIZED_OPS = specializedOps;
|
|
107
|
-
|
|
108
|
-
// ── Step 4: replace matching instructions with the new single-byte opcode ─
|
|
109
|
-
const result: Bytecode = [];
|
|
110
|
-
|
|
111
|
-
for (const instr of bc) {
|
|
112
|
-
const op = instr[0];
|
|
113
|
-
// Only consider instructions with one or more operands
|
|
114
|
-
if (op === null || instr.length <= 1 || op === compiler.OP.MAKE_CLOSURE) {
|
|
115
|
-
result.push(instr);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const operands = instr.slice(1);
|
|
120
|
-
const operandsKey = JSON.stringify(operands);
|
|
121
|
-
|
|
122
|
-
const key = `${op},${operandsKey}`;
|
|
123
|
-
|
|
124
|
-
const specialOpCode = sigToSpecial.get(key)!;
|
|
125
|
-
|
|
126
|
-
if (!specialOpCode) {
|
|
127
|
-
result.push(instr);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const newOperands = operands.map((operand) => {
|
|
132
|
-
const operandAsObject: any =
|
|
133
|
-
typeof operand === "object" && operand
|
|
134
|
-
? operand
|
|
135
|
-
: {
|
|
136
|
-
type: "number",
|
|
137
|
-
resolvedValue: operand,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
operandAsObject.placeholder = true;
|
|
141
|
-
return operandAsObject;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const newInstr: Instruction = [specialOpCode, ...newOperands];
|
|
145
|
-
|
|
146
|
-
// Preserve source-node information for error reporting
|
|
147
|
-
(newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
|
|
148
|
-
|
|
149
|
-
result.push(newInstr);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { bytecode: result };
|
|
153
|
-
}
|
|
@@ -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
|
-
}
|