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,566 +0,0 @@
|
|
|
1
|
-
// Control Flow Flattening (CFF)
|
|
2
|
-
//
|
|
3
|
-
// Splits each function into basic blocks and routes all execution through a
|
|
4
|
-
// while-loop + switch-style comparison chain that dispatches based on a
|
|
5
|
-
// `state` register. Original jumps become state transitions.
|
|
6
|
-
//
|
|
7
|
-
// ── How it works ─────────────────────────────────────────────────────────────
|
|
8
|
-
//
|
|
9
|
-
// 1. Each function's instruction stream is split into basic blocks at every
|
|
10
|
-
// label definition and after every terminator (JUMP, JUMP_IF_*, RETURN,
|
|
11
|
-
// THROW).
|
|
12
|
-
//
|
|
13
|
-
// 2. Each block is assigned a random u16 state value. A sentinel endState
|
|
14
|
-
// (not used by any block) marks loop termination.
|
|
15
|
-
//
|
|
16
|
-
// 3. A dispatch loop is compiled from a Template:
|
|
17
|
-
//
|
|
18
|
-
// var state = <startState>;
|
|
19
|
-
// while (state !== <endState>) {
|
|
20
|
-
// if (state === <s0>) _VM_JUMP_("<block0>");
|
|
21
|
-
// if (state === <s1>) _VM_JUMP_("<block1>");
|
|
22
|
-
// ...
|
|
23
|
-
// }
|
|
24
|
-
//
|
|
25
|
-
// The Template's `state` register is extracted via compileInline() so that
|
|
26
|
-
// block bodies can write state transitions to it.
|
|
27
|
-
//
|
|
28
|
-
// 4. Block bodies are emitted with their original instructions. Terminators
|
|
29
|
-
// are rewritten:
|
|
30
|
-
//
|
|
31
|
-
// JUMP target → LOAD_INT state, targetBlock.stateValue
|
|
32
|
-
// JUMP <loopTop>
|
|
33
|
-
//
|
|
34
|
-
// JUMP_IF_FALSE c, t → JUMP_IF_TRUE c, <skipLabel>
|
|
35
|
-
// LOAD_INT state, targetBlock.stateValue
|
|
36
|
-
// JUMP <loopTop>
|
|
37
|
-
// <skipLabel>:
|
|
38
|
-
// LOAD_INT state, fallthroughBlock.stateValue
|
|
39
|
-
// JUMP <loopTop>
|
|
40
|
-
//
|
|
41
|
-
// RETURN / THROW → kept in-place (exits the VM frame directly)
|
|
42
|
-
//
|
|
43
|
-
// 5. Block order is shuffled randomly so spatial locality gives no hints.
|
|
44
|
-
//
|
|
45
|
-
// ── Pipeline position ─────────────────────────────────────────────────────────
|
|
46
|
-
// Same slot as Dispatcher: before resolveRegisters and resolveLabels.
|
|
47
|
-
// Can run alongside Dispatcher (they are composable).
|
|
48
|
-
|
|
49
|
-
import type {
|
|
50
|
-
Bytecode,
|
|
51
|
-
Instruction,
|
|
52
|
-
RegisterOperand,
|
|
53
|
-
} from "../../types.ts";
|
|
54
|
-
import { Compiler } from "../../compiler.ts";
|
|
55
|
-
import { getRandomInt } from "../../utils/random-utils.ts";
|
|
56
|
-
import { U16_MAX } from "../../utils/op-utils.ts";
|
|
57
|
-
import { Template } from "../../template.ts";
|
|
58
|
-
import {
|
|
59
|
-
ref,
|
|
60
|
-
buildMaxIdMap,
|
|
61
|
-
forEachFunction,
|
|
62
|
-
extractLabel,
|
|
63
|
-
} from "../../utils/pass-utils.ts";
|
|
64
|
-
|
|
65
|
-
// ── Basic block splitting ────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
interface BasicBlock {
|
|
68
|
-
label: string;
|
|
69
|
-
body: Bytecode;
|
|
70
|
-
terminator: Instruction | null;
|
|
71
|
-
stateValue: number;
|
|
72
|
-
// Index of the block that originally followed this one (for fallthroughs).
|
|
73
|
-
// -1 means "no successor" (last block, or ends with RETURN/THROW).
|
|
74
|
-
originalNextIndex: number;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isTerminator(op: number, compiler: Compiler): boolean {
|
|
78
|
-
const OP = compiler.OP;
|
|
79
|
-
return (
|
|
80
|
-
op === OP.JUMP ||
|
|
81
|
-
op === OP.JUMP_IF_FALSE ||
|
|
82
|
-
op === OP.JUMP_IF_TRUE ||
|
|
83
|
-
op === OP.RETURN ||
|
|
84
|
-
op === OP.THROW
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function splitBasicBlocks(
|
|
89
|
-
instrs: Bytecode,
|
|
90
|
-
compiler: Compiler,
|
|
91
|
-
): BasicBlock[] {
|
|
92
|
-
const blocks: BasicBlock[] = [];
|
|
93
|
-
const usedStates = new Set<number>();
|
|
94
|
-
|
|
95
|
-
const assignState = (): number => {
|
|
96
|
-
let s: number;
|
|
97
|
-
do {
|
|
98
|
-
s = getRandomInt(0, U16_MAX);
|
|
99
|
-
} while (usedStates.has(s));
|
|
100
|
-
usedStates.add(s);
|
|
101
|
-
return s;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
let currentLabel: string | null = null;
|
|
105
|
-
let currentBody: Bytecode = [];
|
|
106
|
-
|
|
107
|
-
const flushBlock = (terminator: Instruction | null) => {
|
|
108
|
-
if (currentBody.length === 0 && terminator === null && currentLabel === null)
|
|
109
|
-
return;
|
|
110
|
-
|
|
111
|
-
const label = currentLabel ?? compiler._makeLabel("cff_block");
|
|
112
|
-
blocks.push({
|
|
113
|
-
label,
|
|
114
|
-
body: currentBody,
|
|
115
|
-
terminator,
|
|
116
|
-
stateValue: assignState(),
|
|
117
|
-
originalNextIndex: -1, // filled in after all blocks are created
|
|
118
|
-
});
|
|
119
|
-
currentBody = [];
|
|
120
|
-
currentLabel = null;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
for (const instr of instrs) {
|
|
124
|
-
const op = instr[0];
|
|
125
|
-
|
|
126
|
-
// defineLabel → start a new block boundary
|
|
127
|
-
if (op === null && (instr[1] as any)?.type === "defineLabel") {
|
|
128
|
-
flushBlock(null);
|
|
129
|
-
currentLabel = (instr[1] as any).label;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Terminator → ends the current block
|
|
134
|
-
if (op !== null && isTerminator(op, compiler)) {
|
|
135
|
-
flushBlock(instr);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
currentBody.push(instr);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Flush trailing instructions
|
|
143
|
-
flushBlock(null);
|
|
144
|
-
|
|
145
|
-
// Split large blocks (> MAX_BLOCK_SIZE instructions) into smaller chunks
|
|
146
|
-
// so that no single block reveals too much sequential code.
|
|
147
|
-
const MAX_BLOCK_SIZE = 3;
|
|
148
|
-
const splitBlocks: BasicBlock[] = [];
|
|
149
|
-
for (const block of blocks) {
|
|
150
|
-
if (block.body.length <= MAX_BLOCK_SIZE) {
|
|
151
|
-
splitBlocks.push(block);
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
// Chunk the body into pieces of MAX_BLOCK_SIZE
|
|
155
|
-
for (let j = 0; j < block.body.length; j += MAX_BLOCK_SIZE) {
|
|
156
|
-
const isFirst = j === 0;
|
|
157
|
-
const isLast = j + MAX_BLOCK_SIZE >= block.body.length;
|
|
158
|
-
splitBlocks.push({
|
|
159
|
-
label: isFirst ? block.label : compiler._makeLabel("cff_split"),
|
|
160
|
-
body: block.body.slice(j, j + MAX_BLOCK_SIZE),
|
|
161
|
-
terminator: isLast ? block.terminator : null,
|
|
162
|
-
stateValue: isFirst ? block.stateValue : assignState(),
|
|
163
|
-
originalNextIndex: -1,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// Replace blocks with split result
|
|
168
|
-
blocks.length = 0;
|
|
169
|
-
blocks.push(...splitBlocks);
|
|
170
|
-
|
|
171
|
-
// Wire up originalNextIndex for fallthrough resolution
|
|
172
|
-
for (let i = 0; i < blocks.length - 1; i++) {
|
|
173
|
-
blocks[i].originalNextIndex = i + 1;
|
|
174
|
-
}
|
|
175
|
-
// Last block has no successor
|
|
176
|
-
if (blocks.length > 0) {
|
|
177
|
-
blocks[blocks.length - 1].originalNextIndex = -1;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return blocks;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ── Cross-block register promotion ───────────────────────────────────────────
|
|
184
|
-
// Scans all blocks (bodies + terminators) and finds register operands that
|
|
185
|
-
// appear in more than one block. Those registers must not be in the "temp"
|
|
186
|
-
// pool because resolveRegisters' linear scan doesn't understand the CFF
|
|
187
|
-
// dispatch loop and would reuse their slots between blocks.
|
|
188
|
-
//
|
|
189
|
-
// Promotion is done in-place: we delete the `kind` property on the operand
|
|
190
|
-
// objects so they default to the "local::" pool (which never reuses slots).
|
|
191
|
-
|
|
192
|
-
function promoteMultiBlockRegisters(blocks: BasicBlock[]): void {
|
|
193
|
-
// (fnId, regId) → index of first block where this register was seen
|
|
194
|
-
const regFirstBlock = new Map<string, number>();
|
|
195
|
-
// Set of register keys that appear in 2+ blocks
|
|
196
|
-
const multiBlockRegs = new Set<string>();
|
|
197
|
-
|
|
198
|
-
for (let bi = 0; bi < blocks.length; bi++) {
|
|
199
|
-
const allInstrs = blocks[bi].terminator
|
|
200
|
-
? [...blocks[bi].body, blocks[bi].terminator!]
|
|
201
|
-
: blocks[bi].body;
|
|
202
|
-
|
|
203
|
-
for (const instr of allInstrs) {
|
|
204
|
-
for (let j = 1; j < instr.length; j++) {
|
|
205
|
-
const op = instr[j] as any;
|
|
206
|
-
if (op && typeof op === "object" && op.type === "register") {
|
|
207
|
-
const key = `${op.fnId}:${op.id}`;
|
|
208
|
-
const first = regFirstBlock.get(key);
|
|
209
|
-
if (first === undefined) {
|
|
210
|
-
regFirstBlock.set(key, bi);
|
|
211
|
-
} else if (first !== bi) {
|
|
212
|
-
multiBlockRegs.add(key);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (multiBlockRegs.size === 0) return;
|
|
220
|
-
|
|
221
|
-
// Second pass: pin all operand instances of multi-block registers so that
|
|
222
|
-
// resolveRegisters assigns them to the "local::" pool (no slot reuse).
|
|
223
|
-
for (const block of blocks) {
|
|
224
|
-
const allInstrs = block.terminator
|
|
225
|
-
? [...block.body, block.terminator!]
|
|
226
|
-
: block.body;
|
|
227
|
-
|
|
228
|
-
for (const instr of allInstrs) {
|
|
229
|
-
for (let j = 1; j < instr.length; j++) {
|
|
230
|
-
const op = instr[j] as any;
|
|
231
|
-
if (op && typeof op === "object" && op.type === "register") {
|
|
232
|
-
const key = `${op.fnId}:${op.id}`;
|
|
233
|
-
if (multiBlockRegs.has(key)) {
|
|
234
|
-
op.pinned = true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ── Generate the dispatch loop via Template ──────────────────────────────────
|
|
243
|
-
|
|
244
|
-
function buildDispatchTemplate(
|
|
245
|
-
blocks: BasicBlock[],
|
|
246
|
-
endState: number,
|
|
247
|
-
startState: number,
|
|
248
|
-
compiler: Compiler,
|
|
249
|
-
fnId: number,
|
|
250
|
-
maxId: Map<number, number>,
|
|
251
|
-
): {
|
|
252
|
-
bytecode: Bytecode;
|
|
253
|
-
rState: RegisterOperand;
|
|
254
|
-
loopTopLabel: string;
|
|
255
|
-
loopExitLabel: string;
|
|
256
|
-
innerBytecode: Bytecode;
|
|
257
|
-
} {
|
|
258
|
-
// Build the if-chain cases
|
|
259
|
-
const cases = blocks
|
|
260
|
-
.map(
|
|
261
|
-
(block) =>
|
|
262
|
-
`if (state === ${block.stateValue}) _VM_JUMP_("${block.label}");`,
|
|
263
|
-
)
|
|
264
|
-
.join("\n ");
|
|
265
|
-
|
|
266
|
-
const source = `
|
|
267
|
-
var state = ${startState};
|
|
268
|
-
while (state !== ${endState}) {
|
|
269
|
-
${cases}
|
|
270
|
-
}
|
|
271
|
-
`;
|
|
272
|
-
|
|
273
|
-
const tmpl = new Template(source);
|
|
274
|
-
const result = tmpl.compileInline({}, compiler, fnId, maxId);
|
|
275
|
-
|
|
276
|
-
// Pin ALL dispatch-loop registers so resolveRegisters assigns them to the
|
|
277
|
-
// "local::" pool (no slot reuse). The dispatch loop is re-entered on every
|
|
278
|
-
// state transition (backward JUMP to while_top), but the linear-scan liveness
|
|
279
|
-
// in resolveRegisters doesn't track loops and would incorrectly treat dispatch
|
|
280
|
-
// temps as dead after one pass, allowing their slots to be reused by body
|
|
281
|
-
// registers that are live across blocks.
|
|
282
|
-
for (const instr of result.bytecode) {
|
|
283
|
-
for (let j = 1; j < instr.length; j++) {
|
|
284
|
-
const op = instr[j] as any;
|
|
285
|
-
if (op && typeof op === "object" && op.type === "register") {
|
|
286
|
-
op.pinned = true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const rState = result.registers.get("state");
|
|
292
|
-
if (!rState) {
|
|
293
|
-
throw new Error("CFF: Template did not produce a 'state' register");
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Find the while loop labels from the compiled IR
|
|
297
|
-
let loopTopLabel: string | null = null;
|
|
298
|
-
let loopExitLabel: string | null = null;
|
|
299
|
-
|
|
300
|
-
for (const instr of result.bytecode) {
|
|
301
|
-
if (instr[0] === null && (instr[1] as any)?.type === "defineLabel") {
|
|
302
|
-
const label = (instr[1] as any).label as string;
|
|
303
|
-
if (label.includes("while_top") && !loopTopLabel) {
|
|
304
|
-
loopTopLabel = label;
|
|
305
|
-
}
|
|
306
|
-
if (label.includes("while_exit") && !loopExitLabel) {
|
|
307
|
-
loopExitLabel = label;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (!loopTopLabel || !loopExitLabel) {
|
|
313
|
-
throw new Error("CFF: Could not find while loop labels in Template output");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
bytecode: result.bytecode,
|
|
318
|
-
rState,
|
|
319
|
-
loopTopLabel,
|
|
320
|
-
loopExitLabel,
|
|
321
|
-
innerBytecode: result.innerBytecode,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ── State transition helpers ─────────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
function emitStateTransition(
|
|
328
|
-
out: Bytecode,
|
|
329
|
-
rState: RegisterOperand,
|
|
330
|
-
targetState: number,
|
|
331
|
-
loopTopLabel: string,
|
|
332
|
-
compiler: Compiler,
|
|
333
|
-
): void {
|
|
334
|
-
out.push([compiler.OP.LOAD_INT!, ref(rState), targetState]);
|
|
335
|
-
out.push([compiler.OP.JUMP!, { type: "label", label: loopTopLabel }]);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ── Per-function transformation ──────────────────────────────────────────────
|
|
339
|
-
|
|
340
|
-
function processFunctionBlock(
|
|
341
|
-
instrs: Bytecode,
|
|
342
|
-
fnId: number,
|
|
343
|
-
compiler: Compiler,
|
|
344
|
-
maxId: Map<number, number>,
|
|
345
|
-
): { instrs: Bytecode; tail: Bytecode } {
|
|
346
|
-
const OP = compiler.OP;
|
|
347
|
-
|
|
348
|
-
// Only transform functions that contain simple jumps
|
|
349
|
-
const hasRoutableJump = instrs.some((instr) => {
|
|
350
|
-
const op = instr[0];
|
|
351
|
-
return op === OP.JUMP || op === OP.JUMP_IF_FALSE || op === OP.JUMP_IF_TRUE;
|
|
352
|
-
});
|
|
353
|
-
if (!hasRoutableJump) return { instrs, tail: [] };
|
|
354
|
-
|
|
355
|
-
// ── 1. Split into basic blocks ──────────────────────────────────────────
|
|
356
|
-
const blocks = splitBasicBlocks(instrs, compiler);
|
|
357
|
-
if (blocks.length < 2) return { instrs, tail: [] };
|
|
358
|
-
|
|
359
|
-
// ── 1b. Promote cross-block registers to "local" pool ──────────────────
|
|
360
|
-
// resolveRegisters does a linear-scan liveness analysis that doesn't
|
|
361
|
-
// understand the CFF dispatch loop (backward jumps). A "temp" register
|
|
362
|
-
// that's live across two blocks would appear to die within its first
|
|
363
|
-
// block and get its slot reused, corrupting values read in later blocks.
|
|
364
|
-
//
|
|
365
|
-
// Fix: find every register that appears in more than one block and
|
|
366
|
-
// delete its "temp" kind so it lands in the "local::" pool (no reuse).
|
|
367
|
-
promoteMultiBlockRegisters(blocks);
|
|
368
|
-
|
|
369
|
-
const usedStates = new Set(blocks.map((b) => b.stateValue));
|
|
370
|
-
|
|
371
|
-
// Pick endState sentinel
|
|
372
|
-
let endState: number;
|
|
373
|
-
do {
|
|
374
|
-
endState = getRandomInt(0, U16_MAX);
|
|
375
|
-
} while (usedStates.has(endState));
|
|
376
|
-
|
|
377
|
-
const startState = blocks[0].stateValue;
|
|
378
|
-
|
|
379
|
-
// ── 2. Build dispatch loop from Template ────────────────────────────────
|
|
380
|
-
const dispatch = buildDispatchTemplate(
|
|
381
|
-
blocks,
|
|
382
|
-
endState,
|
|
383
|
-
startState,
|
|
384
|
-
compiler,
|
|
385
|
-
fnId,
|
|
386
|
-
maxId,
|
|
387
|
-
);
|
|
388
|
-
const { rState, loopTopLabel, loopExitLabel } = dispatch;
|
|
389
|
-
|
|
390
|
-
// ── 3. Pre-compute all state mappings BEFORE shuffle ─────────────────
|
|
391
|
-
// These maps capture the correct stateValues while the blocks array is
|
|
392
|
-
// still in its original split order. After the shuffle, indexing into
|
|
393
|
-
// blocks[] by original index would give the wrong block.
|
|
394
|
-
|
|
395
|
-
// label → stateValue (for jump target resolution)
|
|
396
|
-
const labelToState = new Map<string, number>();
|
|
397
|
-
for (const block of blocks) {
|
|
398
|
-
labelToState.set(block.label, block.stateValue);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// originalIndex → fallthrough stateValue
|
|
402
|
-
const fallthroughStateMap = new Map<number, number>();
|
|
403
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
404
|
-
const next = blocks[i].originalNextIndex;
|
|
405
|
-
fallthroughStateMap.set(
|
|
406
|
-
i,
|
|
407
|
-
next >= 0 ? blocks[next].stateValue : endState,
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// ── 4. Shuffle block order ──────────────────────────────────────────────
|
|
412
|
-
// Track which original index each shuffled position came from, so we can
|
|
413
|
-
// look up fallthroughStateMap correctly during emission.
|
|
414
|
-
const originalIndices = blocks.map((_, i) => i);
|
|
415
|
-
|
|
416
|
-
// Fisher-Yates shuffle
|
|
417
|
-
for (let i = blocks.length - 1; i > 0; i--) {
|
|
418
|
-
const j = getRandomInt(0, i);
|
|
419
|
-
[blocks[i], blocks[j]] = [blocks[j], blocks[i]];
|
|
420
|
-
[originalIndices[i], originalIndices[j]] = [
|
|
421
|
-
originalIndices[j],
|
|
422
|
-
originalIndices[i],
|
|
423
|
-
];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ── 5. Emit: dispatch loop + block bodies ───────────────────────────────
|
|
427
|
-
const out: Bytecode = [];
|
|
428
|
-
|
|
429
|
-
// Dispatch loop (var state = ...; while(...) { if-chain })
|
|
430
|
-
out.push(...dispatch.bytecode);
|
|
431
|
-
|
|
432
|
-
// Each block: defineLabel → body → state transition → JUMP loopTop
|
|
433
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
434
|
-
const block = blocks[i];
|
|
435
|
-
const origIdx = originalIndices[i];
|
|
436
|
-
|
|
437
|
-
// Block label
|
|
438
|
-
out.push([null, { type: "defineLabel", label: block.label }]);
|
|
439
|
-
|
|
440
|
-
// Block body
|
|
441
|
-
out.push(...block.body);
|
|
442
|
-
|
|
443
|
-
// Terminator rewriting
|
|
444
|
-
const term = block.terminator;
|
|
445
|
-
|
|
446
|
-
if (term === null) {
|
|
447
|
-
// Fallthrough → transition to the original next block's state
|
|
448
|
-
emitStateTransition(
|
|
449
|
-
out,
|
|
450
|
-
rState,
|
|
451
|
-
fallthroughStateMap.get(origIdx)!,
|
|
452
|
-
loopTopLabel,
|
|
453
|
-
compiler,
|
|
454
|
-
);
|
|
455
|
-
} else if (term[0] === OP.RETURN || term[0] === OP.THROW) {
|
|
456
|
-
// Exits the frame — emit as-is
|
|
457
|
-
out.push(term);
|
|
458
|
-
} else if (term[0] === OP.JUMP) {
|
|
459
|
-
const targetLabel = extractLabel(term[1]);
|
|
460
|
-
if (targetLabel !== null) {
|
|
461
|
-
const targetState = labelToState.get(targetLabel);
|
|
462
|
-
if (targetState !== undefined) {
|
|
463
|
-
emitStateTransition(out, rState, targetState, loopTopLabel, compiler);
|
|
464
|
-
} else {
|
|
465
|
-
// Target outside this function's blocks — keep original
|
|
466
|
-
out.push(term);
|
|
467
|
-
}
|
|
468
|
-
} else {
|
|
469
|
-
out.push(term);
|
|
470
|
-
}
|
|
471
|
-
} else if (term[0] === OP.JUMP_IF_FALSE) {
|
|
472
|
-
// Original: if (!cond) goto target; else fallthrough
|
|
473
|
-
// → if (cond) goto skipLabel (inverted)
|
|
474
|
-
// state = targetState; goto loopTop
|
|
475
|
-
// skipLabel:
|
|
476
|
-
// state = fallthroughState; goto loopTop
|
|
477
|
-
const cond = term[1] as RegisterOperand;
|
|
478
|
-
const targetLabel = extractLabel(term[2]);
|
|
479
|
-
|
|
480
|
-
if (targetLabel !== null) {
|
|
481
|
-
const targetState = labelToState.get(targetLabel);
|
|
482
|
-
if (targetState !== undefined) {
|
|
483
|
-
const skipLabel = compiler._makeLabel("cff_skip");
|
|
484
|
-
|
|
485
|
-
out.push([
|
|
486
|
-
OP.JUMP_IF_TRUE!,
|
|
487
|
-
cond,
|
|
488
|
-
{ type: "label", label: skipLabel },
|
|
489
|
-
]);
|
|
490
|
-
emitStateTransition(
|
|
491
|
-
out,
|
|
492
|
-
rState,
|
|
493
|
-
targetState,
|
|
494
|
-
loopTopLabel,
|
|
495
|
-
compiler,
|
|
496
|
-
);
|
|
497
|
-
out.push([null, { type: "defineLabel", label: skipLabel }]);
|
|
498
|
-
emitStateTransition(
|
|
499
|
-
out,
|
|
500
|
-
rState,
|
|
501
|
-
fallthroughStateMap.get(origIdx)!,
|
|
502
|
-
loopTopLabel,
|
|
503
|
-
compiler,
|
|
504
|
-
);
|
|
505
|
-
} else {
|
|
506
|
-
out.push(term);
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
out.push(term);
|
|
510
|
-
}
|
|
511
|
-
} else if (term[0] === OP.JUMP_IF_TRUE) {
|
|
512
|
-
// Original: if (cond) goto target; else fallthrough
|
|
513
|
-
// → if (!cond) goto skipLabel (inverted)
|
|
514
|
-
// state = targetState; goto loopTop
|
|
515
|
-
// skipLabel:
|
|
516
|
-
// state = fallthroughState; goto loopTop
|
|
517
|
-
const cond = term[1] as RegisterOperand;
|
|
518
|
-
const targetLabel = extractLabel(term[2]);
|
|
519
|
-
|
|
520
|
-
if (targetLabel !== null) {
|
|
521
|
-
const targetState = labelToState.get(targetLabel);
|
|
522
|
-
if (targetState !== undefined) {
|
|
523
|
-
const skipLabel = compiler._makeLabel("cff_skip");
|
|
524
|
-
|
|
525
|
-
out.push([
|
|
526
|
-
OP.JUMP_IF_FALSE!,
|
|
527
|
-
cond,
|
|
528
|
-
{ type: "label", label: skipLabel },
|
|
529
|
-
]);
|
|
530
|
-
emitStateTransition(
|
|
531
|
-
out,
|
|
532
|
-
rState,
|
|
533
|
-
targetState,
|
|
534
|
-
loopTopLabel,
|
|
535
|
-
compiler,
|
|
536
|
-
);
|
|
537
|
-
out.push([null, { type: "defineLabel", label: skipLabel }]);
|
|
538
|
-
emitStateTransition(
|
|
539
|
-
out,
|
|
540
|
-
rState,
|
|
541
|
-
fallthroughStateMap.get(origIdx)!,
|
|
542
|
-
loopTopLabel,
|
|
543
|
-
compiler,
|
|
544
|
-
);
|
|
545
|
-
} else {
|
|
546
|
-
out.push(term);
|
|
547
|
-
}
|
|
548
|
-
} else {
|
|
549
|
-
out.push(term);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return { instrs: out, tail: dispatch.innerBytecode };
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// ── Pass entry point ──────────────────────────────────────────────────────────
|
|
558
|
-
export function controlFlowFlattening(
|
|
559
|
-
bc: Bytecode,
|
|
560
|
-
compiler: Compiler,
|
|
561
|
-
): { bytecode: Bytecode } {
|
|
562
|
-
const maxId = buildMaxIdMap(bc);
|
|
563
|
-
return forEachFunction(bc, compiler, (fnInstrs, fnId) =>
|
|
564
|
-
processFunctionBlock(fnInstrs, fnId, compiler, maxId),
|
|
565
|
-
);
|
|
566
|
-
}
|