js-confuser-vm 0.1.0 → 0.1.2

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