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.
Files changed (58) hide show
  1. package/README.md +242 -89
  2. package/dist/compiler.js +583 -208
  3. package/dist/disassembler.js +58 -8
  4. package/dist/runtime.js +93 -74
  5. package/dist/template.js +81 -76
  6. package/dist/transforms/bytecode/concealConstants.js +2 -2
  7. package/dist/transforms/bytecode/controlFlowFlattening.js +143 -25
  8. package/dist/transforms/bytecode/dispatcher.js +3 -3
  9. package/dist/transforms/bytecode/resolveRegisters.js +19 -4
  10. package/dist/transforms/bytecode/selfModifying.js +88 -21
  11. package/dist/transforms/bytecode/specializedOpcodes.js +6 -3
  12. package/dist/transforms/bytecode/stringConcealing.js +253 -75
  13. package/dist/utils/ast-utils.js +61 -0
  14. package/dist/utils/op-utils.js +1 -0
  15. package/package.json +7 -1
  16. package/.gitmodules +0 -4
  17. package/.prettierignore +0 -1
  18. package/CHANGELOG.md +0 -358
  19. package/babel-plugin-inline-runtime.cjs +0 -34
  20. package/babel.config.json +0 -23
  21. package/bench.ts +0 -146
  22. package/disassemble.ts +0 -12
  23. package/index.ts +0 -43
  24. package/jest-strip-types.js +0 -10
  25. package/jest.config.js +0 -64
  26. package/output.disassembled.js +0 -41
  27. package/src/build-runtime.ts +0 -113
  28. package/src/compiler.ts +0 -2703
  29. package/src/disassembler.ts +0 -329
  30. package/src/index.ts +0 -24
  31. package/src/minify.ts +0 -21
  32. package/src/options.ts +0 -24
  33. package/src/runtime.ts +0 -956
  34. package/src/template.ts +0 -265
  35. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -151
  36. package/src/transforms/bytecode/concealConstants.ts +0 -52
  37. package/src/transforms/bytecode/controlFlowFlattening.ts +0 -566
  38. package/src/transforms/bytecode/dispatcher.ts +0 -292
  39. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  40. package/src/transforms/bytecode/resolveConstants.ts +0 -126
  41. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  42. package/src/transforms/bytecode/resolveRegisters.ts +0 -226
  43. package/src/transforms/bytecode/selfModifying.ts +0 -121
  44. package/src/transforms/bytecode/specializedOpcodes.ts +0 -164
  45. package/src/transforms/bytecode/stringConcealing.ts +0 -130
  46. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  47. package/src/transforms/runtime/classObfuscation.ts +0 -59
  48. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  49. package/src/transforms/runtime/minify.ts +0 -1
  50. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  51. package/src/transforms/runtime/specializedOpcodes.ts +0 -161
  52. package/src/types.ts +0 -134
  53. package/src/utils/ast-utils.ts +0 -19
  54. package/src/utils/op-utils.ts +0 -46
  55. package/src/utils/pass-utils.ts +0 -126
  56. package/src/utils/profile-utils.ts +0 -3
  57. package/src/utils/random-utils.ts +0 -31
  58. package/tsconfig.json +0 -12
