js-confuser-vm 0.0.5 → 0.0.7

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