package/src/compiler.ts DELETED
@@ -1,2703 +0,0 @@
1
- import * as t from "@babel/types";
2
- import * as b from "./types.ts";
3
- import { parse } from "@babel/parser";
4
- import traverseImport from "@babel/traverse";
5
- import { generate } from "@babel/generator";
6
- import { join } from "path";
7
- import { readFileSync } from "fs";
8
- import { stripTypeScriptTypes } from "module";
9
- import { ok } from "assert";
10
- import { buildRuntime } from "./build-runtime.ts";
11
- import { DEFAULT_OPTIONS, type Options } from "./options.ts";
12
- import { resolveLabels } from "./transforms/bytecode/resolveLabels.ts";
13
- import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.ts";
14
- import { resolveConstants } from "./transforms/bytecode/resolveConstants.ts";
15
- import { selfModifying } from "./transforms/bytecode/selfModifying.ts";
16
- import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.ts";
17
- import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.ts";
18
- import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.ts";
19
- import { getRandomInt } from "./utils/random-utils.ts";
20
- import { U16_MAX } from "./utils/op-utils.ts";
21
- import { concealConstants } from "./transforms/bytecode/concealConstants.ts";
22
- import { dispatcher } from "./transforms/bytecode/dispatcher.ts";
23
- import { controlFlowFlattening } from "./transforms/bytecode/controlFlowFlattening.ts";
24
- import { stringConcealing } from "./transforms/bytecode/stringConcealing.ts";
25
- import { now } from "./utils/profile-utils.ts";
26
-
27
- const traverse = (traverseImport.default ||
28
- traverseImport) as typeof traverseImport.default;
29
-
30
- const readVMRuntimeFile = () => {
31
- // During "npm run build", babel-plugin-inline-runtime.cjs replaces this function with the raw, type-stripped contents
32
- let code = readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
33
- return stripTypeScriptTypes?.(code) || code;
34
- };
35
-
36
- export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
37
- export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
38
-
39
- // ── Opcodes ──────────────────────────────────────────────────────────────────
40
- // Register-based encoding. Operand convention (x86 / CPython style):
41
- // destination register first, then source registers, then immediates.
42
- //
43
- // dst – register index that receives the result
44
- // src – register index holding an input value
45
- // imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
46
- //
47
- // Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
48
- // Every load: [op, dst, ...]
49
- // Every store: [op, target, src]
50
- // Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
51
- // CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
52
- export const OP_ORIGINAL = {
53
- // ── Loads ─────────────────────────────────────────────────────────────────
54
- LOAD_CONST: 0, // dst, constIdx regs[dst] = constants[constIdx]
55
- LOAD_INT: 1, // dst, imm regs[dst] = imm (raw u16 literal)
56
- LOAD_GLOBAL: 2, // dst, nameIdx regs[dst] = globals[constants[nameIdx]]
57
- LOAD_UPVALUE: 3, // dst, uvIdx regs[dst] = upvalues[uvIdx].read()
58
- LOAD_THIS: 4, // dst regs[dst] = frame.thisVal
59
- MOVE: 5, // dst, src regs[dst] = regs[src]
60
-
61
- // ── Stores ────────────────────────────────────────────────────────────────
62
- STORE_GLOBAL: 6, // nameIdx, src globals[constants[nameIdx]] = regs[src]
63
- STORE_UPVALUE: 7, // uvIdx, src upvalues[uvIdx].write(regs[src])
64
-
65
- // ── Property access ───────────────────────────────────────────────────────
66
- GET_PROP: 8, // dst, obj, key regs[dst] = regs[obj][regs[key]]
67
- SET_PROP: 9, // obj, key, val regs[obj][regs[key]] = regs[val] (result stays in val reg)
68
- DELETE_PROP: 10, // dst, obj, key regs[dst] = delete regs[obj][regs[key]]
69
-
70
- // ── Arithmetic / bitwise (dst, src1, src2) ───────────────────────────────
71
- ADD: 11,
72
- SUB: 12,
73
- MUL: 13,
74
- DIV: 14,
75
- MOD: 15,
76
- BAND: 16,
77
- BOR: 17,
78
- BXOR: 18,
79
- SHL: 19,
80
- SHR: 20,
81
- USHR: 21,
82
-
83
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────────
84
- LT: 22,
85
- GT: 23,
86
- LTE: 24,
87
- GTE: 25,
88
- EQ: 26,
89
- NEQ: 27,
90
- LOOSE_EQ: 28,
91
- LOOSE_NEQ: 29,
92
- IN: 30,
93
- INSTANCEOF: 31,
94
-
95
- // ── Unary (dst, src) ─────────────────────────────────────────────────────
96
- UNARY_NEG: 32,
97
- UNARY_POS: 33,
98
- UNARY_NOT: 34,
99
- UNARY_BITNOT: 35,
100
- TYPEOF: 36, // dst, src
101
- VOID: 37, // dst, src – regs[dst] = undefined (src evaluated for side-effects)
102
- TYPEOF_SAFE: 38, // dst, nameConstIdx – safe typeof for potentially-undeclared globals
103
-
104
- // ── Control flow ──────────────────────────────────────────────────────────
105
- JUMP: 39, // target
106
- JUMP_IF_FALSE: 40, // src, target if !regs[src] then pc = target
107
- JUMP_IF_TRUE: 41, // src, target if regs[src] then pc = target (|| short-circuit)
108
-
109
- // ── Calls & constructors ──────────────────────────────────────────────────
110
- CALL: 42, // dst, callee, argc, [argRegs…]
111
- CALL_METHOD: 43, // dst, receiver, callee, argc, [argRegs…]
112
- NEW: 44, // dst, callee, argc, [argRegs…]
113
- RETURN: 45, // src
114
- THROW: 46, // src
115
-
116
- // ── Closures ──────────────────────────────────────────────────────────────
117
- // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
118
- MAKE_CLOSURE: 47,
119
-
120
- // ── Collections ───────────────────────────────────────────────────────────
121
- BUILD_ARRAY: 48, // dst, count, [elemRegs…]
122
- BUILD_OBJECT: 49, // dst, pairCount, [keyReg, valReg, …]
123
-
124
- // ── Property definitions (getters / setters) ──────────────────────────────
125
- DEFINE_GETTER: 50, // obj, key, fn
126
- DEFINE_SETTER: 51, // obj, key, fn
127
-
128
- // ── For-in iteration ──────────────────────────────────────────────────────
129
- FOR_IN_SETUP: 52, // dst, src dst = { _keys: enumKeys(src), i: 0 }
130
- FOR_IN_NEXT: 53, // dst, iter, exitTarget
131
-
132
- // ── Exception handling ────────────────────────────────────────────────────
133
- TRY_SETUP: 54, // handlerPc, exceptionReg
134
- TRY_END: 55,
135
-
136
- // ── Self-modifying bytecode ───────────────────────────────────────────────
137
- PATCH: 56, // destPc, sliceStart, sliceEnd
138
-
139
- // ── Debug ─────────────────────────────────────────────────────────────────
140
- DEBUGGER: 57,
141
-
142
- // ── Indirect jump (register-addressed) ───────────────────────────────────
143
- // Used by Dispatcher pass. The target PC is read from a register
144
- // rather than encoded as a bytecode immediate, so static analysis cannot
145
- // determine the destination without tracking register values at runtime.
146
- JUMP_REG: 58, // src — frame._pc = regs[src]
147
- };
148
-
149
- // ── Scope ─────────────────────────────────────────────────────────────────────
150
- // Maps variable names to virtual RegisterOperands.
151
- // Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
152
- // resolveRegisters() assigns concrete slot indices before serialization.
153
- class Scope {
154
- parent: Scope | null;
155
- _locals: Map<string, b.RegisterOperand>;
156
-
157
- constructor(parent = null) {
158
- this.parent = parent;
159
- this._locals = new Map();
160
- }
161
-
162
- define(name: string, ctx: FnContext): b.RegisterOperand {
163
- if (!this._locals.has(name)) {
164
- this._locals.set(name, ctx._newReg());
165
- }
166
- return this._locals.get(name)!;
167
- }
168
-
169
- resolve(
170
- name: string,
171
- ): { kind: "local"; reg: b.RegisterOperand } | { kind: "global" } {
172
- if (this._locals.has(name)) {
173
- return { kind: "local", reg: this._locals.get(name)! };
174
- }
175
- if (this.parent) return this.parent.resolve(name);
176
- return { kind: "global" };
177
- }
178
- }
179
-
180
- // ── FnContext ─────────────────────────────────────────────────────────────────
181
- // Compiler-side state for the function currently being compiled.
182
- // Distinct from the runtime Frame — this is compile-time only.
183
- //
184
- // Virtual-register model (Lua/LLVM style):
185
- // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
186
- // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
187
- // liveness-aware slot assignment and sets desc.regCount at the end of the
188
- // pipeline, just like resolveLabels() fills in jump targets.
189
- class FnContext {
190
- // index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
191
- upvalues: {
192
- name: string;
193
- isLocal: number;
194
- index: number | b.RegisterOperand;
195
- }[];
196
- parentCtx: FnContext | null;
197
- scope: Scope;
198
- compiler: Compiler;
199
- bc: b.Instruction[];
200
-
201
- // Unique ID for this function — matches the index in compiler.fnDescriptors.
202
- _fnId: number;
203
- // Monotonically increasing counter; each call to _newReg() bumps it.
204
- _nextId: number = 0;
205
-
206
- constructor(
207
- compiler: Compiler,
208
- parentCtx: FnContext | null = null,
209
- fnId: number = 0,
210
- ) {
211
- this.compiler = compiler;
212
- this.parentCtx = parentCtx;
213
- this.scope = new Scope();
214
- this.bc = [];
215
- this.upvalues = [];
216
- this._fnId = fnId;
217
- }
218
-
219
- /** Create a new virtual register owned by this function. */
220
- _newReg(): b.RegisterOperand {
221
- return b.registerOperand(this._nextId++, this._fnId);
222
- }
223
-
224
- /**
225
- * Allocate a short-lived temporary register (pool "temp::").
226
- * resolveRegisters() will reuse its concrete slot once its live range ends.
227
- * Do NOT use for named locals or upvalue-captured variables — use _newReg()
228
- * via scope.define() for those, so they stay in the stable "local::" pool.
229
- */
230
- allocReg(): b.RegisterOperand {
231
- return b.registerOperand(this._nextId++, this._fnId, { kind: "temp" });
232
- }
233
-
234
- /**
235
- * Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
236
- *
237
- * NOTE: This is extraneous for any programmatically generated IR.
238
- * resolveRegisters() already computes lastUse as the last instruction index
239
- * where the register appears as a real operand — which is always the tightest
240
- * correct bound when you stop emitting a register after its last logical use.
241
- * freeReg is only needed in the rare case where a register has a late syntactic
242
- * appearance that does NOT represent its true logical death (e.g. a dummy read
243
- * emitted for side-effects long after the value is logically dead). No current
244
- * pass in this codebase uses it; it is kept as an extension point only.
245
- */
246
- freeReg(bc: b.Bytecode, reg: b.RegisterOperand): void {
247
- bc.push([null, b.freeRegOperand(reg)]);
248
- }
249
-
250
- /** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
251
- resetTemps(): void {}
252
-
253
- addUpvalue(
254
- name: string,
255
- isLocal: number,
256
- index: number | b.RegisterOperand,
257
- ): number {
258
- const existing = this.upvalues.findIndex((u) => u.name === name);
259
- if (existing !== -1) return existing;
260
- const idx = this.upvalues.length;
261
- this.upvalues.push({ name, isLocal, index });
262
- return idx;
263
- }
264
- }
265
-
266
- interface FnDescriptor {
267
- name?: string;
268
- entryLabel?: string;
269
- startLabel?: string;
270
- bytecode?: b.Bytecode;
271
- paramCount?: number;
272
- regCount?: number;
273
- upvalues?: any[];
274
- _fnIdx?: number;
275
-
276
- hasRest?: boolean;
277
-
278
- /**
279
- * Only populated AFTER resolveLabels
280
- */
281
- startPc?: number;
282
- ctx?: FnContext;
283
- }
284
-
285
- // ── Compiler ──────────────────────────────────────────────────────────────────
286
- export class Compiler {
287
- fnDescriptors: FnDescriptor[];
288
- bytecode: b.Bytecode;
289
- mainRegCount: number;
290
- mainFn: ReturnType<typeof this._compileFunctionDecl>;
291
- mainStartPc: number;
292
-
293
- _currentCtx: FnContext | null;
294
- _pendingLabel: string | null;
295
- _forInCount: number;
296
- _labelCount: number;
297
- _loopStack: {
298
- type: "loop" | "switch" | "block";
299
- label: string | null;
300
- breakLabel: string;
301
- continueLabel: string;
302
- }[];
303
-
304
- options: Options;
305
- serializer: Serializer;
306
-
307
- OP: Partial<typeof OP_ORIGINAL>;
308
- MACRO_OPS: Record<number, number[]>;
309
- SPECIALIZED_OPS: Record<
310
- number,
311
- {
312
- originalOp: number;
313
- operands: b.InstrOperand[];
314
- }
315
- >;
316
- ALIASED_OPS: Record<number, { originalOp: number; order: number[] }>;
317
- MICRO_OPS: Record<
318
- number,
319
- { originalOp: number; stmtIndex: number; irOperandCount: number }
320
- >;
321
-
322
- OP_NAME: Record<number, string>;
323
- JUMP_OPS: Set<number>;
324
-
325
- constants: any[];
326
-
327
- log(...messages: any[]) {
328
- if (this.options.verbose) {
329
- console.log(...messages);
330
- }
331
- }
332
-
333
- _cloneRegisterOperand<T extends b.InstrOperand>(operand: T): T {
334
- if (!operand || typeof operand !== "object") return operand;
335
- if ((operand as any).type !== "register") return operand;
336
-
337
- return JSON.parse(JSON.stringify(operand)) as T;
338
- }
339
-
340
- emit(bc: b.Bytecode, instr: b.Instruction, node: t.Node) {
341
- for (let i = 1; i < instr.length; i++) {
342
- instr[i] = this._cloneRegisterOperand(instr[i]);
343
- }
344
- bc.push(instr);
345
- instr[SOURCE_NODE_SYM] = node;
346
- }
347
-
348
- constructor(options: Options = DEFAULT_OPTIONS) {
349
- this.options = options;
350
- this.fnDescriptors = [];
351
- this.bytecode = [];
352
- this.mainStartPc = 0;
353
- this.mainRegCount = 0;
354
- this._currentCtx = null;
355
- this._loopStack = [];
356
- this._pendingLabel = null;
357
- this._forInCount = 0;
358
- this._labelCount = 0;
359
-
360
- this.serializer = new Serializer(this);
361
- this.MACRO_OPS = {};
362
- this.MICRO_OPS = {};
363
- this.SPECIALIZED_OPS = {};
364
- this.ALIASED_OPS = {};
365
-
366
- this.OP = { ...OP_ORIGINAL };
367
-
368
- if (this.options.randomizeOpcodes) {
369
- let usedNumbers = new Set<number>();
370
- for (const key in this.OP) {
371
- let val;
372
- do {
373
- val = getRandomInt(0, U16_MAX);
374
- } while (usedNumbers.has(val));
375
- usedNumbers.add(val);
376
- this.OP[key] = val;
377
- }
378
- }
379
-
380
- this.OP_NAME = Object.fromEntries(
381
- Object.entries(this.OP).map(([k, v]) => [v, k]),
382
- );
383
-
384
- this.JUMP_OPS = new Set([
385
- this.OP.JUMP,
386
- this.OP.JUMP_IF_FALSE,
387
- this.OP.JUMP_IF_TRUE,
388
- this.OP.FOR_IN_NEXT,
389
- this.OP.TRY_SETUP,
390
- ]);
391
- }
392
-
393
- _makeLabel(hint = ""): string {
394
- return `${hint || "L"}_${this._labelCount++}`;
395
- }
396
-
397
- _resolve(
398
- name: string,
399
- ctx: FnContext | null,
400
- ):
401
- | { kind: "local"; reg: b.RegisterOperand }
402
- | { kind: "upvalue"; index: number }
403
- | { kind: "global" } {
404
- if (!ctx) return { kind: "global" };
405
-
406
- if (ctx.scope._locals.has(name)) {
407
- return { kind: "local", reg: ctx.scope._locals.get(name)! };
408
- }
409
-
410
- if (!ctx.parentCtx) return { kind: "global" };
411
-
412
- const parentResult = this._resolve(name, ctx.parentCtx);
413
- if (parentResult.kind === "global") return { kind: "global" };
414
-
415
- const isLocal = parentResult.kind === "local";
416
- const index = isLocal ? parentResult.reg : parentResult.index;
417
- const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
418
- return { kind: "upvalue", index: uvIdx };
419
- }
420
-
421
- // ── Variable hoisting ──────────────────────────────────────────────────────
422
- // Pre-scan a statement list and reserve virtual registers for every var
423
- // declaration, function declaration, for-in iterator, and try-catch binding.
424
- // Must be called before any emit so that locals are allocated before temps.
425
- _hoistVars(stmts: t.Statement[], scope: Scope, ctx: FnContext): void {
426
- for (const stmt of stmts) {
427
- switch (stmt.type) {
428
- case "VariableDeclaration":
429
- for (const decl of stmt.declarations) {
430
- if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
431
- }
432
- break;
433
-
434
- case "FunctionDeclaration":
435
- if (stmt.id) scope.define(stmt.id.name, ctx);
436
- break;
437
-
438
- case "BlockStatement":
439
- this._hoistVars(stmt.body, scope, ctx);
440
- break;
441
-
442
- case "IfStatement": {
443
- const cons =
444
- stmt.consequent.type === "BlockStatement"
445
- ? stmt.consequent.body
446
- : [stmt.consequent];
447
- this._hoistVars(cons, scope, ctx);
448
- if (stmt.alternate) {
449
- const alt =
450
- stmt.alternate.type === "BlockStatement"
451
- ? stmt.alternate.body
452
- : [stmt.alternate];
453
- this._hoistVars(alt, scope, ctx);
454
- }
455
- break;
456
- }
457
-
458
- case "WhileStatement":
459
- case "DoWhileStatement": {
460
- const body =
461
- stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
462
- this._hoistVars(body, scope, ctx);
463
- break;
464
- }
465
-
466
- case "ForStatement": {
467
- if (stmt.init?.type === "VariableDeclaration") {
468
- for (const decl of stmt.init.declarations) {
469
- if (decl.id.type === "Identifier")
470
- scope.define(decl.id.name, ctx);
471
- }
472
- }
473
- const body =
474
- stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
475
- this._hoistVars(body, scope, ctx);
476
- break;
477
- }
478
-
479
- case "ForInStatement": {
480
- // Reserve a hidden virtual register for the iterator object.
481
- (stmt as any)._iterSlot = ctx._newReg();
482
- if (stmt.left.type === "VariableDeclaration") {
483
- for (const decl of stmt.left.declarations) {
484
- if (decl.id.type === "Identifier")
485
- scope.define(decl.id.name, ctx);
486
- }
487
- }
488
- const body =
489
- stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
490
- this._hoistVars(body, scope, ctx);
491
- break;
492
- }
493
-
494
- case "SwitchStatement":
495
- for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
496
- break;
497
-
498
- case "TryStatement":
499
- this._hoistVars(stmt.block.body, scope, ctx);
500
- if (stmt.handler) {
501
- if (stmt.handler.param?.type === "Identifier") {
502
- // Catch parameter IS the exception register.
503
- scope.define((stmt.handler.param as t.Identifier).name, ctx);
504
- } else {
505
- // No catch binding – reserve a dummy virtual register for the exception value.
506
- (stmt as any)._exceptionSlot = ctx._newReg();
507
- }
508
- this._hoistVars(stmt.handler.body.body, scope, ctx);
509
- }
510
- break;
511
-
512
- case "LabeledStatement":
513
- this._hoistVars([stmt.body], scope, ctx);
514
- break;
515
- }
516
- }
517
- }
518
-
519
- profileData?: Partial<b.ObfuscationResult["profileData"]> = {
520
- transforms: {},
521
- };
522
-
523
- // ── Entry point ───────────────────────────────────────────────────────────
524
- compile(source: string) {
525
- let startedAt = now();
526
-
527
- const ast = parse(source, {
528
- sourceType: "script",
529
- allowReturnOutsideFunction: true,
530
- });
531
-
532
- this.profileData.parseTime = now() - startedAt;
533
-
534
- return this.compileAST(ast);
535
- }
536
-
537
- compileAST(ast: t.File) {
538
- let startedAt = now();
539
-
540
- this._compileMain(ast.program.body);
541
-
542
- this.profileData.compileTime = now() - startedAt;
543
-
544
- return this.bytecode;
545
- }
546
-
547
- // ── Function compilation ───────────────────────────────────────────────────
548
- _compileFunctionDecl(node: t.FunctionDeclaration | t.FunctionExpression) {
549
- ok(!node.generator, "Generator functions are not supported");
550
- ok(!node.async, "Async functions are not supported");
551
-
552
- var fnIdx = this.fnDescriptors.length;
553
- const entryLabel = this._makeLabel(`fn_${fnIdx}`);
554
- var desc: FnDescriptor = {};
555
- this.fnDescriptors.push(desc);
556
-
557
- const ctx = new FnContext(this, this._currentCtx, fnIdx);
558
- const savedCtx = this._currentCtx;
559
- this._currentCtx = ctx;
560
-
561
- const savedLoopStack = this._loopStack;
562
- this._loopStack = [];
563
-
564
- // 1. Define parameters as virtual registers (occupy the first IDs in order).
565
- let hasRest = false;
566
- for (const param of node.params) {
567
- if (param.type === "RestElement") {
568
- ok(
569
- param.argument.type === "Identifier",
570
- "Rest element must be a simple identifier",
571
- );
572
- hasRest = true;
573
- ctx.scope.define((param.argument as t.Identifier).name, ctx);
574
- } else {
575
- let identifier =
576
- param.type === "AssignmentPattern" ? param.left : param;
577
- ok(
578
- identifier.type === "Identifier",
579
- "Only simple identifiers allowed as parameters",
580
- );
581
- ctx.scope.define((identifier as t.Identifier).name, ctx);
582
- }
583
- }
584
-
585
- // 2. Reserve the `arguments` virtual register (immediately after params).
586
- ctx.scope.define("arguments", ctx);
587
-
588
- // 3. Hoist all var declarations so locals are allocated before any temps.
589
- this._hoistVars(node.body.body, ctx.scope, ctx);
590
-
591
- // 5. Emit default-value guards.
592
- for (const param of node.params) {
593
- if (param.type !== "AssignmentPattern") continue;
594
-
595
- const slot = ctx.scope._locals.get((param.left as t.Identifier).name)!;
596
- const skipLabel = this._makeLabel("param_skip");
597
-
598
- // if (param === undefined) param = <default>
599
- const reg_undef = ctx.allocReg();
600
- this.emit(
601
- ctx.bc,
602
- [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)],
603
- param,
604
- );
605
- const reg_cmp = ctx.allocReg();
606
- this.emit(ctx.bc, [this.OP.EQ, reg_cmp, slot, reg_undef], param);
607
- this.emit(
608
- ctx.bc,
609
- [this.OP.JUMP_IF_FALSE, reg_cmp, { type: "label", label: skipLabel }],
610
- param,
611
- );
612
- ctx.resetTemps();
613
-
614
- const srcReg = this._compileExpr(param.right, ctx.scope, ctx.bc);
615
- if (srcReg !== slot) {
616
- this.emit(ctx.bc, [this.OP.MOVE, slot, srcReg], param);
617
- }
618
- ctx.resetTemps();
619
-
620
- this.emit(
621
- ctx.bc,
622
- [null, { type: "defineLabel", label: skipLabel }],
623
- param,
624
- );
625
- }
626
-
627
- // 6. Compile body.
628
- for (const stmt of node.body.body) {
629
- this._compileStatement(stmt, ctx.scope, ctx.bc);
630
- }
631
-
632
- // Implicit return undefined at end of function.
633
- const reg_undef = ctx.allocReg();
634
- this.emit(
635
- ctx.bc,
636
- [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)],
637
- node,
638
- );
639
- this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
640
-
641
- this._currentCtx = savedCtx;
642
- this._loopStack = savedLoopStack;
643
-
644
- (node as any)._fnIdx = fnIdx;
645
-
646
- desc.name = (node as any).id?.name || "<anonymous>";
647
- desc.entryLabel = entryLabel;
648
- desc.bytecode = ctx.bc as b.Bytecode;
649
- desc._fnIdx = fnIdx;
650
- desc.paramCount = node.params.length;
651
- desc.hasRest = hasRest;
652
- // regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
653
- desc.upvalues = ctx.upvalues.slice();
654
- desc.ctx = ctx;
655
-
656
- return desc;
657
- }
658
-
659
- // Emit MAKE_CLOSURE with all metadata as inline operands.
660
- // Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
661
- // regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
662
- _emitMakeClosure(desc: any, node: t.Node, bc: b.Bytecode) {
663
- const ctx = this._currentCtx!;
664
- const dst = ctx.allocReg();
665
- const uvOperands: b.InstrOperand[] = [];
666
- for (const uv of desc.upvalues) {
667
- uvOperands.push(uv.isLocal ? 1 : 0);
668
- uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
669
- }
670
- this.emit(
671
- bc,
672
- [
673
- this.OP.MAKE_CLOSURE,
674
- dst,
675
- { type: "label", label: desc.entryLabel },
676
- desc.paramCount,
677
- b.fnRegCountOperand(desc._fnIdx), // resolved by resolveRegisters()
678
- desc.upvalues.length,
679
- desc.hasRest ? 1 : 0, // 1 = last param is a rest element
680
- ...uvOperands,
681
- ] as b.Instruction,
682
- node,
683
- );
684
- return dst;
685
- }
686
-
687
- // ── Main (top-level) ───────────────────────────────────────────────────────
688
- _compileMain(body: t.Statement[]) {
689
- const mainCtx = new FnContext(this, null);
690
- const savedCtx = this._currentCtx;
691
- this._currentCtx = mainCtx;
692
-
693
- var desc = this._compileFunctionDecl({
694
- type: "FunctionDeclaration",
695
- async: false,
696
- generator: false,
697
- params: [],
698
- id: t.identifier("main"),
699
- body: t.blockStatement([...body]),
700
- });
701
-
702
- for (const descriptor of this.fnDescriptors) {
703
- this.bytecode.push([
704
- null,
705
- { type: "defineLabel", label: descriptor.entryLabel },
706
- ]);
707
- for (const instr of descriptor.bytecode) {
708
- this.bytecode.push(instr);
709
- }
710
- }
711
-
712
- // mainRegCount is set by resolveRegisters() after the pipeline runs.
713
- this.mainFn = desc;
714
- this._currentCtx = savedCtx;
715
- }
716
-
717
- // ── Statements ────────────────────────────────────────────────────────────
718
- // Wrapper that resets temps after every statement so that short-lived
719
- // expression temps don't accumulate across statements.
720
- _compileStatement(node: t.Statement, scope: Scope | null, bc: b.Bytecode) {
721
- this._compileStatementImpl(node, scope, bc);
722
- this._currentCtx?.resetTemps();
723
- }
724
-
725
- _compileStatementImpl(
726
- node: t.Statement,
727
- scope: Scope | null,
728
- bc: b.Bytecode,
729
- ) {
730
- const ctx = this._currentCtx!;
731
-
732
- switch (node.type) {
733
- case "EmptyStatement":
734
- break;
735
-
736
- case "DebuggerStatement":
737
- this.emit(bc, [this.OP.DEBUGGER], node);
738
- break;
739
-
740
- case "BlockStatement":
741
- for (const stmt of node.body) {
742
- this._compileStatement(stmt, scope, bc);
743
- }
744
- break;
745
-
746
- case "FunctionDeclaration": {
747
- const desc = this._compileFunctionDecl(node);
748
- const closureReg = this._emitMakeClosure(desc, node, bc);
749
- if (scope) {
750
- const slot = scope._locals.get(node.id!.name)!;
751
- if (closureReg !== slot) {
752
- this.emit(bc, [this.OP.MOVE, slot, closureReg], node);
753
- }
754
- } else {
755
- this.emit(
756
- bc,
757
- [
758
- this.OP.STORE_GLOBAL,
759
- b.constantOperand(node.id!.name),
760
- closureReg,
761
- ],
762
- node,
763
- );
764
- }
765
- break;
766
- }
767
-
768
- case "ThrowStatement": {
769
- const reg = this._compileExpr(node.argument, scope, bc);
770
- this.emit(bc, [this.OP.THROW, reg], node);
771
- break;
772
- }
773
-
774
- case "ReturnStatement": {
775
- let reg: b.RegisterOperand;
776
- if (node.argument) {
777
- reg = this._compileExpr(node.argument, scope, bc);
778
- } else {
779
- reg = ctx.allocReg();
780
- this.emit(
781
- bc,
782
- [this.OP.LOAD_CONST, reg, b.constantOperand(undefined)],
783
- node,
784
- );
785
- }
786
- for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
787
- if ((this._loopStack[_ri].type as any) === "try") {
788
- this.emit(bc, [this.OP.TRY_END], node);
789
- }
790
- }
791
- this.emit(bc, [this.OP.RETURN, reg], node);
792
- break;
793
- }
794
-
795
- case "ExpressionStatement":
796
- this._compileExpr(node.expression, scope, bc);
797
- // Result is discarded; resetTemps in the wrapper handles cleanup.
798
- break;
799
-
800
- case "VariableDeclaration": {
801
- for (const decl of node.declarations) {
802
- ok(
803
- decl.id.type === "Identifier",
804
- "Only simple identifiers can be declared",
805
- );
806
- const name = (decl.id as t.Identifier).name;
807
-
808
- if (scope) {
809
- const slot = scope._locals.get(name)!; // already defined by _hoistVars
810
- if (decl.init) {
811
- const srcReg = this._compileExpr(decl.init, scope, bc);
812
- if (srcReg !== slot) {
813
- this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
814
- }
815
- } else {
816
- // No initializer: var x; → load undefined directly into the local's register.
817
- this.emit(
818
- bc,
819
- [this.OP.LOAD_CONST, slot, b.constantOperand(undefined)],
820
- node,
821
- );
822
- }
823
- } else {
824
- if (decl.init) {
825
- const srcReg = this._compileExpr(decl.init, scope, bc);
826
- this.emit(
827
- bc,
828
- [this.OP.STORE_GLOBAL, b.constantOperand(name), srcReg],
829
- node,
830
- );
831
- } else {
832
- const tmp = ctx.allocReg();
833
- this.emit(
834
- bc,
835
- [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)],
836
- node,
837
- );
838
- this.emit(
839
- bc,
840
- [this.OP.STORE_GLOBAL, b.constantOperand(name), tmp],
841
- node,
842
- );
843
- }
844
- }
845
- }
846
- break;
847
- }
848
-
849
- case "IfStatement": {
850
- const elseOrEndLabel = this._makeLabel("if_else");
851
-
852
- const testReg = this._compileExpr(node.test, scope, bc);
853
- this.emit(
854
- bc,
855
- [
856
- this.OP.JUMP_IF_FALSE,
857
- testReg,
858
- { type: "label", label: elseOrEndLabel },
859
- ],
860
- node,
861
- );
862
-
863
- const consequentBody =
864
- node.consequent.type === "BlockStatement"
865
- ? node.consequent.body
866
- : [node.consequent];
867
- for (const stmt of consequentBody) {
868
- this._compileStatement(stmt, scope, bc);
869
- }
870
-
871
- if (node.alternate) {
872
- const endLabel = this._makeLabel("if_end");
873
- this.emit(
874
- bc,
875
- [this.OP.JUMP, { type: "label", label: endLabel }],
876
- node,
877
- );
878
- this.emit(
879
- bc,
880
- [null, { type: "defineLabel", label: elseOrEndLabel }],
881
- node,
882
- );
883
- const altBody =
884
- node.alternate.type === "BlockStatement"
885
- ? node.alternate.body
886
- : [node.alternate];
887
- for (const stmt of altBody) {
888
- this._compileStatement(stmt, scope, bc);
889
- }
890
- this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
891
- } else {
892
- this.emit(
893
- bc,
894
- [null, { type: "defineLabel", label: elseOrEndLabel }],
895
- node,
896
- );
897
- }
898
- break;
899
- }
900
-
901
- case "WhileStatement": {
902
- const _wLabel = this._pendingLabel;
903
- this._pendingLabel = null;
904
-
905
- const loopTopLabel = this._makeLabel("while_top");
906
- const exitLabel = this._makeLabel("while_exit");
907
-
908
- this._loopStack.push({
909
- type: "loop",
910
- label: _wLabel,
911
- breakLabel: exitLabel,
912
- continueLabel: loopTopLabel,
913
- });
914
-
915
- this.emit(
916
- bc,
917
- [null, { type: "defineLabel", label: loopTopLabel }],
918
- node,
919
- );
920
-
921
- const testReg = this._compileExpr(node.test, scope, bc);
922
- this.emit(
923
- bc,
924
- [this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
925
- node,
926
- );
927
-
928
- const whileBody =
929
- node.body.type === "BlockStatement" ? node.body.body : [node.body];
930
- for (const stmt of whileBody) {
931
- this._compileStatement(stmt, scope, bc);
932
- }
933
-
934
- this.emit(
935
- bc,
936
- [this.OP.JUMP, { type: "label", label: loopTopLabel }],
937
- node,
938
- );
939
- this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
940
-
941
- this._loopStack.pop();
942
- break;
943
- }
944
-
945
- case "DoWhileStatement": {
946
- const _dwLabel = this._pendingLabel;
947
- this._pendingLabel = null;
948
-
949
- const loopTopLabel = this._makeLabel("dowhile_top");
950
- const continueLabel = this._makeLabel("dowhile_cont");
951
- const exitLabel = this._makeLabel("dowhile_exit");
952
-
953
- this._loopStack.push({
954
- type: "loop",
955
- label: _dwLabel,
956
- breakLabel: exitLabel,
957
- continueLabel: continueLabel,
958
- });
959
-
960
- this.emit(
961
- bc,
962
- [null, { type: "defineLabel", label: loopTopLabel }],
963
- node,
964
- );
965
-
966
- const doWhileBody =
967
- node.body.type === "BlockStatement" ? node.body.body : [node.body];
968
- for (const stmt of doWhileBody) {
969
- this._compileStatement(stmt, scope, bc);
970
- }
971
-
972
- this.emit(
973
- bc,
974
- [null, { type: "defineLabel", label: continueLabel }],
975
- node,
976
- );
977
-
978
- const testReg = this._compileExpr(node.test, scope, bc);
979
- this.emit(
980
- bc,
981
- [this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
982
- node,
983
- );
984
-
985
- this.emit(
986
- bc,
987
- [this.OP.JUMP, { type: "label", label: loopTopLabel }],
988
- node,
989
- );
990
-
991
- this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
992
- this._loopStack.pop();
993
- break;
994
- }
995
-
996
- case "ForStatement": {
997
- const _fLabel = this._pendingLabel;
998
- this._pendingLabel = null;
999
-
1000
- const loopTopLabel = this._makeLabel("for_top");
1001
- const exitLabel = this._makeLabel("for_exit");
1002
- const updateLabel = node.update
1003
- ? this._makeLabel("for_update")
1004
- : loopTopLabel;
1005
-
1006
- this._loopStack.push({
1007
- type: "loop",
1008
- label: _fLabel,
1009
- breakLabel: exitLabel,
1010
- continueLabel: updateLabel,
1011
- });
1012
-
1013
- if (node.init) {
1014
- if (node.init.type === "VariableDeclaration") {
1015
- this._compileStatement(node.init, scope, bc);
1016
- } else {
1017
- this._compileExpr(node.init as t.Expression, scope, bc);
1018
- // result discarded; resetTemps in next iteration
1019
- }
1020
- }
1021
-
1022
- this.emit(
1023
- bc,
1024
- [null, { type: "defineLabel", label: loopTopLabel }],
1025
- node,
1026
- );
1027
-
1028
- if (node.test) {
1029
- const testReg = this._compileExpr(node.test, scope, bc);
1030
- this.emit(
1031
- bc,
1032
- [
1033
- this.OP.JUMP_IF_FALSE,
1034
- testReg,
1035
- { type: "label", label: exitLabel },
1036
- ],
1037
- node,
1038
- );
1039
- }
1040
-
1041
- const forBody =
1042
- node.body.type === "BlockStatement" ? node.body.body : [node.body];
1043
- for (const stmt of forBody) {
1044
- this._compileStatement(stmt, scope, bc);
1045
- }
1046
-
1047
- if (node.update) {
1048
- this.emit(
1049
- bc,
1050
- [null, { type: "defineLabel", label: updateLabel }],
1051
- node,
1052
- );
1053
- this._compileExpr(node.update, scope, bc);
1054
- ctx.resetTemps(); // discard update expression result
1055
- }
1056
-
1057
- this.emit(
1058
- bc,
1059
- [this.OP.JUMP, { type: "label", label: loopTopLabel }],
1060
- node,
1061
- );
1062
- this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
1063
-
1064
- this._loopStack.pop();
1065
- break;
1066
- }
1067
-
1068
- case "BreakStatement": {
1069
- let _bTargetIdx = -1;
1070
- if (node.label) {
1071
- const _bLabelName = node.label.name;
1072
- for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
1073
- if (this._loopStack[_bi].label === _bLabelName) {
1074
- _bTargetIdx = _bi;
1075
- break;
1076
- }
1077
- }
1078
- if (_bTargetIdx === -1)
1079
- throw new Error(`Label '${node.label.name}' not found`);
1080
- } else {
1081
- for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
1082
- if ((this._loopStack[_bi].type as any) !== "try") {
1083
- _bTargetIdx = _bi;
1084
- break;
1085
- }
1086
- }
1087
- if (_bTargetIdx === -1) throw new Error("break outside loop");
1088
- }
1089
- for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
1090
- if ((this._loopStack[_bi].type as any) === "try") {
1091
- this.emit(bc, [this.OP.TRY_END], node);
1092
- }
1093
- }
1094
- this.emit(
1095
- bc,
1096
- [
1097
- this.OP.JUMP,
1098
- { type: "label", label: this._loopStack[_bTargetIdx].breakLabel },
1099
- ],
1100
- node,
1101
- );
1102
- break;
1103
- }
1104
-
1105
- case "ContinueStatement": {
1106
- let _cTargetIdx = -1;
1107
- if (node.label) {
1108
- const _cLabelName = node.label.name;
1109
- for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
1110
- if (
1111
- this._loopStack[_ci].label === _cLabelName &&
1112
- this._loopStack[_ci].type === "loop"
1113
- ) {
1114
- _cTargetIdx = _ci;
1115
- break;
1116
- }
1117
- }
1118
- if (_cTargetIdx === -1)
1119
- throw new Error(
1120
- `Label '${node.label.name}' not found for continue`,
1121
- );
1122
- } else {
1123
- for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
1124
- if (this._loopStack[_ci].type === "loop") {
1125
- _cTargetIdx = _ci;
1126
- break;
1127
- }
1128
- }
1129
- if (_cTargetIdx === -1) throw new Error("continue outside loop");
1130
- }
1131
- for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
1132
- if ((this._loopStack[_ci].type as any) === "try") {
1133
- this.emit(bc, [this.OP.TRY_END], node);
1134
- }
1135
- }
1136
- this.emit(
1137
- bc,
1138
- [
1139
- this.OP.JUMP,
1140
- {
1141
- type: "label",
1142
- label: this._loopStack[_cTargetIdx].continueLabel,
1143
- },
1144
- ],
1145
- node,
1146
- );
1147
- break;
1148
- }
1149
-
1150
- case "SwitchStatement": {
1151
- const _swLabel = this._pendingLabel;
1152
- this._pendingLabel = null;
1153
-
1154
- const switchBreakLabel = this._makeLabel("sw_break");
1155
-
1156
- this._loopStack.push({
1157
- type: "switch",
1158
- label: _swLabel,
1159
- breakLabel: switchBreakLabel,
1160
- continueLabel: switchBreakLabel,
1161
- });
1162
-
1163
- // Compile discriminant into a register that lives for the whole switch.
1164
- const discReg = this._compileExpr(node.discriminant, scope, bc);
1165
-
1166
- const cases = node.cases;
1167
- const defaultIdx = cases.findIndex((c) => c.test === null);
1168
- const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
1169
-
1170
- // Dispatch: for each non-default case, test and jump.
1171
- for (let i = 0; i < cases.length; i++) {
1172
- const cas = cases[i];
1173
- if (cas.test === null) continue;
1174
-
1175
- const nextCheckLabel = this._makeLabel("sw_next");
1176
- const caseValReg = this._compileExpr(cas.test, scope, bc);
1177
- const cmpReg = ctx.allocReg();
1178
- this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
1179
- this.emit(
1180
- bc,
1181
- [
1182
- this.OP.JUMP_IF_FALSE,
1183
- cmpReg,
1184
- { type: "label", label: nextCheckLabel },
1185
- ],
1186
- node,
1187
- );
1188
-
1189
- this.emit(
1190
- bc,
1191
- [this.OP.JUMP, { type: "label", label: caseLabels[i] }],
1192
- node,
1193
- );
1194
- this.emit(
1195
- bc,
1196
- [null, { type: "defineLabel", label: nextCheckLabel }],
1197
- node,
1198
- );
1199
- }
1200
-
1201
- this.emit(
1202
- bc,
1203
- [
1204
- this.OP.JUMP,
1205
- {
1206
- type: "label",
1207
- label:
1208
- defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel,
1209
- },
1210
- ],
1211
- node,
1212
- );
1213
-
1214
- for (let i = 0; i < cases.length; i++) {
1215
- this.emit(
1216
- bc,
1217
- [null, { type: "defineLabel", label: caseLabels[i] }],
1218
- node,
1219
- );
1220
- for (const stmt of cases[i].consequent) {
1221
- this._compileStatement(stmt, scope, bc);
1222
- }
1223
- }
1224
-
1225
- // Break lands here – discriminant register is simply abandoned.
1226
- this.emit(
1227
- bc,
1228
- [null, { type: "defineLabel", label: switchBreakLabel }],
1229
- node,
1230
- );
1231
-
1232
- this._loopStack.pop();
1233
- break;
1234
- }
1235
-
1236
- case "LabeledStatement": {
1237
- const _lName = node.label.name;
1238
- const _lBody = node.body;
1239
- const _lIsLoop =
1240
- _lBody.type === "ForStatement" ||
1241
- _lBody.type === "WhileStatement" ||
1242
- _lBody.type === "DoWhileStatement" ||
1243
- _lBody.type === "ForInStatement";
1244
- const _lIsSwitch = _lBody.type === "SwitchStatement";
1245
-
1246
- if (_lIsLoop || _lIsSwitch) {
1247
- this._pendingLabel = _lName;
1248
- this._compileStatement(_lBody, scope, bc);
1249
- this._pendingLabel = null;
1250
- } else {
1251
- const blockBreakLabel = this._makeLabel("block_break");
1252
- this._loopStack.push({
1253
- type: "block",
1254
- label: _lName,
1255
- breakLabel: blockBreakLabel,
1256
- continueLabel: blockBreakLabel,
1257
- });
1258
- this._compileStatement(_lBody, scope, bc);
1259
- this._loopStack.pop();
1260
- this.emit(
1261
- bc,
1262
- [null, { type: "defineLabel", label: blockBreakLabel }],
1263
- node,
1264
- );
1265
- }
1266
- break;
1267
- }
1268
-
1269
- case "ForInStatement": {
1270
- const _fiLabel = this._pendingLabel;
1271
- this._pendingLabel = null;
1272
-
1273
- // Iterator register was reserved by _hoistVars.
1274
- const iterSlot: b.RegisterOperand = (node as any)._iterSlot;
1275
-
1276
- // FOR_IN_SETUP dst, src
1277
- const objReg = this._compileExpr(node.right, scope, bc);
1278
- this.emit(bc, [this.OP.FOR_IN_SETUP, iterSlot, objReg], node);
1279
-
1280
- const loopTopLabel = this._makeLabel("forin_top");
1281
- const exitLabel = this._makeLabel("forin_exit");
1282
-
1283
- this._loopStack.push({
1284
- type: "loop",
1285
- label: _fiLabel,
1286
- breakLabel: exitLabel,
1287
- continueLabel: loopTopLabel,
1288
- });
1289
-
1290
- this.emit(
1291
- bc,
1292
- [null, { type: "defineLabel", label: loopTopLabel }],
1293
- node,
1294
- );
1295
-
1296
- // FOR_IN_NEXT keyDst, iter, exitTarget
1297
- const keyReg = ctx.allocReg();
1298
- this.emit(
1299
- bc,
1300
- [
1301
- this.OP.FOR_IN_NEXT,
1302
- keyReg,
1303
- iterSlot,
1304
- { type: "label", label: exitLabel },
1305
- ],
1306
- node,
1307
- );
1308
-
1309
- // Assign the key to the loop variable.
1310
- if (node.left.type === "VariableDeclaration") {
1311
- const identifier = node.left.declarations[0].id;
1312
- ok(
1313
- identifier.type === "Identifier",
1314
- "Only simple identifiers can be declared in for-in loops",
1315
- );
1316
- const name = (identifier as t.Identifier).name;
1317
- if (scope) {
1318
- const slot = scope._locals.get(name)!;
1319
- if (keyReg !== slot)
1320
- this.emit(bc, [this.OP.MOVE, slot, keyReg], node);
1321
- } else {
1322
- this.emit(
1323
- bc,
1324
- [this.OP.STORE_GLOBAL, b.constantOperand(name), keyReg],
1325
- node,
1326
- );
1327
- }
1328
- } else if (node.left.type === "Identifier") {
1329
- const res = this._resolve(node.left.name, this._currentCtx);
1330
- if (res.kind === "local") {
1331
- if (keyReg !== res.reg)
1332
- this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
1333
- } else if (res.kind === "upvalue") {
1334
- this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
1335
- } else {
1336
- this.emit(
1337
- bc,
1338
- [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name), keyReg],
1339
- node,
1340
- );
1341
- }
1342
- } else {
1343
- const src = generate(node.left).code;
1344
- throw new Error(
1345
- `Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`,
1346
- );
1347
- }
1348
-
1349
- const fiBody =
1350
- node.body.type === "BlockStatement" ? node.body.body : [node.body];
1351
- for (const stmt of fiBody) {
1352
- this._compileStatement(stmt, scope, bc);
1353
- }
1354
-
1355
- this.emit(
1356
- bc,
1357
- [this.OP.JUMP, { type: "label", label: loopTopLabel }],
1358
- node,
1359
- );
1360
- this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
1361
-
1362
- this._loopStack.pop();
1363
- break;
1364
- }
1365
-
1366
- case "TryStatement": {
1367
- if (node.finalizer) {
1368
- throw new Error("try..finally is not supported");
1369
- }
1370
- if (!node.handler) {
1371
- throw new Error("try without catch is not supported");
1372
- }
1373
-
1374
- const catchLabel = this._makeLabel("catch");
1375
- const afterCatchLabel = this._makeLabel("after_catch");
1376
-
1377
- // Determine where the caught exception is written.
1378
- const exceptionReg =
1379
- node.handler.param?.type === "Identifier"
1380
- ? (scope?._locals.get((node.handler.param as t.Identifier).name) ??
1381
- ctx.allocReg()) // shouldn't normally reach here
1382
- : (node as any)._exceptionSlot;
1383
-
1384
- this.emit(
1385
- bc,
1386
- [
1387
- this.OP.TRY_SETUP,
1388
- { type: "label", label: catchLabel },
1389
- exceptionReg,
1390
- ],
1391
- node,
1392
- );
1393
-
1394
- this._loopStack.push({
1395
- type: "try" as any,
1396
- label: null,
1397
- breakLabel: "",
1398
- continueLabel: "",
1399
- });
1400
-
1401
- for (const stmt of node.block.body) {
1402
- this._compileStatement(stmt, scope, bc);
1403
- }
1404
-
1405
- this._loopStack.pop();
1406
-
1407
- this.emit(bc, [this.OP.TRY_END], node);
1408
- this.emit(
1409
- bc,
1410
- [this.OP.JUMP, { type: "label", label: afterCatchLabel }],
1411
- node,
1412
- );
1413
-
1414
- // Catch block: exceptionReg already holds the caught value.
1415
- this.emit(bc, [null, { type: "defineLabel", label: catchLabel }], node);
1416
-
1417
- // If no param binding, just ignore the exception (it's in the dummy slot).
1418
- for (const stmt of node.handler!.body.body) {
1419
- this._compileStatement(stmt, scope, bc);
1420
- }
1421
-
1422
- this.emit(
1423
- bc,
1424
- [null, { type: "defineLabel", label: afterCatchLabel }],
1425
- node,
1426
- );
1427
- break;
1428
- }
1429
-
1430
- default: {
1431
- const src = generate(node).code;
1432
- throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
1433
- }
1434
- }
1435
- }
1436
-
1437
- // ── Expressions ───────────────────────────────────────────────────────────
1438
- // Returns the virtual RegisterOperand that holds the result.
1439
- // For local variables: returns their RegisterOperand directly (no instruction emitted).
1440
- // For all others: allocates a fresh virtual register, emits the instruction(s),
1441
- // and returns the allocated register.
1442
- _compileExpr(
1443
- node: t.Expression | t.Node,
1444
- scope: Scope | null,
1445
- bc: b.Bytecode,
1446
- ): b.RegisterOperand {
1447
- const ctx = this._currentCtx!;
1448
-
1449
- // Intrinsic for emitting raw bytecode, useful for emitting register address
1450
- if (
1451
- node.type === "CallExpression" &&
1452
- node.callee.type === "Identifier" &&
1453
- node.callee.name === "_VM_"
1454
- ) {
1455
- const argJSONStrng = (node.arguments[0] as t.StringLiteral).value;
1456
- console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
1457
- const arg = JSON.parse(argJSONStrng);
1458
- console.log("Parsed bytecode:", arg);
1459
-
1460
- const dst = ctx.allocReg();
1461
-
1462
- let operand = arg[0];
1463
-
1464
- this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
1465
-
1466
- return dst;
1467
- }
1468
-
1469
- // _VM_JUMP_("labelName") — emits JUMP with a label operand.
1470
- // Used by bytecode transforms (e.g. CFF) via Template to express jumps
1471
- // to labels that exist in the parent compiler's bytecode stream.
1472
- if (
1473
- node.type === "CallExpression" &&
1474
- node.callee.type === "Identifier" &&
1475
- node.callee.name === "_VM_JUMP_"
1476
- ) {
1477
- const label = (node.arguments[0] as t.StringLiteral).value;
1478
- bc.push([this.OP.JUMP!, { type: "label", label }]);
1479
- // Return a dummy register — caller (ExpressionStatement) discards it.
1480
- return ctx.allocReg();
1481
- }
1482
-
1483
- switch ((node as any).type) {
1484
- case "NumericLiteral":
1485
- case "StringLiteral":
1486
- case "BooleanLiteral": {
1487
- const dst = ctx.allocReg();
1488
- this.emit(
1489
- bc,
1490
- [this.OP.LOAD_CONST, dst, b.constantOperand((node as any).value)],
1491
- node,
1492
- );
1493
- return dst;
1494
- }
1495
-
1496
- case "NullLiteral": {
1497
- const dst = ctx.allocReg();
1498
- this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(null)], node);
1499
- return dst;
1500
- }
1501
-
1502
- case "Identifier": {
1503
- const res = this._resolve(
1504
- (node as t.Identifier).name,
1505
- this._currentCtx,
1506
- );
1507
- if (res.kind === "local") return res.reg; // register IS the local
1508
- if (res.kind === "upvalue") {
1509
- const dst = ctx.allocReg();
1510
- this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
1511
- return dst;
1512
- }
1513
- // global
1514
- const dst = ctx.allocReg();
1515
- this.emit(
1516
- bc,
1517
- [
1518
- this.OP.LOAD_GLOBAL,
1519
- dst,
1520
- b.constantOperand((node as t.Identifier).name),
1521
- ],
1522
- node,
1523
- );
1524
- return dst;
1525
- }
1526
-
1527
- case "ThisExpression": {
1528
- const dst = ctx.allocReg();
1529
- this.emit(bc, [this.OP.LOAD_THIS, dst], node);
1530
- return dst;
1531
- }
1532
-
1533
- case "NewExpression": {
1534
- const calleeReg = this._compileExpr(
1535
- (node as t.NewExpression).callee,
1536
- scope,
1537
- bc,
1538
- );
1539
- const argRegs = (node as t.NewExpression).arguments.map((a) =>
1540
- this._compileExpr(a as t.Expression, scope, bc),
1541
- );
1542
- const dst = ctx.allocReg();
1543
- this.emit(
1544
- bc,
1545
- [
1546
- this.OP.NEW,
1547
- dst,
1548
- calleeReg,
1549
- (node as t.NewExpression).arguments.length,
1550
- ...argRegs,
1551
- ],
1552
- node,
1553
- );
1554
- return dst;
1555
- }
1556
-
1557
- case "SequenceExpression": {
1558
- const exprs = (node as t.SequenceExpression).expressions;
1559
- for (let i = 0; i < exprs.length - 1; i++) {
1560
- this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
1561
- }
1562
- return this._compileExpr(exprs[exprs.length - 1], scope, bc);
1563
- }
1564
-
1565
- case "ConditionalExpression": {
1566
- const n = node as t.ConditionalExpression;
1567
- const elseLabel = this._makeLabel("ternary_else");
1568
- const endLabel = this._makeLabel("ternary_end");
1569
-
1570
- const testReg = this._compileExpr(n.test, scope, bc);
1571
- this.emit(
1572
- bc,
1573
- [this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: elseLabel }],
1574
- node,
1575
- );
1576
-
1577
- // reg_result is a stable virtual register both branches write into.
1578
- const reg_result = ctx.allocReg();
1579
-
1580
- // Consequent branch.
1581
- const consReg = this._compileExpr(n.consequent, scope, bc);
1582
- if (consReg !== reg_result)
1583
- this.emit(bc, [this.OP.MOVE, reg_result, consReg], node);
1584
- this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
1585
-
1586
- // Alternate branch — each allocReg() gets a unique virtual ID so no
1587
- // slot collision is possible; no need to "re-occupy" reg_result.
1588
- this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
1589
- const altReg = this._compileExpr(n.alternate, scope, bc);
1590
- if (altReg !== reg_result)
1591
- this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
1592
-
1593
- this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1594
- return reg_result;
1595
- }
1596
-
1597
- case "LogicalExpression": {
1598
- const n = node as t.LogicalExpression;
1599
- const endLabel = this._makeLabel("logical_end");
1600
- const isOr = n.operator === "||";
1601
- if (!isOr && n.operator !== "&&")
1602
- throw new Error(`Unsupported logical operator: ${n.operator}`);
1603
-
1604
- const lhsReg = this._compileExpr(n.left, scope, bc);
1605
- const reg_result = ctx.allocReg();
1606
- if (lhsReg !== reg_result)
1607
- this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
1608
-
1609
- // For ||: if truthy keep LHS, jump past RHS.
1610
- // For &&: if falsy keep LHS, jump past RHS.
1611
- this.emit(
1612
- bc,
1613
- [
1614
- isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE,
1615
- reg_result,
1616
- { type: "label", label: endLabel },
1617
- ],
1618
- node,
1619
- );
1620
-
1621
- // Compile RHS into reg_result.
1622
- const rhsReg = this._compileExpr(n.right, scope, bc);
1623
- if (rhsReg !== reg_result)
1624
- this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
1625
-
1626
- this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1627
- return reg_result;
1628
- }
1629
-
1630
- case "TemplateLiteral": {
1631
- const n = node as t.TemplateLiteral;
1632
- // Fold: quasi[0] + expr[0] + quasi[1] + ... + quasi[last]
1633
- let acc = ctx.allocReg();
1634
- this.emit(
1635
- bc,
1636
- [
1637
- this.OP.LOAD_CONST,
1638
- acc,
1639
- b.constantOperand(n.quasis[0].value.cooked ?? ""),
1640
- ],
1641
- node,
1642
- );
1643
- for (let i = 0; i < n.expressions.length; i++) {
1644
- const exprReg = this._compileExpr(
1645
- n.expressions[i] as t.Expression,
1646
- scope,
1647
- bc,
1648
- );
1649
- const t1 = ctx.allocReg();
1650
- this.emit(bc, [this.OP.ADD, t1, acc, exprReg], node);
1651
- acc = t1;
1652
- const quasiReg = ctx.allocReg();
1653
- this.emit(
1654
- bc,
1655
- [
1656
- this.OP.LOAD_CONST,
1657
- quasiReg,
1658
- b.constantOperand(n.quasis[i + 1].value.cooked ?? ""),
1659
- ],
1660
- node,
1661
- );
1662
- const t2 = ctx.allocReg();
1663
- this.emit(bc, [this.OP.ADD, t2, acc, quasiReg], node);
1664
- acc = t2;
1665
- }
1666
- return acc;
1667
- }
1668
-
1669
- case "BinaryExpression": {
1670
- const n = node as t.BinaryExpression;
1671
- const lhsReg = this._compileExpr(n.left as t.Expression, scope, bc);
1672
- const rhsReg = this._compileExpr(n.right as t.Expression, scope, bc);
1673
- const dst = ctx.allocReg();
1674
-
1675
- const op = (
1676
- {
1677
- "+": this.OP.ADD,
1678
- "-": this.OP.SUB,
1679
- "*": this.OP.MUL,
1680
- "/": this.OP.DIV,
1681
- "%": this.OP.MOD,
1682
- "&": this.OP.BAND,
1683
- "|": this.OP.BOR,
1684
- "^": this.OP.BXOR,
1685
- "<<": this.OP.SHL,
1686
- ">>": this.OP.SHR,
1687
- ">>>": this.OP.USHR,
1688
- "<": this.OP.LT,
1689
- ">": this.OP.GT,
1690
- "===": this.OP.EQ,
1691
- "==": this.OP.LOOSE_EQ,
1692
- "<=": this.OP.LTE,
1693
- ">=": this.OP.GTE,
1694
- "!==": this.OP.NEQ,
1695
- "!=": this.OP.LOOSE_NEQ,
1696
- in: this.OP.IN,
1697
- instanceof: this.OP.INSTANCEOF,
1698
- } as Record<string, number | undefined>
1699
- )[n.operator];
1700
-
1701
- if (op === undefined)
1702
- throw new Error(`Unsupported operator: ${n.operator}`);
1703
-
1704
- this.emit(bc, [op, dst, lhsReg, rhsReg], node);
1705
- return dst;
1706
- }
1707
-
1708
- case "UpdateExpression": {
1709
- const n = node as t.UpdateExpression;
1710
- const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
1711
-
1712
- // Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
1713
- const applyBump = (
1714
- curReg: b.RegisterOperand,
1715
- ): [b.RegisterOperand, b.RegisterOperand] => {
1716
- const postfixReg = n.prefix
1717
- ? curReg // prefix: postfix copy unused; caller returns newReg instead
1718
- : (() => {
1719
- const r = ctx.allocReg();
1720
- this.emit(bc, [this.OP.MOVE, r, curReg], node as t.Node);
1721
- return r;
1722
- })();
1723
- const oneReg = ctx.allocReg();
1724
- this.emit(
1725
- bc,
1726
- [this.OP.LOAD_CONST, oneReg, b.constantOperand(1)],
1727
- node as t.Node,
1728
- );
1729
- const newReg = ctx.allocReg();
1730
- this.emit(bc, [bumpOp, newReg, curReg, oneReg], node as t.Node);
1731
- return [postfixReg, newReg];
1732
- };
1733
-
1734
- if (n.argument.type === "MemberExpression") {
1735
- const mem = n.argument as t.MemberExpression;
1736
- const objReg = this._compileExpr(mem.object, scope, bc);
1737
- let keyReg: b.RegisterOperand;
1738
- if (mem.computed) {
1739
- keyReg = this._compileExpr(mem.property as t.Expression, scope, bc);
1740
- } else {
1741
- keyReg = ctx.allocReg();
1742
- this.emit(
1743
- bc,
1744
- [
1745
- this.OP.LOAD_CONST,
1746
- keyReg,
1747
- b.constantOperand((mem.property as t.Identifier).name),
1748
- ],
1749
- node as t.Node,
1750
- );
1751
- }
1752
- const curReg = ctx.allocReg();
1753
- this.emit(
1754
- bc,
1755
- [this.OP.GET_PROP, curReg, objReg, keyReg],
1756
- node as t.Node,
1757
- );
1758
- const [postfixReg, newReg] = applyBump(curReg);
1759
- this.emit(
1760
- bc,
1761
- [this.OP.SET_PROP, objReg, keyReg, newReg],
1762
- node as t.Node,
1763
- );
1764
- return n.prefix ? newReg : postfixReg;
1765
- }
1766
-
1767
- ok(
1768
- n.argument.type === "Identifier",
1769
- "UpdateExpression requires identifier or member expression",
1770
- );
1771
- const name = (n.argument as t.Identifier).name;
1772
- const res = this._resolve(name, this._currentCtx);
1773
-
1774
- let curReg: b.RegisterOperand;
1775
- if (res.kind === "local") {
1776
- curReg = res.reg;
1777
- } else if (res.kind === "upvalue") {
1778
- curReg = ctx.allocReg();
1779
- this.emit(
1780
- bc,
1781
- [this.OP.LOAD_UPVALUE, curReg, res.index],
1782
- node as t.Node,
1783
- );
1784
- } else {
1785
- curReg = ctx.allocReg();
1786
- this.emit(
1787
- bc,
1788
- [this.OP.LOAD_GLOBAL, curReg, b.constantOperand(name)],
1789
- node as t.Node,
1790
- );
1791
- }
1792
-
1793
- const [postfixReg, newReg] = applyBump(curReg);
1794
-
1795
- if (res.kind === "local") {
1796
- this.emit(bc, [this.OP.MOVE, res.reg, newReg], node as t.Node);
1797
- } else if (res.kind === "upvalue") {
1798
- this.emit(
1799
- bc,
1800
- [this.OP.STORE_UPVALUE, res.index, newReg],
1801
- node as t.Node,
1802
- );
1803
- } else {
1804
- this.emit(
1805
- bc,
1806
- [this.OP.STORE_GLOBAL, b.constantOperand(name), newReg],
1807
- node as t.Node,
1808
- );
1809
- }
1810
-
1811
- return n.prefix ? newReg : postfixReg;
1812
- }
1813
-
1814
- case "AssignmentExpression": {
1815
- const n = node as t.AssignmentExpression;
1816
- const compoundOp = (
1817
- {
1818
- "+=": this.OP.ADD,
1819
- "-=": this.OP.SUB,
1820
- "*=": this.OP.MUL,
1821
- "/=": this.OP.DIV,
1822
- "%=": this.OP.MOD,
1823
- "&=": this.OP.BAND,
1824
- "|=": this.OP.BOR,
1825
- "^=": this.OP.BXOR,
1826
- "<<=": this.OP.SHL,
1827
- ">>=": this.OP.SHR,
1828
- ">>>=": this.OP.USHR,
1829
- } as Record<string, number | undefined>
1830
- )[n.operator];
1831
- const isCompound = compoundOp !== undefined;
1832
-
1833
- if (n.operator !== "=" && !isCompound)
1834
- throw new Error(`Unsupported assignment operator: ${n.operator}`);
1835
-
1836
- // Member assignment: obj.x = val or arr[i] = val
1837
- if (n.left.type === "MemberExpression") {
1838
- const objReg = this._compileExpr(n.left.object, scope, bc);
1839
-
1840
- let keyReg: b.RegisterOperand;
1841
- if (n.left.computed) {
1842
- keyReg = this._compileExpr(
1843
- n.left.property as t.Expression,
1844
- scope,
1845
- bc,
1846
- );
1847
- } else {
1848
- keyReg = ctx.allocReg();
1849
- this.emit(
1850
- bc,
1851
- [
1852
- this.OP.LOAD_CONST,
1853
- keyReg,
1854
- b.constantOperand((n.left.property as t.Identifier).name),
1855
- ],
1856
- node,
1857
- );
1858
- }
1859
-
1860
- let valReg: b.RegisterOperand;
1861
- if (isCompound) {
1862
- const curReg = ctx.allocReg();
1863
- this.emit(bc, [this.OP.GET_PROP, curReg, objReg, keyReg], node);
1864
- const rhsReg = this._compileExpr(n.right, scope, bc);
1865
- valReg = ctx.allocReg();
1866
- this.emit(bc, [compoundOp!, valReg, curReg, rhsReg], node);
1867
- } else {
1868
- valReg = this._compileExpr(n.right, scope, bc);
1869
- }
1870
-
1871
- this.emit(bc, [this.OP.SET_PROP, objReg, keyReg, valReg], node);
1872
- return valReg;
1873
- }
1874
-
1875
- // Plain identifier assignment.
1876
- const res = this._resolve(
1877
- (n.left as t.Identifier).name,
1878
- this._currentCtx,
1879
- );
1880
-
1881
- let rhsReg: b.RegisterOperand;
1882
- if (isCompound) {
1883
- // Load current value of the variable.
1884
- let curReg: b.RegisterOperand;
1885
- if (res.kind === "local") {
1886
- curReg = res.reg;
1887
- } else if (res.kind === "upvalue") {
1888
- curReg = ctx.allocReg();
1889
- this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
1890
- } else {
1891
- curReg = ctx.allocReg();
1892
- this.emit(
1893
- bc,
1894
- [
1895
- this.OP.LOAD_GLOBAL,
1896
- curReg,
1897
- b.constantOperand((n.left as t.Identifier).name),
1898
- ],
1899
- node,
1900
- );
1901
- }
1902
- const rhs2 = this._compileExpr(n.right, scope, bc);
1903
- rhsReg = ctx.allocReg();
1904
- this.emit(bc, [compoundOp!, rhsReg, curReg, rhs2], node);
1905
- } else {
1906
- rhsReg = this._compileExpr(n.right, scope, bc);
1907
- }
1908
-
1909
- // Store result and return it.
1910
- if (res.kind === "local") {
1911
- if (rhsReg !== res.reg)
1912
- this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
1913
- return res.reg;
1914
- } else if (res.kind === "upvalue") {
1915
- this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
1916
- return rhsReg;
1917
- } else {
1918
- const nameIdx = b.constantOperand((n.left as t.Identifier).name);
1919
- this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx, rhsReg], node);
1920
- return rhsReg;
1921
- }
1922
- }
1923
-
1924
- case "CallExpression": {
1925
- const n = node as t.CallExpression;
1926
-
1927
- if (n.callee.type === "MemberExpression") {
1928
- // Method call: receiver.method(args)
1929
- const receiverReg = this._compileExpr(n.callee.object, scope, bc);
1930
-
1931
- let methodKeyReg: b.RegisterOperand;
1932
- if (n.callee.computed) {
1933
- methodKeyReg = this._compileExpr(
1934
- n.callee.property as t.Expression,
1935
- scope,
1936
- bc,
1937
- );
1938
- } else {
1939
- methodKeyReg = ctx.allocReg();
1940
- this.emit(
1941
- bc,
1942
- [
1943
- this.OP.LOAD_CONST,
1944
- methodKeyReg,
1945
- b.constantOperand((n.callee.property as t.Identifier).name),
1946
- ],
1947
- node,
1948
- );
1949
- }
1950
-
1951
- const calleeReg = ctx.allocReg();
1952
- this.emit(
1953
- bc,
1954
- [this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg],
1955
- node,
1956
- );
1957
-
1958
- const argRegs = n.arguments.map((a) =>
1959
- this._compileExpr(a as t.Expression, scope, bc),
1960
- );
1961
- const dst = ctx.allocReg();
1962
- this.emit(
1963
- bc,
1964
- [
1965
- this.OP.CALL_METHOD,
1966
- dst,
1967
- receiverReg,
1968
- calleeReg,
1969
- n.arguments.length,
1970
- ...argRegs,
1971
- ],
1972
- node,
1973
- );
1974
- return dst;
1975
- } else {
1976
- // Plain call: fn(args)
1977
- const calleeReg = this._compileExpr(
1978
- n.callee as t.Expression,
1979
- scope,
1980
- bc,
1981
- );
1982
- const argRegs = n.arguments.map((a) =>
1983
- this._compileExpr(a as t.Expression, scope, bc),
1984
- );
1985
- const dst = ctx.allocReg();
1986
- this.emit(
1987
- bc,
1988
- [this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs],
1989
- node,
1990
- );
1991
- return dst;
1992
- }
1993
- }
1994
-
1995
- case "UnaryExpression": {
1996
- const n = node as t.UnaryExpression;
1997
-
1998
- // typeof on a potentially-undeclared global -- safe guard.
1999
- if (n.operator === "typeof" && n.argument.type === "Identifier") {
2000
- const res = this._resolve(n.argument.name, this._currentCtx);
2001
- if (res.kind === "global") {
2002
- const dst = ctx.allocReg();
2003
- this.emit(
2004
- bc,
2005
- [this.OP.TYPEOF_SAFE, dst, b.constantOperand(n.argument.name)],
2006
- node,
2007
- );
2008
- return dst;
2009
- }
2010
- }
2011
-
2012
- // delete expression.
2013
- if (n.operator === "delete") {
2014
- const arg = n.argument;
2015
- if (arg.type === "MemberExpression") {
2016
- const objReg = this._compileExpr(arg.object, scope, bc);
2017
- let keyReg: b.RegisterOperand;
2018
- if (arg.computed) {
2019
- keyReg = this._compileExpr(
2020
- arg.property as t.Expression,
2021
- scope,
2022
- bc,
2023
- );
2024
- } else {
2025
- keyReg = ctx.allocReg();
2026
- this.emit(
2027
- bc,
2028
- [
2029
- this.OP.LOAD_CONST,
2030
- keyReg,
2031
- b.constantOperand((arg.property as t.Identifier).name),
2032
- ],
2033
- node,
2034
- );
2035
- }
2036
- const dst = ctx.allocReg();
2037
- this.emit(bc, [this.OP.DELETE_PROP, dst, objReg, keyReg], node);
2038
- return dst;
2039
- } else {
2040
- // delete x or delete 0 -- always true in sloppy mode.
2041
- const dst = ctx.allocReg();
2042
- this.emit(
2043
- bc,
2044
- [this.OP.LOAD_CONST, dst, b.constantOperand(true)],
2045
- node,
2046
- );
2047
- return dst;
2048
- }
2049
- }
2050
-
2051
- // All other unary operators.
2052
- const srcReg = this._compileExpr(n.argument, scope, bc);
2053
- const dst = ctx.allocReg();
2054
- const unaryOp = (
2055
- {
2056
- "-": this.OP.UNARY_NEG,
2057
- "+": this.OP.UNARY_POS,
2058
- "!": this.OP.UNARY_NOT,
2059
- "~": this.OP.UNARY_BITNOT,
2060
- typeof: this.OP.TYPEOF,
2061
- void: this.OP.VOID,
2062
- } as Record<string, number | undefined>
2063
- )[n.operator];
2064
-
2065
- if (unaryOp === undefined)
2066
- throw new Error(`Unsupported unary operator: ${n.operator}`);
2067
-
2068
- this.emit(bc, [unaryOp, dst, srcReg], node);
2069
- return dst;
2070
- }
2071
-
2072
- case "RegExpLiteral": {
2073
- const n = node as t.RegExpLiteral;
2074
- // new RegExp(pattern, flags)
2075
- const regExpReg = ctx.allocReg();
2076
- this.emit(
2077
- bc,
2078
- [this.OP.LOAD_GLOBAL, regExpReg, b.constantOperand("RegExp")],
2079
- node,
2080
- );
2081
- const patternReg = ctx.allocReg();
2082
- this.emit(
2083
- bc,
2084
- [this.OP.LOAD_CONST, patternReg, b.constantOperand(n.pattern)],
2085
- node,
2086
- );
2087
- const flagsReg = ctx.allocReg();
2088
- this.emit(
2089
- bc,
2090
- [this.OP.LOAD_CONST, flagsReg, b.constantOperand(n.flags)],
2091
- node,
2092
- );
2093
- const dst = ctx.allocReg();
2094
- this.emit(
2095
- bc,
2096
- [this.OP.NEW, dst, regExpReg, 2, patternReg, flagsReg],
2097
- node,
2098
- );
2099
- return dst;
2100
- }
2101
-
2102
- case "FunctionExpression": {
2103
- const desc = this._compileFunctionDecl(node as t.FunctionExpression);
2104
- return this._emitMakeClosure(desc, node, bc);
2105
- }
2106
-
2107
- case "MemberExpression": {
2108
- const n = node as t.MemberExpression;
2109
- const objReg = this._compileExpr(n.object, scope, bc);
2110
- let keyReg: b.RegisterOperand;
2111
- if (n.computed) {
2112
- keyReg = this._compileExpr(n.property as t.Expression, scope, bc);
2113
- } else {
2114
- keyReg = ctx.allocReg();
2115
- this.emit(
2116
- bc,
2117
- [
2118
- this.OP.LOAD_CONST,
2119
- keyReg,
2120
- b.constantOperand((n.property as t.Identifier).name),
2121
- ],
2122
- node,
2123
- );
2124
- }
2125
- const dst = ctx.allocReg();
2126
- this.emit(bc, [this.OP.GET_PROP, dst, objReg, keyReg], node);
2127
- return dst;
2128
- }
2129
-
2130
- case "ArrayExpression": {
2131
- const n = node as t.ArrayExpression;
2132
- const elemRegs = n.elements.map((el) => {
2133
- if (el === null) {
2134
- const r = ctx.allocReg();
2135
- this.emit(
2136
- bc,
2137
- [this.OP.LOAD_CONST, r, b.constantOperand(undefined)],
2138
- node,
2139
- );
2140
- return r;
2141
- }
2142
- return this._compileExpr(el as t.Expression, scope, bc);
2143
- });
2144
- const dst = ctx.allocReg();
2145
- this.emit(
2146
- bc,
2147
- [this.OP.BUILD_ARRAY, dst, n.elements.length, ...elemRegs],
2148
- node,
2149
- );
2150
- return dst;
2151
- }
2152
-
2153
- case "ObjectExpression": {
2154
- const n = node as t.ObjectExpression;
2155
- const regularProps: t.ObjectProperty[] = [];
2156
- const accessorProps: t.ObjectMethod[] = [];
2157
-
2158
- for (const prop of n.properties) {
2159
- if (prop.type === "SpreadElement")
2160
- throw new Error("Object spread not supported");
2161
- if (prop.type === "ObjectMethod") {
2162
- if (prop.kind === "get" || prop.kind === "set") {
2163
- if (prop.computed)
2164
- throw new Error(
2165
- "Computed getter/setter keys are not supported",
2166
- );
2167
- accessorProps.push(prop);
2168
- } else {
2169
- throw new Error("Shorthand method syntax is not supported");
2170
- }
2171
- } else {
2172
- regularProps.push(prop as t.ObjectProperty);
2173
- }
2174
- }
2175
-
2176
- // Build flat [key, val, key, val, …] register list.
2177
- const pairRegs: b.RegisterOperand[] = [];
2178
- for (const prop of regularProps) {
2179
- let keyStr: string;
2180
- const key = prop.key;
2181
- if (key.type === "Identifier") keyStr = key.name;
2182
- else if (
2183
- key.type === "StringLiteral" ||
2184
- key.type === "NumericLiteral"
2185
- )
2186
- keyStr = String(key.value);
2187
- else throw new Error(`Unsupported object key type: ${key.type}`);
2188
-
2189
- const keyReg = ctx.allocReg();
2190
- this.emit(
2191
- bc,
2192
- [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)],
2193
- node,
2194
- );
2195
- const valReg = this._compileExpr(
2196
- prop.value as t.Expression,
2197
- scope,
2198
- bc,
2199
- );
2200
- pairRegs.push(keyReg, valReg);
2201
- }
2202
-
2203
- const dst = ctx.allocReg();
2204
- this.emit(
2205
- bc,
2206
- [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs],
2207
- node,
2208
- );
2209
-
2210
- // Define accessors on the object now sitting in `dst`.
2211
- for (const prop of accessorProps) {
2212
- const key = prop.key;
2213
- let keyStr: string;
2214
- if (key.type === "Identifier") keyStr = key.name;
2215
- else if (
2216
- key.type === "StringLiteral" ||
2217
- key.type === "NumericLiteral"
2218
- )
2219
- keyStr = String(key.value);
2220
- else throw new Error(`Unsupported object key type: ${key.type}`);
2221
-
2222
- const keyReg = ctx.allocReg();
2223
- this.emit(
2224
- bc,
2225
- [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)],
2226
- node,
2227
- );
2228
- const fnReg = this._emitMakeClosure(
2229
- this._compileFunctionDecl(prop as any),
2230
- prop as any,
2231
- bc,
2232
- );
2233
- this.emit(
2234
- bc,
2235
- [
2236
- prop.kind === "get"
2237
- ? this.OP.DEFINE_GETTER
2238
- : this.OP.DEFINE_SETTER,
2239
- dst,
2240
- keyReg,
2241
- fnReg,
2242
- ],
2243
- node,
2244
- );
2245
- }
2246
-
2247
- return dst;
2248
- }
2249
-
2250
- default: {
2251
- throw new Error(`Unsupported expression: ${(node as any).type}`);
2252
- }
2253
- }
2254
- }
2255
- }
2256
-
2257
- // ── Serializer ────────────────────────────────────────────────────────────────
2258
- class Serializer {
2259
- compiler: Compiler;
2260
-
2261
- constructor(compiler: Compiler) {
2262
- this.compiler = compiler;
2263
- }
2264
-
2265
- get options() {
2266
- return this.compiler.options;
2267
- }
2268
- get OP() {
2269
- return this.compiler.OP;
2270
- }
2271
- get OP_NAME() {
2272
- return this.compiler.OP_NAME;
2273
- }
2274
- get JUMP_OPS() {
2275
- return this.compiler.JUMP_OPS;
2276
- }
2277
-
2278
- _serializeConst(val: any) {
2279
- if (val === null) return "null";
2280
- if (val === undefined) return "undefined";
2281
- return JSON.stringify(val);
2282
- }
2283
-
2284
- // Reverse the concealment applied by resolveConstants so disassembly comments
2285
- // always show the plaintext value regardless of the concealConstants option.
2286
- _decryptConst(constants: any[], idx: number, key: number): any {
2287
- const v = constants[idx];
2288
- if (!key) return v;
2289
- if (typeof v === "number") return v ^ key;
2290
- if (typeof v !== "string") return v;
2291
- // String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
2292
- const bytes = Buffer.from(v as string, "base64");
2293
- let out = "";
2294
- for (let i = 0; i < bytes.length / 2; i++) {
2295
- const code = bytes[i * 2] | (bytes[i * 2 + 1] << 8);
2296
- out += String.fromCharCode(code ^ ((key + i) & 0xffff));
2297
- }
2298
- return out;
2299
- }
2300
-
2301
- _generateComment(instr: b.Instruction) {
2302
- const op = instr[0] as number;
2303
- const operands = instr.slice(1) as number[];
2304
-
2305
- if (op === null && (operands[0] as any)?.type === "defineLabel") {
2306
- const label = (operands[0] as any).label;
2307
- return `${label}:`;
2308
- }
2309
-
2310
- const constants = this.compiler.constants;
2311
-
2312
- const emittedOperands = operands.filter(
2313
- (operand) => (operand as any)?.placeholder !== true,
2314
- );
2315
-
2316
- const resolvedOperands = emittedOperands.map(
2317
- (o) => (o as any)?.resolvedValue ?? o,
2318
- );
2319
-
2320
- const displayOperands = operands.map((o, i) => {
2321
- const resolvedValue = resolvedOperands[i];
2322
- const label = (o as any)?.label;
2323
-
2324
- let displayOperand = resolvedValue;
2325
- if (label) {
2326
- return label;
2327
- }
2328
-
2329
- return displayOperand;
2330
- });
2331
-
2332
- let name = this.OP_NAME[op];
2333
- if (!name || name.includes("{")) {
2334
- name = `OP_${op}`;
2335
- }
2336
-
2337
- let comment = name;
2338
-
2339
- function formatLoc(loc: t.Node["loc"]["start"]) {
2340
- return loc ? `${loc.line}:${loc.column}` : "";
2341
- }
2342
-
2343
- const sourceNode = instr[SOURCE_NODE_SYM];
2344
- const sourceLocation = sourceNode?.loc
2345
- ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)]
2346
- .filter(Boolean)
2347
- .join("-")
2348
- : "";
2349
-
2350
- if (displayOperands.length > 0) {
2351
- // Operand[0] is always `dst` for instruction types that produce a value.
2352
- const dst = displayOperands[0];
2353
-
2354
- switch (op) {
2355
- case this.OP.LOAD_CONST: {
2356
- // resolvedOperands: [dst, constIdx, concealKey]
2357
- const val = this._decryptConst(
2358
- constants,
2359
- displayOperands[1],
2360
- displayOperands[2],
2361
- );
2362
- comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
2363
- break;
2364
- }
2365
-
2366
- case this.OP.LOAD_INT: {
2367
- // resolvedOperands: [dst, intValue]
2368
- comment += ` reg[${dst}] = ${displayOperands[1]}`;
2369
- break;
2370
- }
2371
-
2372
- case this.OP.LOAD_GLOBAL:
2373
- // resolvedOperands: [dst, constIdx, concealKey]
2374
- comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
2375
- break;
2376
- case this.OP.STORE_GLOBAL:
2377
- // resolvedOperands: [constIdx, concealKey, srcReg]
2378
- comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
2379
- break;
2380
- case this.OP.LOAD_UPVALUE:
2381
- comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
2382
- break;
2383
- case this.OP.STORE_UPVALUE:
2384
- comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
2385
- break;
2386
- case this.OP.MOVE:
2387
- comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
2388
- break;
2389
- case this.OP.MAKE_CLOSURE:
2390
- comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
2391
- break;
2392
- case this.OP.CALL:
2393
- comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands
2394
- .slice(3)
2395
- .map((v) => `reg[${v}]`)
2396
- .join(", ")})`;
2397
- break;
2398
- case this.OP.CALL_METHOD:
2399
- comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
2400
- break;
2401
- case this.OP.NEW:
2402
- comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
2403
- break;
2404
- case this.OP.RETURN:
2405
- comment += ` reg[${displayOperands[0]}]`;
2406
- break;
2407
- case this.OP.BUILD_ARRAY:
2408
- comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
2409
- break;
2410
- case this.OP.BUILD_OBJECT:
2411
- comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
2412
- break;
2413
- case this.OP.GET_PROP:
2414
- comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
2415
- break;
2416
- case this.OP.SET_PROP:
2417
- comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
2418
- break;
2419
-
2420
- case this.OP.JUMP_REG:
2421
- comment += ` PC = reg[${displayOperands[0]}]`;
2422
- break;
2423
-
2424
- default:
2425
- comment +=
2426
- displayOperands.length === 1
2427
- ? ` ${displayOperands[0]}`
2428
- : ` [${displayOperands.join(", ")}]`;
2429
- }
2430
- }
2431
-
2432
- comment = comment.padEnd(50) + sourceLocation;
2433
-
2434
- const values = [op, ...resolvedOperands];
2435
- const instrText = `[${values.join(", ")}]`;
2436
- const text = `${(instrText + ",").padEnd(20)} ${comment}`;
2437
-
2438
- return text;
2439
- }
2440
-
2441
- _serializeConstants(constants: any[]) {
2442
- const lines = ["var CONSTANTS = ["];
2443
- constants.forEach((val, idx) => {
2444
- lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
2445
- });
2446
- lines.push("];");
2447
- return lines.join("\n");
2448
- }
2449
-
2450
- _serializeBytecode(
2451
- bytecode: b.Bytecode,
2452
- compiler: Compiler,
2453
- ): { bytecode: b.Bytecode } {
2454
- const serialized = [];
2455
- for (const instr of bytecode) {
2456
- const op = instr[0];
2457
- const operands = instr.slice(1);
2458
-
2459
- if (instr[0] === null) continue; // null opcodes are not emitted
2460
-
2461
- const resolvedValues = operands.map(
2462
- (o) => (o as any)?.resolvedValue ?? o,
2463
- );
2464
-
2465
- const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
2466
- if (specializedOpInfo) {
2467
- const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
2468
- compiler.OP_NAME[instr[0]] =
2469
- `${originalName}_${resolvedValues.join("_")}`;
2470
- }
2471
-
2472
- // Validate no opcode or operand exceeds u16 limit
2473
- for (const o of resolvedValues) {
2474
- ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
2475
-
2476
- ok(
2477
- o >= 0 && o <= 0xffffffff,
2478
- `Operand overflow (max 0xFFFFFFFF u32): ${o}`,
2479
- );
2480
- }
2481
- ok(
2482
- op >= 0 && op <= 0xffffffff,
2483
- `Opcode overflow (max 0xFFFFFFFF u32): ${op}`,
2484
- );
2485
-
2486
- serialized.push(instr);
2487
- }
2488
- return { bytecode: serialized };
2489
- }
2490
-
2491
- _encodeBytecode(flat: number[]) {
2492
- const buf = new Uint8Array(flat.length * 4);
2493
- flat.forEach((w, i) => {
2494
- buf[i * 4] = w & 0xff;
2495
- buf[i * 4 + 1] = (w >>> 8) & 0xff;
2496
- buf[i * 4 + 2] = (w >>> 16) & 0xff;
2497
- buf[i * 4 + 3] = (w >>> 24) & 0xff;
2498
- });
2499
- return Buffer.from(buf).toString("base64");
2500
- }
2501
-
2502
- serialize(bytecode: b.Bytecode, constants: any[], compiler: Compiler) {
2503
- const mainStartPc = compiler.mainStartPc;
2504
- const mainRegCount = compiler.mainRegCount;
2505
- let sections = [];
2506
-
2507
- var initBody = [];
2508
- var bytecodeResult = this._serializeBytecode(bytecode, compiler);
2509
-
2510
- const flat = bytecodeResult.bytecode.flatMap((instr) => {
2511
- let filtered = instr.filter((x) => (x as any)?.placeholder !== true);
2512
- let resolved = filtered.map((x) => (x as any)?.resolvedValue ?? x);
2513
- return resolved as number[];
2514
- });
2515
-
2516
- if (this.options.encodeBytecode) {
2517
- sections.push(`var BYTECODE = "${this._encodeBytecode(flat)}";`);
2518
- } else {
2519
- sections.push(`var BYTECODE = [${flat.join(",")}]`);
2520
- }
2521
-
2522
- sections.push(`var MAIN_START_PC = ${mainStartPc};`);
2523
- sections.push(`var MAIN_REG_COUNT = ${mainRegCount};`);
2524
- sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
2525
- sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
2526
-
2527
- const object = t.objectExpression(
2528
- Object.entries(this.OP).map(([name, value]) =>
2529
- t.objectProperty(t.identifier(name), t.numericLiteral(value)),
2530
- ),
2531
- );
2532
- sections.push(`var OP = ${generate(object).code};`);
2533
-
2534
- initBody.push(this._serializeConstants(constants));
2535
-
2536
- sections = [...initBody, ...sections];
2537
- sections.push(VM_RUNTIME);
2538
-
2539
- return sections.join("\n\n");
2540
- }
2541
- }
2542
-
2543
- export async function compileAndSerialize(
2544
- sourceCode: string,
2545
- options: Options,
2546
- ): Promise<b.ObfuscationResult> {
2547
- let obfuscationStartedAt = now();
2548
-
2549
- const compiler = new Compiler(options);
2550
- let bytecode = compiler.compile(sourceCode);
2551
-
2552
- const passes: {
2553
- pass: (
2554
- bytecode: b.Bytecode,
2555
- compiler: Compiler,
2556
- ) => { bytecode: b.Bytecode; constants?: any[] };
2557
- name: string;
2558
- }[] = [];
2559
-
2560
- if (options.stringConcealing) {
2561
- passes.push({
2562
- pass: stringConcealing,
2563
- name: "stringConcealing",
2564
- });
2565
- }
2566
-
2567
- // CFF and Dispatcher both run before resolveRegisters and resolveLabels
2568
- if (options.controlFlowFlattening) {
2569
- passes.push({
2570
- pass: controlFlowFlattening,
2571
- name: "controlFlowFlattening",
2572
- });
2573
- }
2574
-
2575
- if (options.dispatcher) {
2576
- passes.push({
2577
- pass: dispatcher,
2578
- name: "dispatcher",
2579
- });
2580
- }
2581
-
2582
- passes.push({
2583
- pass: concealConstants,
2584
- name: "concealConstants",
2585
- });
2586
-
2587
- if (options.specializedOpcodes) {
2588
- passes.push({
2589
- pass: specializedOpcodes,
2590
- name: "specializedOpcodes",
2591
- });
2592
- }
2593
-
2594
- if (options.macroOpcodes) {
2595
- passes.push({
2596
- pass: macroOpcodes,
2597
- name: "macroOpcodes",
2598
- });
2599
- }
2600
-
2601
- if (options.aliasedOpcodes) {
2602
- passes.push({
2603
- pass: aliasedOpcodes,
2604
- name: "aliasedOpcodes",
2605
- });
2606
- }
2607
-
2608
- const timings = {};
2609
-
2610
- function runAndTime(pass, name) {
2611
- const startedAt = now();
2612
-
2613
- compiler.log(`Running bytecode pass ${name}...`);
2614
-
2615
- const passResult = pass(bytecode, compiler);
2616
- bytecode = passResult.bytecode;
2617
-
2618
- const endedAt = now();
2619
- const elapsedMs = endedAt - startedAt;
2620
- timings[name] = elapsedMs;
2621
-
2622
- compiler.profileData.transforms[name] = {
2623
- transformTime: elapsedMs,
2624
- bytecodeSize: bytecode.length,
2625
- };
2626
-
2627
- compiler.log(
2628
- `Bytecode pass ${name} completed in ${Math.floor(elapsedMs)}ms`,
2629
- );
2630
-
2631
- return passResult;
2632
- }
2633
-
2634
- for (const pass of passes) {
2635
- runAndTime(pass.pass, pass.name);
2636
- }
2637
-
2638
- // Resolve virtual registers to concrete slot indices and set regCount per fn.
2639
- // Must run BEFORE selfModifying: that pass moves body instructions to the end
2640
- // of the bytecode while leaving RETURN in place, splitting a function's code
2641
- // into two non-contiguous regions. Linear-scan liveness then sees incorrect
2642
- // firstUse/lastUse for registers that span the gap, causing slot collisions.
2643
- const regsResult = runAndTime(resolveRegisters, "resolveRegisters");
2644
- bytecode = regsResult.bytecode;
2645
-
2646
- // selfModifying runs after register resolution so concrete slot indices are
2647
- // already in place; only label operands remain unresolved at this stage.
2648
- if (options.selfModifying) {
2649
- const smResult = runAndTime(selfModifying, "selfModifying");
2650
- bytecode = smResult.bytecode;
2651
- }
2652
-
2653
- // Resolve label references to flat bytecode indices.
2654
- const labelsResult = runAndTime(resolveLabels, "resolveLabels");
2655
- bytecode = labelsResult.bytecode;
2656
-
2657
- // Set mainStartPc from the first function descriptor (or 0 for top-level start).
2658
- compiler.mainStartPc = compiler.mainFn.startPc;
2659
-
2660
- // Resolve constant references to pool indices (+ conceal key operand).
2661
- const constResult = runAndTime(resolveConstants, "resolveConstants");
2662
- bytecode = constResult.bytecode;
2663
- compiler.constants = constResult.constants;
2664
-
2665
- // Build and obfuscate the runtime.
2666
- const runtimeSource = compiler.serializer.serialize(
2667
- bytecode,
2668
- constResult.constants,
2669
- compiler,
2670
- );
2671
-
2672
- // for (const key of Object.keys(timings)) {
2673
- // console.log(` ${key}: ${timings[key]}ms`);
2674
- // }
2675
-
2676
- // This part was purposefully pulled out Serializer as OP_NAME's get resolved during buildRuntime
2677
- // So for the most useful comments, it's ran absolutely last
2678
- // Tests also rely on correct comments so it's required
2679
- const generateBytecodeComment = () => {
2680
- var lines: string[] = [];
2681
- for (const instr of bytecode) {
2682
- const comment = compiler.serializer._generateComment(instr);
2683
- lines.push("// " + comment);
2684
- }
2685
-
2686
- return lines.join("\n");
2687
- };
2688
-
2689
- const code = await buildRuntime(
2690
- runtimeSource,
2691
- bytecode,
2692
- options,
2693
- compiler,
2694
- generateBytecodeComment,
2695
- );
2696
-
2697
- compiler.profileData.obfuscationTime = now() - obfuscationStartedAt;
2698
-
2699
- return {
2700
- code,
2701
- profileData: compiler.profileData as b.ObfuscationResult["profileData"],
2702
- };
2703
- }