js-confuser-vm 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/compiler.ts CHANGED
@@ -1,4 +1,4 @@
1
- import parser from "@babel/parser";
1
+ import { parse } from "@babel/parser";
2
2
  import traverseImport from "@babel/traverse";
3
3
  import { generate } from "@babel/generator";
4
4
 
@@ -6,23 +6,31 @@ import { readFileSync } from "fs";
6
6
  import { join } from "path";
7
7
  import { stripTypeScriptTypes } from "module";
8
8
  import JSON5 from "json5";
9
- import { choice, getRandomInt } from "./random.ts";
10
9
  import * as t from "@babel/types";
11
10
  import { ok } from "assert";
12
11
  import { obfuscateRuntime } from "./runtimeObf.ts";
13
- import type { Options } from "./options.ts";
12
+ import { DEFAULT_OPTIONS, type Options } from "./options.ts";
13
+ import { resolveLabels } from "./transforms/resolveLabels.ts";
14
+ import { resolveConstants } from "./transforms/resolveContants.ts";
15
+ import { selfModifying } from "./transforms/selfModifying.ts";
16
+ import * as b from "./types.ts";
14
17
 
15
- const traverse = traverseImport.default;
18
+ const traverse = (traverseImport.default ||
19
+ traverseImport) as typeof traverseImport.default;
16
20
 
17
21
  const readVMRuntimeFile = () => {
22
+ let code;
18
23
  try {
19
- return readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
24
+ code = readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
20
25
  } catch (e) {
21
- return readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
26
+ code = readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
22
27
  }
28
+
29
+ return stripTypeScriptTypes?.(code) || code;
23
30
  };
24
31
 
25
- const VM_RUNTIME = stripTypeScriptTypes(readVMRuntimeFile().split("@START")[1]);
32
+ const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
33
+ export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE"); // Attach source node location to pseudo bytecode instructions
26
34
 
27
35
  // Opcodes
28
36
  export const OP_ORIGINAL = {
@@ -94,36 +102,26 @@ export const OP_ORIGINAL = {
94
102
 
95
103
  // Self-modifying bytecode
96
104
  PATCH: 56, // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
97
- };
98
105
 
99
- // Constant Pool
100
- // Primitives (string/number/bool) are interned (deduped).
101
- // Object entries (fn descriptors) are always appended - no dedup.
102
- class ConstantPool {
103
- items: any[];
104
- _index: Map<string, number>;
106
+ // Try-Catch
107
+ TRY_SETUP: 57, // operand = catch_pc; push exception handler onto frame._handlerStack
108
+ TRY_END: 58, // pop exception handler (normal exit from try body)
105
109
 
106
- constructor() {
107
- this.items = []; // ordered pool entries
108
- this._index = new Map(); // primitive dedup map
109
- }
110
+ // Getter / Setter (ES5 object literal accessor syntax)
111
+ DEFINE_GETTER: 59, // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {get: fn})
112
+ DEFINE_SETTER: 60, // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
110
113
 
111
- intern(val) {
112
- // Only intern primitives -- objects must use addObject()
113
- const key = `${typeof val}:${val}`;
114
- if (this._index.has(key)) return this._index.get(key);
115
- const idx = this.items.length;
116
- this.items.push(val);
117
- this._index.set(key, idx);
118
- return idx;
119
- }
114
+ DEBUGGER: 61, // for dev/testing -- emits a "debugger" statement with a comment of the original source location
120
115
 
121
- addObject(obj) {
122
- const idx = this.items.length;
123
- this.items.push(obj);
124
- return idx;
125
- }
126
- }
116
+ // Push the raw integer operand directly onto the stack (no constant pool lookup).
117
+ // Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
118
+ // raw PC number that becomes the operand, which is pushed as-is at runtime.
119
+ LOAD_INT: 62,
120
+
121
+ // Reserved / unused opcode slot (formerly the inline DATA header word).
122
+ // Kept to avoid renumbering; should never appear in compiled output.
123
+ DATA: 63,
124
+ };
127
125
 
128
126
  // Scope
129
127
  // Each function call gets its own Scope. Locals are resolved to
@@ -168,7 +166,7 @@ class FnContext {
168
166
  parentCtx: FnContext | null;
169
167
  scope: Scope;
170
168
  compiler: Compiler;
171
- bc: any[];
169
+ bc: b.Instruction[];
172
170
 
173
171
  constructor(compiler, parentCtx = null) {
174
172
  this.compiler = compiler;
@@ -192,20 +190,22 @@ class FnContext {
192
190
  }
193
191
 
194
192
  // Compiler
195
- class Compiler {
196
- constants: ConstantPool;
193
+ export class Compiler {
197
194
  fnDescriptors: any[];
198
- bytecode: any[];
195
+ bytecode: b.Bytecode;
199
196
  mainStartPc: number;
200
197
 
201
198
  _currentCtx: FnContext | null;
202
199
  _pendingLabel: string | null;
203
200
  _forInCount: number;
201
+ _labelCount: number;
204
202
  _loopStack: {
205
203
  type: "loop" | "switch" | "block";
206
204
  label: string | null;
207
- breakJumps: number[];
208
- continueJumps: number[];
205
+ // Label that break statements targeting this entry should jump to.
206
+ breakLabel: string;
207
+ // Label that continue statements targeting this entry should jump to.
208
+ continueLabel: string;
209
209
  }[];
210
210
 
211
211
  options: Options;
@@ -215,16 +215,26 @@ class Compiler {
215
215
  OP_NAME: Record<number, string>;
216
216
  JUMP_OPS: Set<number>;
217
217
 
218
- constructor(options: Options) {
218
+ emit(bc: b.Bytecode, instr: b.Instruction, node: t.Node) {
219
+ bc.push(instr);
220
+
221
+ instr[SOURCE_NODE_SYM] = node;
222
+ }
223
+
224
+ // DO NOT USE THIS KEY UNLESS YOU ARE "RESOLVE CONSTANTS"
225
+ // CONSTANTS DURING COMPILATION MUST BE USED BY REFERENCE WITH b.constantOperand("myConstantHere")
226
+ constants: any[];
227
+
228
+ constructor(options: Options = DEFAULT_OPTIONS) {
219
229
  this.options = options;
220
- this.constants = new ConstantPool();
221
230
  this.fnDescriptors = []; // populated in pass 1
222
231
  this.bytecode = [];
223
232
  this.mainStartPc = 0;
224
233
  this._currentCtx = null; // FnContext of the function being compiled, null at top-level
225
- this._loopStack = []; // { breakJumps: number[], continueJumps: number[] } per active loop
234
+ this._loopStack = []; // per active loop/switch/block/try
226
235
  this._pendingLabel = null;
227
236
  this._forInCount = 0; // counter for synthetic for-in iterator global names
237
+ this._labelCount = 0; // monotonically increasing counter for unique label names
228
238
 
229
239
  this.serializer = new Serializer(this);
230
240
 
@@ -255,9 +265,16 @@ class Compiler {
255
265
  this.OP.JUMP_IF_TRUE_OR_POP,
256
266
  this.OP.JUMP_IF_FALSE_OR_POP,
257
267
  this.OP.FOR_IN_NEXT,
268
+ this.OP.TRY_SETUP, // catch_pc operand needs offset adjustment like jump targets
258
269
  ]);
259
270
  }
260
271
 
272
+ // Generate a globally unique label string with an optional hint for readability.
273
+ _makeLabel(hint = ""): string {
274
+ var id = this._labelCount++;
275
+ return `${hint || "L"}_${id}`;
276
+ }
277
+
261
278
  // Variable resolution
262
279
  // Walks up the FnContext chain. Crossing a context boundary means
263
280
  // we're capturing from an outer function - register an upvalue.
@@ -287,7 +304,7 @@ class Compiler {
287
304
 
288
305
  // Entry point
289
306
  compile(source: string) {
290
- const ast = parser.parse(source, { sourceType: "script" });
307
+ const ast = parse(source, { sourceType: "script" });
291
308
 
292
309
  return this.compileAST(ast);
293
310
  }
@@ -308,21 +325,32 @@ class Compiler {
308
325
  // Pass 2 -- compile top-level statements into BYTECODE.
309
326
  this._compileMain(ast.program.body);
310
327
 
311
- return {
312
- bytecode: this.bytecode,
313
- mainStartPc: this.mainStartPc,
314
- };
328
+ return this.bytecode;
315
329
  }
316
330
 
317
331
  // Function Declaration
318
332
 
319
333
  _compileFunctionDecl(node: t.FunctionDeclaration | t.FunctionExpression) {
334
+ // Reserve a slot in fnDescriptors NOW, before compiling the body, so that
335
+ // any nested _compileFunctionDecl calls see the correct .length and get a
336
+ // distinct _fnIdx. The placeholder object is mutated in-place below once
337
+ // the body and header are ready.
338
+ var fnIdx = this.fnDescriptors.length;
339
+ const entryLabel = this._makeLabel(`fn_${fnIdx}`);
340
+ var desc: any = {}; // placeholder — filled in after compilation
341
+ this.fnDescriptors.push(desc);
342
+
320
343
  // Create a context whose parent is whatever we're currently compiling.
321
344
  // This is what lets _resolve cross function boundaries correctly.
322
345
  const ctx = new FnContext(this, this._currentCtx);
323
346
  const savedCtx = this._currentCtx;
324
347
  this._currentCtx = ctx;
325
348
 
349
+ // Isolate the loop stack so that try/loop entries from the outer scope
350
+ // don't cause spurious TRY_END / extra jumps inside this function body.
351
+ const savedLoopStack = this._loopStack;
352
+ this._loopStack = [];
353
+
326
354
  // Params occupy the first N local slots (args are copied in on CALL)
327
355
  for (const param of node.params) {
328
356
  let identifier = param.type === "AssignmentPattern" ? param.left : param;
@@ -341,24 +369,34 @@ class Compiler {
341
369
  // Pass 2: emit default-value guards at top of fn body
342
370
  // Mirrors what JS engines do: if the caller passed undefined (or
343
371
  // nothing), evaluate the default expression and overwrite the slot.
344
- // Default expressions are full expressions, so f(x = a + b) and
345
- // f(x = foo()) both work correctly.
346
372
  for (const param of node.params) {
347
373
  if (param.type !== "AssignmentPattern") continue;
348
374
 
349
375
  const slot = ctx.scope._locals.get((param.left as t.Identifier).name);
376
+ const skipLabel = this._makeLabel("param_skip");
350
377
 
351
378
  // if (param === undefined) param = <default expr>
352
- ctx.bc.push([this.OP.LOAD_LOCAL, slot]);
353
- ctx.bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
354
- ctx.bc.push([this.OP.EQ]);
355
- ctx.bc.push([this.OP.JUMP_IF_FALSE, 0]);
356
- const skipIdx = ctx.bc.length - 1;
379
+ this.emit(ctx.bc, [this.OP.LOAD_LOCAL, slot], param);
380
+ this.emit(
381
+ ctx.bc,
382
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
383
+ param,
384
+ );
385
+ this.emit(ctx.bc, [this.OP.EQ], param);
386
+ this.emit(
387
+ ctx.bc,
388
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: skipLabel }],
389
+ param,
390
+ );
357
391
 
358
392
  this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
359
- ctx.bc.push([this.OP.STORE_LOCAL, slot]);
393
+ this.emit(ctx.bc, [this.OP.STORE_LOCAL, slot], param);
360
394
 
361
- ctx.bc[skipIdx][1] = ctx.bc.length; // patch skip jump
395
+ this.emit(
396
+ ctx.bc,
397
+ [null, { type: "defineLabel", label: skipLabel }],
398
+ param,
399
+ );
362
400
  }
363
401
 
364
402
  for (const stmt of node.body.body) {
@@ -366,36 +404,48 @@ class Compiler {
366
404
  }
367
405
 
368
406
  // If we fall off the end of the function, implicitly return undefined.
369
- ctx.bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
370
- ctx.bc.push([this.OP.RETURN]);
407
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
408
+ this.emit(ctx.bc, [this.OP.RETURN], node);
371
409
 
372
410
  this._currentCtx = savedCtx; // restore before touching fnDescriptors
411
+ this._loopStack = savedLoopStack;
412
+
413
+ (node as any)._fnIdx = fnIdx;
414
+
415
+ // Fill the placeholder that was reserved at the top of this function.
416
+ // Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
417
+ // as LOAD_INT instructions onto the value stack at each MAKE_CLOSURE call
418
+ // site — the runtime reads them from the stack, not from DATA words.
419
+ desc.name = node.id?.name || "<anonymous>";
420
+ desc.entryLabel = entryLabel;
421
+ desc.bytecode = ctx.bc as b.Bytecode;
422
+ desc._fnIdx = fnIdx;
423
+ desc.paramCount = node.params.length;
424
+ desc.localCount = ctx.scope.localCount;
425
+ desc.upvalues = ctx.upvalues.slice();
373
426
 
374
- var fnIdx = this.fnDescriptors.length;
375
- (node as any)._fnIdx = fnIdx; // for error messages
376
-
377
- const desc = {
378
- name: node.id?.name || "<anonymous>",
379
- paramCount: node.params.length,
380
- localCount: ctx.scope.localCount,
381
- upvalueDescriptors: ctx.upvalues.map((u) => ({
382
- isLocal: u.isLocal,
383
- _index: u.index,
384
- })),
385
- bytecode: ctx.bc,
386
- // Indices assigned after pushing into the pool
387
- _fnIdx: this.fnDescriptors.length,
388
- _constIdx: null,
389
- };
390
-
391
- this.fnDescriptors.push(desc);
392
- desc._constIdx = this.constants.addObject(desc); // object entry, no dedup
393
427
  return desc;
394
428
  }
395
429
 
430
+ // Emit LOAD_INT instructions that push closure metadata onto the value stack
431
+ // immediately before a MAKE_CLOSURE instruction. The runtime pops these
432
+ // values in MAKE_CLOSURE instead of reading DATA words from bytecode.
433
+ //
434
+ // Stack layout when MAKE_CLOSURE executes (top is rightmost):
435
+ // [isLocal_0, idx_0, ..., isLocal_N-1, idx_N-1, uvCount, localCount, paramCount]
436
+ _emitClosureMetadata(desc: any, node: t.Node, bc: b.Bytecode) {
437
+ // Push each upvalue descriptor in order; runtime pops them in reverse.
438
+ for (const uv of desc.upvalues) {
439
+ this.emit(bc, [this.OP.LOAD_INT, uv.isLocal ? 1 : 0], node);
440
+ this.emit(bc, [this.OP.LOAD_INT, uv.index], node);
441
+ }
442
+ this.emit(bc, [this.OP.LOAD_INT, desc.upvalues.length], node);
443
+ this.emit(bc, [this.OP.LOAD_INT, desc.localCount], node);
444
+ this.emit(bc, [this.OP.LOAD_INT, desc.paramCount], node);
445
+ }
446
+
396
447
  // Main (top-level)
397
448
  _compileMain(body: t.Statement[]) {
398
- this.mainStartPc = 0; // ← record main's entry point
399
449
  const bc = this.bytecode;
400
450
 
401
451
  // Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
@@ -405,9 +455,14 @@ class Compiler {
405
455
  const desc = this.fnDescriptors.find(
406
456
  (d) => d._fnIdx === (node as any)._fnIdx,
407
457
  );
408
- const nameIdx = this.constants.intern(node.id.name);
409
- bc.push([this.OP.MAKE_CLOSURE, desc._constIdx]);
410
- bc.push([this.OP.STORE_GLOBAL, nameIdx]);
458
+ const nameRef = b.constantOperand(node.id.name);
459
+ this._emitClosureMetadata(desc, node, bc);
460
+ this.emit(
461
+ bc,
462
+ [this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
463
+ node,
464
+ );
465
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameRef], node);
411
466
  }
412
467
 
413
468
  // Compile everything else in order
@@ -416,46 +471,18 @@ class Compiler {
416
471
  this._compileStatement(node, null, bc); // null scope -> global context
417
472
  }
418
473
 
419
- bc.push([this.OP.RETURN]); // end program
474
+ this.emit(bc, [this.OP.RETURN], null); // end program
420
475
 
421
- // Now that main is compiled, we can append all the function bodies at the end of the bytecode.
476
+ // Append all function bodies. Each function's entryLabel (already generated
477
+ // in _compileFunctionDecl) points directly to the first body instruction;
478
+ // metadata is pushed onto the stack at each call site, not stored inline.
422
479
  for (const descriptor of this.fnDescriptors) {
423
- descriptor.startPc = this.bytecode.length;
424
-
425
- descriptor.bytecode.push([this.OP.RETURN]); // ensure every function ends with RETURN
426
-
427
- if (this.options.selfModifying) {
428
- // Preamble is 2 instructions: LOAD_CONST(destPc) + PATCH(bodyConst)
429
- // Real body starts immediately after the preamble.
430
- const bodyPc = descriptor.startPc + 2;
431
-
432
- // Build real body with jump targets resolved from bodyPc as the base.
433
- const realBodyInstrs = descriptor.bytecode.map((instr) =>
434
- this._offsetJump(instr, bodyPc),
435
- );
436
-
437
- // Pack each instruction into a 32-bit word and store as a constant.
438
- // The PATCH handler will write these words directly into this.bytecode.
439
- const realBodyWords =
440
- this.serializer._serializeBytecode(realBodyInstrs);
441
- const bodyConstIdx = this.constants.addObject(realBodyWords);
442
-
443
- // Emit preamble: push destination PC, then PATCH.
444
- const destPcConstIdx = this.constants.intern(bodyPc);
445
- this.bytecode.push([this.OP.LOAD_CONST, destPcConstIdx]);
446
- this.bytecode.push([this.OP.PATCH, bodyConstIdx]);
447
-
448
- // Garbage fill -- same length as real body, never executed (PATCH fires first).
449
- for (let i = 0; i < realBodyInstrs.length; i++) {
450
- this.bytecode.push([
451
- choice(Object.values(this.OP)),
452
- getRandomInt(0, 255),
453
- ]);
454
- }
455
- } else {
456
- for (const instr of descriptor.bytecode) {
457
- this.bytecode.push(this._offsetJump(instr, descriptor.startPc));
458
- }
480
+ this.bytecode.push([
481
+ null,
482
+ { type: "defineLabel", label: descriptor.entryLabel },
483
+ ]);
484
+ for (const instr of descriptor.bytecode) {
485
+ this.bytecode.push(instr);
459
486
  }
460
487
  }
461
488
 
@@ -464,27 +491,24 @@ class Compiler {
464
491
  `Program too large: ${this.bytecode.length} instructions, max 16,777,215`,
465
492
  );
466
493
 
467
- if (this.constants.items.length > 0xffffff)
468
- throw new Error(
469
- `Constant pool too large: ${this.constants.items.length} entries, max 16,777,215`,
470
- );
471
- }
472
-
473
- _offsetJump(instr, offset) {
474
- if (this.JUMP_OPS.has(instr[0]) && instr[1] !== undefined) {
475
- return [instr[0], instr[1] + offset];
476
- }
477
- return instr;
494
+ // if (this.constants.items.length > 0xffffff)
495
+ // throw new Error(
496
+ // `Constant pool too large: ${this.constants.items.length} entries, max 16,777,215`,
497
+ // );
478
498
  }
479
499
 
480
500
  // Statements
481
- _compileStatement(node: t.Statement, scope, bc) {
501
+ _compileStatement(node: t.Statement, scope: Scope, bc: b.Bytecode) {
482
502
  switch (node.type) {
483
503
  case "EmptyStatement": {
484
504
  // nothing to emit -- bare semicolon is a no-op
485
505
  break;
486
506
  }
487
507
 
508
+ case "DebuggerStatement":
509
+ this.emit(bc, [this.OP.DEBUGGER], node);
510
+ break;
511
+
488
512
  case "BlockStatement": {
489
513
  for (const stmt of node.body) {
490
514
  this._compileStatement(stmt, scope, bc);
@@ -497,19 +521,28 @@ class Compiler {
497
521
  // MAKE_CLOSURE so it's captured as a live closure at runtime.
498
522
  // (_compileFunctionDecl pushes/pops _currentCtx internally)
499
523
  const desc = this._compileFunctionDecl(node);
500
- bc.push([this.OP.MAKE_CLOSURE, desc._constIdx]);
524
+ this._emitClosureMetadata(desc, node, bc);
525
+ this.emit(
526
+ bc,
527
+ [this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
528
+ node,
529
+ );
501
530
  if (scope) {
502
531
  const slot = scope.define(node.id.name);
503
- bc.push([this.OP.STORE_LOCAL, slot]);
532
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
504
533
  } else {
505
- bc.push([this.OP.STORE_GLOBAL, this.constants.intern(node.id.name)]);
534
+ this.emit(
535
+ bc,
536
+ [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)],
537
+ node,
538
+ );
506
539
  }
507
540
  break;
508
541
  }
509
542
 
510
543
  case "ThrowStatement": {
511
544
  this._compileExpr(node.argument, scope, bc);
512
- bc.push([this.OP.THROW]);
545
+ this.emit(bc, [this.OP.THROW], node);
513
546
  break;
514
547
  }
515
548
 
@@ -517,15 +550,27 @@ class Compiler {
517
550
  if (node.argument) {
518
551
  this._compileExpr(node.argument, scope, bc);
519
552
  } else {
520
- bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
553
+ this.emit(
554
+ bc,
555
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
556
+ node,
557
+ );
558
+ }
559
+ // Disarm any open try handlers before leaving the function.
560
+ // TRY_END only touches frame._handlerStack, not the value stack,
561
+ // so the return value sitting on top is safe.
562
+ for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
563
+ if ((this._loopStack[_ri].type as any) === "try") {
564
+ this.emit(bc, [this.OP.TRY_END], node);
565
+ }
521
566
  }
522
- bc.push([this.OP.RETURN]);
567
+ this.emit(bc, [this.OP.RETURN], node);
523
568
  break;
524
569
  }
525
570
 
526
571
  case "ExpressionStatement": {
527
572
  this._compileExpr(node.expression, scope, bc);
528
- bc.push([this.OP.POP]); // discard return value of statement-level expressions
573
+ this.emit(bc, [this.OP.POP], node); // discard return value of statement-level expressions
529
574
  break;
530
575
  }
531
576
 
@@ -535,7 +580,11 @@ class Compiler {
535
580
  if (decl.init) {
536
581
  this._compileExpr(decl.init, scope, bc);
537
582
  } else {
538
- bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
583
+ this.emit(
584
+ bc,
585
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
586
+ node,
587
+ );
539
588
  }
540
589
 
541
590
  ok(
@@ -546,25 +595,29 @@ class Compiler {
546
595
  // Store: local slot if inside a function, global name otherwise
547
596
  if (scope) {
548
597
  const slot = scope.define(decl.id.name);
549
- bc.push([this.OP.STORE_LOCAL, slot]);
598
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
550
599
  } else {
551
- bc.push([
552
- this.OP.STORE_GLOBAL,
553
- this.constants.intern(decl.id.name),
554
- ]);
600
+ this.emit(
601
+ bc,
602
+ [this.OP.STORE_GLOBAL, b.constantOperand(decl.id.name)],
603
+ node,
604
+ );
555
605
  }
556
606
  }
557
607
  break;
558
608
  }
559
609
 
560
610
  case "IfStatement": {
611
+ const elseOrEndLabel = this._makeLabel("if_else");
561
612
  // 1. Compile the test expression -> leaves a value on the stack
562
613
  this._compileExpr(node.test, scope, bc);
563
- // 2. Emit JUMP_IF_FALSE with placeholder target
564
- bc.push([this.OP.JUMP_IF_FALSE, 0]);
565
- const jumpIfFalseIdx = bc.length - 1;
614
+ // 2. Emit JUMP_IF_FALSE to the else branch (or end if no else)
615
+ this.emit(
616
+ bc,
617
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: elseOrEndLabel }],
618
+ node,
619
+ );
566
620
  // 3. Compile the consequent block (the "then" branch)
567
- // Consequent may be a BlockStatement or a bare statement (no braces)
568
621
  const consequentBody =
569
622
  node.consequent.type === "BlockStatement"
570
623
  ? node.consequent.body
@@ -574,10 +627,18 @@ class Compiler {
574
627
  }
575
628
  if (node.alternate) {
576
629
  // 4a. Consequent needs to jump OVER the else block when done
577
- bc.push([this.OP.JUMP, 0]);
578
- const jumpOverElseIdx = bc.length - 1;
579
- // Patch JUMP_IF_FALSE to land here (start of else)
580
- bc[jumpIfFalseIdx][1] = bc.length;
630
+ const endLabel = this._makeLabel("if_end");
631
+ this.emit(
632
+ bc,
633
+ [this.OP.JUMP, { type: "label", label: endLabel }],
634
+ node,
635
+ );
636
+ // Mark start of else
637
+ this.emit(
638
+ bc,
639
+ [null, { type: "defineLabel", label: elseOrEndLabel }],
640
+ node,
641
+ );
581
642
  // 5. Compile the alternate (else) block
582
643
  const altBody =
583
644
  node.alternate.type === "BlockStatement"
@@ -586,11 +647,15 @@ class Compiler {
586
647
  for (const stmt of altBody) {
587
648
  this._compileStatement(stmt, scope, bc);
588
649
  }
589
- // Patch the JUMP to land after the else block
590
- bc[jumpOverElseIdx][1] = bc.length;
650
+ // Mark end (consequent's jump lands here)
651
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
591
652
  } else {
592
- // 4b. No else -- patch JUMP_IF_FALSE to land right after the then block
593
- bc[jumpIfFalseIdx][1] = bc.length;
653
+ // 4b. No else -- label lands right after the then block
654
+ this.emit(
655
+ bc,
656
+ [null, { type: "defineLabel", label: elseOrEndLabel }],
657
+ node,
658
+ );
594
659
  }
595
660
  break;
596
661
  }
@@ -598,18 +663,28 @@ class Compiler {
598
663
  case "WhileStatement": {
599
664
  const _wLabel = this._pendingLabel;
600
665
  this._pendingLabel = null;
666
+
667
+ const loopTopLabel = this._makeLabel("while_top");
668
+ const exitLabel = this._makeLabel("while_exit");
669
+
601
670
  this._loopStack.push({
602
671
  type: "loop",
603
672
  label: _wLabel,
604
- breakJumps: [],
605
- continueJumps: [],
673
+ breakLabel: exitLabel,
674
+ continueLabel: loopTopLabel, // continue re-evaluates the test
606
675
  });
607
- const loopCtxW = this._loopStack[this._loopStack.length - 1];
608
676
 
609
- const loopTop = bc.length;
677
+ this.emit(
678
+ bc,
679
+ [null, { type: "defineLabel", label: loopTopLabel }],
680
+ node,
681
+ );
610
682
  this._compileExpr(node.test, scope, bc);
611
- bc.push([this.OP.JUMP_IF_FALSE, 0]);
612
- const exitJumpIdx = bc.length - 1;
683
+ this.emit(
684
+ bc,
685
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
686
+ node,
687
+ );
613
688
 
614
689
  const whileBody =
615
690
  node.body.type === "BlockStatement" ? node.body.body : [node.body];
@@ -617,13 +692,12 @@ class Compiler {
617
692
  this._compileStatement(stmt, scope, bc);
618
693
  }
619
694
 
620
- // continue -> re-evaluate the test
621
- for (const idx of loopCtxW.continueJumps) bc[idx][1] = loopTop;
622
- bc.push([this.OP.JUMP, loopTop]);
623
-
624
- const exitTargetW = bc.length;
625
- bc[exitJumpIdx][1] = exitTargetW;
626
- for (const idx of loopCtxW.breakJumps) bc[idx][1] = exitTargetW;
695
+ this.emit(
696
+ bc,
697
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
698
+ node,
699
+ );
700
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
627
701
 
628
702
  this._loopStack.pop();
629
703
  break;
@@ -632,15 +706,23 @@ class Compiler {
632
706
  case "DoWhileStatement": {
633
707
  const _dwLabel = this._pendingLabel;
634
708
  this._pendingLabel = null;
709
+
710
+ const loopTopLabel = this._makeLabel("dowhile_top");
711
+ const continueLabel = this._makeLabel("dowhile_cont");
712
+ const exitLabel = this._makeLabel("dowhile_exit");
713
+
635
714
  this._loopStack.push({
636
715
  type: "loop",
637
716
  label: _dwLabel,
638
- breakJumps: [],
639
- continueJumps: [],
717
+ breakLabel: exitLabel,
718
+ continueLabel: continueLabel, // continue falls to the test
640
719
  });
641
- const loopCtxDW = this._loopStack[this._loopStack.length - 1];
642
720
 
643
- const loopTopDW = bc.length;
721
+ this.emit(
722
+ bc,
723
+ [null, { type: "defineLabel", label: loopTopLabel }],
724
+ node,
725
+ );
644
726
 
645
727
  const doWhileBody =
646
728
  node.body.type === "BlockStatement" ? node.body.body : [node.body];
@@ -649,18 +731,24 @@ class Compiler {
649
731
  }
650
732
 
651
733
  // continue -> skip rest of body, fall through to test
652
- const continueTargetDW = bc.length;
653
- for (const idx of loopCtxDW.continueJumps)
654
- bc[idx][1] = continueTargetDW;
655
-
734
+ this.emit(
735
+ bc,
736
+ [null, { type: "defineLabel", label: continueLabel }],
737
+ node,
738
+ );
656
739
  this._compileExpr(node.test, scope, bc);
657
- bc.push([this.OP.JUMP_IF_FALSE, 0]);
658
- const exitJumpIdxDW = bc.length - 1;
659
- bc.push([this.OP.JUMP, loopTopDW]);
740
+ this.emit(
741
+ bc,
742
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
743
+ node,
744
+ );
745
+ this.emit(
746
+ bc,
747
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
748
+ node,
749
+ );
660
750
 
661
- const exitTargetDW = bc.length;
662
- bc[exitJumpIdxDW][1] = exitTargetDW;
663
- for (const idx of loopCtxDW.breakJumps) bc[idx][1] = exitTargetDW;
751
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
664
752
 
665
753
  this._loopStack.pop();
666
754
  break;
@@ -669,29 +757,43 @@ class Compiler {
669
757
  case "ForStatement": {
670
758
  const _fLabel = this._pendingLabel;
671
759
  this._pendingLabel = null;
760
+
761
+ const loopTopLabel = this._makeLabel("for_top");
762
+ const exitLabel = this._makeLabel("for_exit");
763
+ // continue jumps to the update clause if present, else straight to test
764
+ const updateLabel = node.update
765
+ ? this._makeLabel("for_update")
766
+ : loopTopLabel;
767
+
672
768
  this._loopStack.push({
673
769
  type: "loop",
674
770
  label: _fLabel,
675
- breakJumps: [],
676
- continueJumps: [],
771
+ breakLabel: exitLabel,
772
+ continueLabel: updateLabel,
677
773
  });
678
- const loopCtxF = this._loopStack[this._loopStack.length - 1];
679
774
 
680
775
  if (node.init) {
681
776
  if (node.init.type === "VariableDeclaration") {
682
777
  this._compileStatement(node.init, scope, bc);
683
778
  } else {
684
779
  this._compileExpr(node.init, scope, bc);
685
- bc.push([this.OP.POP]);
780
+ this.emit(bc, [this.OP.POP], node);
686
781
  }
687
782
  }
688
783
 
689
- const loopTopF = bc.length;
784
+ this.emit(
785
+ bc,
786
+ [null, { type: "defineLabel", label: loopTopLabel }],
787
+ node,
788
+ );
690
789
  if (node.test) {
691
790
  this._compileExpr(node.test, scope, bc);
692
- bc.push([this.OP.JUMP_IF_FALSE, 0]);
791
+ this.emit(
792
+ bc,
793
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
794
+ node,
795
+ );
693
796
  }
694
- const exitJumpIdxF = node.test ? bc.length - 1 : null;
695
797
 
696
798
  const forBody =
697
799
  node.body.type === "BlockStatement" ? node.body.body : [node.body];
@@ -701,96 +803,126 @@ class Compiler {
701
803
 
702
804
  // continue -> run update (if any) then back to test
703
805
  if (node.update) {
704
- const continueTargetF = bc.length;
705
- for (const idx of loopCtxF.continueJumps)
706
- bc[idx][1] = continueTargetF;
806
+ this.emit(
807
+ bc,
808
+ [null, { type: "defineLabel", label: updateLabel }],
809
+ node,
810
+ );
707
811
  this._compileExpr(node.update, scope, bc);
708
- bc.push([this.OP.POP]);
709
- } else {
710
- // No update -- continue goes straight to the test
711
- for (const idx of loopCtxF.continueJumps) bc[idx][1] = loopTopF;
812
+ this.emit(bc, [this.OP.POP], node);
712
813
  }
713
814
 
714
- bc.push([this.OP.JUMP, loopTopF]);
715
-
716
- const exitTargetF = bc.length;
717
- if (exitJumpIdxF !== null) bc[exitJumpIdxF][1] = exitTargetF;
718
- for (const idx of loopCtxF.breakJumps) bc[idx][1] = exitTargetF;
815
+ this.emit(
816
+ bc,
817
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
818
+ node,
819
+ );
820
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
719
821
 
720
822
  this._loopStack.pop();
721
823
  break;
722
824
  }
723
825
 
724
826
  case "BreakStatement": {
725
- bc.push([this.OP.JUMP, 0]);
726
- const _bJumpIdx = bc.length - 1;
827
+ // Find the jump target in the loop stack.
828
+ let _bTargetIdx = -1;
727
829
  if (node.label) {
728
830
  const _bLabelName = node.label.name;
729
- let _bFound = -1;
730
831
  for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
731
832
  if (this._loopStack[_bi].label === _bLabelName) {
732
- _bFound = _bi;
833
+ _bTargetIdx = _bi;
733
834
  break;
734
835
  }
735
836
  }
736
- if (_bFound === -1)
737
- throw new Error(`Label '${_bLabelName}' not found`);
738
- this._loopStack[_bFound].breakJumps.push(_bJumpIdx);
837
+ if (_bTargetIdx === -1)
838
+ throw new Error(`Label '${node.label.name}' not found`);
739
839
  } else {
740
- if (this._loopStack.length === 0)
741
- throw new Error("break outside loop");
742
- this._loopStack[this._loopStack.length - 1].breakJumps.push(
743
- _bJumpIdx,
744
- );
840
+ // Find innermost loop/switch/block (skip "try" entries)
841
+ for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
842
+ if ((this._loopStack[_bi].type as any) !== "try") {
843
+ _bTargetIdx = _bi;
844
+ break;
845
+ }
846
+ }
847
+ if (_bTargetIdx === -1) throw new Error("break outside loop");
848
+ }
849
+ // Emit TRY_END for every open try block between here and the target.
850
+ for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
851
+ if ((this._loopStack[_bi].type as any) === "try") {
852
+ this.emit(bc, [this.OP.TRY_END], node);
853
+ }
745
854
  }
855
+ this.emit(
856
+ bc,
857
+ [
858
+ this.OP.JUMP,
859
+ { type: "label", label: this._loopStack[_bTargetIdx].breakLabel },
860
+ ],
861
+ node,
862
+ );
746
863
  break;
747
864
  }
748
865
 
749
866
  case "ContinueStatement": {
750
- bc.push([this.OP.JUMP, 0]);
751
- const _cJumpIdx = bc.length - 1;
867
+ // Find the target loop in the loop stack.
868
+ let _cTargetIdx = -1;
752
869
  if (node.label) {
753
870
  const _cLabelName = node.label.name;
754
- let _cFound = -1;
755
871
  for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
756
872
  if (
757
873
  this._loopStack[_ci].label === _cLabelName &&
758
874
  this._loopStack[_ci].type === "loop"
759
875
  ) {
760
- _cFound = _ci;
876
+ _cTargetIdx = _ci;
761
877
  break;
762
878
  }
763
879
  }
764
- if (_cFound === -1)
765
- throw new Error(`Label '${_cLabelName}' not found for continue`);
766
- this._loopStack[_cFound].continueJumps.push(_cJumpIdx);
880
+ if (_cTargetIdx === -1)
881
+ throw new Error(
882
+ `Label '${node.label.name}' not found for continue`,
883
+ );
767
884
  } else {
768
- if (this._loopStack.length === 0)
769
- throw new Error("continue outside loop");
770
- // Find the innermost loop (skip switch and block contexts)
771
- let loopIdx = -1;
772
- for (let i = this._loopStack.length - 1; i >= 0; i--) {
773
- if (this._loopStack[i].type === "loop") {
774
- loopIdx = i;
885
+ // Find the innermost loop (skip switch, block, and try contexts)
886
+ for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
887
+ if (this._loopStack[_ci].type === "loop") {
888
+ _cTargetIdx = _ci;
775
889
  break;
776
890
  }
777
891
  }
778
- if (loopIdx === -1) throw new Error("continue outside loop");
779
- this._loopStack[loopIdx].continueJumps.push(_cJumpIdx);
892
+ if (_cTargetIdx === -1) throw new Error("continue outside loop");
780
893
  }
894
+ // Emit TRY_END for every open try block between here and the target loop.
895
+ for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
896
+ if ((this._loopStack[_ci].type as any) === "try") {
897
+ this.emit(bc, [this.OP.TRY_END], node);
898
+ }
899
+ }
900
+ this.emit(
901
+ bc,
902
+ [
903
+ this.OP.JUMP,
904
+ {
905
+ type: "label",
906
+ label: this._loopStack[_cTargetIdx].continueLabel,
907
+ },
908
+ ],
909
+ node,
910
+ );
781
911
  break;
782
912
  }
783
913
 
784
914
  case "SwitchStatement": {
785
915
  const _swLabel = this._pendingLabel;
786
916
  this._pendingLabel = null;
917
+
918
+ const switchBreakLabel = this._makeLabel("sw_break");
919
+
787
920
  this._loopStack.push({
788
921
  type: "switch",
789
922
  label: _swLabel,
790
- breakJumps: [],
791
- continueJumps: [],
923
+ breakLabel: switchBreakLabel,
924
+ continueLabel: switchBreakLabel, // not used for switch
792
925
  });
793
- const switchCtx = this._loopStack[this._loopStack.length - 1];
794
926
 
795
927
  // Compile the discriminant and leave it on the stack
796
928
  this._compileExpr(node.discriminant, scope, bc);
@@ -798,58 +930,70 @@ class Compiler {
798
930
  const cases = node.cases;
799
931
  const defaultIdx = cases.findIndex((c) => c.test === null);
800
932
 
801
- // Dispatch section: emit case checks
802
- const bodyJumps = []; // { cas, jumpIdx }
933
+ // Pre-allocate a label for each case body so dispatch can reference them
934
+ const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
803
935
 
804
- for (const cas of cases) {
805
- if (cas.test === null) continue; // Skip default in dispatch
936
+ // Dispatch section: for each non-default case, check and jump to its body
937
+ for (let i = 0; i < cases.length; i++) {
938
+ const cas = cases[i];
939
+ if (cas.test === null) continue; // skip default in dispatch
806
940
 
807
- // Check this case: DUP; LOAD_CONST; EQ; JUMP_IF_FALSE
808
- bc.push([this.OP.DUP]);
941
+ const nextCheckLabel = this._makeLabel("sw_next");
942
+ this.emit(bc, [this.OP.DUP], node);
809
943
  this._compileExpr(cas.test, scope, bc);
810
- bc.push([this.OP.EQ]);
811
- bc.push([this.OP.JUMP_IF_FALSE, 0]); // Jump to next check (patched later)
812
- const skipIdx = bc.length - 1;
813
-
814
- // If matched, jump to this case's body
815
- bc.push([this.OP.JUMP, 0]); // Jump to body (patched later)
816
- bodyJumps.push({ cas, jumpIdx: bc.length - 1 });
817
-
818
- // Patch the JUMP_IF_FALSE to the next check
819
- bc[skipIdx][1] = bc.length;
944
+ this.emit(bc, [this.OP.EQ], node);
945
+ // If not matched, fall through to the next check
946
+ this.emit(
947
+ bc,
948
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: nextCheckLabel }],
949
+ node,
950
+ );
951
+ // If matched, jump directly to this case's body
952
+ this.emit(
953
+ bc,
954
+ [this.OP.JUMP, { type: "label", label: caseLabels[i] }],
955
+ node,
956
+ );
957
+ this.emit(
958
+ bc,
959
+ [null, { type: "defineLabel", label: nextCheckLabel }],
960
+ node,
961
+ );
820
962
  }
821
963
 
822
- // No match found: jump to default (or exit if no default)
823
- bc.push([this.OP.JUMP, 0]);
824
- const noMatchJumpIdx = bc.length - 1;
964
+ // No case matched: jump to default body or exit (which pops discriminant)
965
+ this.emit(
966
+ bc,
967
+ [
968
+ this.OP.JUMP,
969
+ {
970
+ type: "label",
971
+ label:
972
+ defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel,
973
+ },
974
+ ],
975
+ node,
976
+ );
825
977
 
826
- // Body section: compile all case bodies in source order
827
- const bodyStart = new Map();
828
- for (const cas of cases) {
829
- bodyStart.set(cas, bc.length);
830
- for (const stmt of cas.consequent) {
978
+ // Body section: compile all case bodies in source order (fallthrough intact)
979
+ for (let i = 0; i < cases.length; i++) {
980
+ this.emit(
981
+ bc,
982
+ [null, { type: "defineLabel", label: caseLabels[i] }],
983
+ node,
984
+ );
985
+ for (const stmt of cases[i].consequent) {
831
986
  this._compileStatement(stmt, scope, bc);
832
987
  }
833
988
  }
834
989
 
835
- // Patch the no-match jump to default or exit
836
- const exitTarget = bc.length;
837
- if (defaultIdx !== -1) {
838
- bc[noMatchJumpIdx][1] = bodyStart.get(cases[defaultIdx]);
839
- } else {
840
- bc[noMatchJumpIdx][1] = exitTarget;
841
- }
842
-
843
- // Patch all body jumps
844
- for (const { cas, jumpIdx } of bodyJumps) {
845
- bc[jumpIdx][1] = bodyStart.get(cas);
846
- }
847
-
848
- // Exit: pop the discriminant and patch break jumps
849
- bc.push([this.OP.POP]);
850
- for (const idx of switchCtx.breakJumps) {
851
- bc[idx][1] = bc.length - 1; // Point to the POP instruction
852
- }
990
+ // break label lands here; pop the discriminant and continue after switch
991
+ this.emit(
992
+ bc,
993
+ [null, { type: "defineLabel", label: switchBreakLabel }],
994
+ node,
995
+ );
996
+ this.emit(bc, [this.OP.POP], node);
853
997
 
854
998
  this._loopStack.pop();
855
999
  break;
@@ -872,16 +1016,20 @@ class Compiler {
872
1016
  this._pendingLabel = null; // safety clear if handler didn't consume it
873
1017
  } else {
874
1018
  // Non-loop labeled statement (e.g. labeled block) -- only break is valid
1019
+ const blockBreakLabel = this._makeLabel("block_break");
875
1020
  this._loopStack.push({
876
1021
  type: "block",
877
1022
  label: _lName,
878
- breakJumps: [],
879
- continueJumps: [],
1023
+ breakLabel: blockBreakLabel,
1024
+ continueLabel: blockBreakLabel, // unused
880
1025
  });
881
1026
  this._compileStatement(_lBody, scope, bc);
882
- const _lEntry = this._loopStack.pop()!;
883
- const _lExit = bc.length;
884
- for (const _lIdx of _lEntry.breakJumps) bc[_lIdx][1] = _lExit;
1027
+ this._loopStack.pop();
1028
+ this.emit(
1029
+ bc,
1030
+ [null, { type: "defineLabel", label: blockBreakLabel }],
1031
+ node,
1032
+ );
885
1033
  }
886
1034
  break;
887
1035
  }
@@ -893,7 +1041,7 @@ class Compiler {
893
1041
  // Evaluate the object expression -> on stack
894
1042
  this._compileExpr(node.right, scope, bc);
895
1043
  // FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
896
- bc.push([this.OP.FOR_IN_SETUP]);
1044
+ this.emit(bc, [this.OP.FOR_IN_SETUP], node);
897
1045
 
898
1046
  // Store iterator in a hidden slot so break/continue need no cleanup
899
1047
  let emitLoadIter: () => void;
@@ -901,32 +1049,43 @@ class Compiler {
901
1049
  if (scope) {
902
1050
  // Reserve a hidden local slot (no name mapping needed)
903
1051
  const iterSlot = scope._next++;
904
- emitLoadIter = () => bc.push([this.OP.LOAD_LOCAL, iterSlot]);
905
- emitStoreIter = () => bc.push([this.OP.STORE_LOCAL, iterSlot]);
1052
+ emitLoadIter = () =>
1053
+ this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
1054
+ emitStoreIter = () =>
1055
+ this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
906
1056
  } else {
907
1057
  // Top level -- use a synthetic global that won't collide with user code
908
- const iterNameIdx = this.constants.intern(
909
- "__fi" + this._forInCount++,
910
- );
911
- emitLoadIter = () => bc.push([this.OP.LOAD_GLOBAL, iterNameIdx]);
912
- emitStoreIter = () => bc.push([this.OP.STORE_GLOBAL, iterNameIdx]);
1058
+ const iterNameIdx = b.constantOperand("__fi" + this._forInCount++);
1059
+ emitLoadIter = () =>
1060
+ this.emit(bc, [this.OP.LOAD_GLOBAL, iterNameIdx], node);
1061
+ emitStoreIter = () =>
1062
+ this.emit(bc, [this.OP.STORE_GLOBAL, iterNameIdx], node);
913
1063
  }
914
1064
  emitStoreIter();
915
1065
 
1066
+ const loopTopLabel = this._makeLabel("forin_top");
1067
+ const exitLabel = this._makeLabel("forin_exit");
1068
+
916
1069
  this._loopStack.push({
917
1070
  type: "loop",
918
1071
  label: _fiLabel,
919
- breakJumps: [],
920
- continueJumps: [],
1072
+ breakLabel: exitLabel,
1073
+ continueLabel: loopTopLabel, // continue re-checks the iterator
921
1074
  });
922
- const loopCtxFI = this._loopStack[this._loopStack.length - 1];
923
1075
 
924
- const loopTopFI = bc.length;
1076
+ this.emit(
1077
+ bc,
1078
+ [null, { type: "defineLabel", label: loopTopLabel }],
1079
+ node,
1080
+ );
925
1081
 
926
1082
  // Load iterator, attempt to get next key
927
1083
  emitLoadIter();
928
- bc.push([this.OP.FOR_IN_NEXT, 0]); // exit target patched below
929
- const forInNextPatch = bc.length - 1;
1084
+ this.emit(
1085
+ bc,
1086
+ [this.OP.FOR_IN_NEXT, { type: "label", label: exitLabel }],
1087
+ node,
1088
+ );
930
1089
 
931
1090
  // Assign the key (now on top of stack) to the loop variable
932
1091
  if (node.left.type === "VariableDeclaration") {
@@ -938,21 +1097,26 @@ class Compiler {
938
1097
  const name = identifier.name;
939
1098
  if (scope) {
940
1099
  const slot = scope.define(name);
941
- bc.push([this.OP.STORE_LOCAL, slot]);
1100
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
942
1101
  } else {
943
- bc.push([this.OP.STORE_GLOBAL, this.constants.intern(name)]);
1102
+ this.emit(
1103
+ bc,
1104
+ [this.OP.STORE_GLOBAL, b.constantOperand(name)],
1105
+ node,
1106
+ );
944
1107
  }
945
1108
  } else if (node.left.type === "Identifier") {
946
1109
  const res = this._resolve(node.left.name, this._currentCtx);
947
1110
  if (res.kind === "local") {
948
- bc.push([this.OP.STORE_LOCAL, res.slot]);
1111
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
949
1112
  } else if (res.kind === "upvalue") {
950
- bc.push([this.OP.STORE_UPVALUE, res.index]);
1113
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
951
1114
  } else {
952
- bc.push([
953
- this.OP.STORE_GLOBAL,
954
- this.constants.intern(node.left.name),
955
- ]);
1115
+ this.emit(
1116
+ bc,
1117
+ [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)],
1118
+ node,
1119
+ );
956
1120
  }
957
1121
  } else {
958
1122
  const src = generate(node.left).code;
@@ -968,15 +1132,101 @@ class Compiler {
968
1132
  this._compileStatement(stmt, scope, bc);
969
1133
  }
970
1134
 
971
- // continue -> re-load iterator and check next key
972
- for (const idx of loopCtxFI.continueJumps) bc[idx][1] = loopTopFI;
973
- bc.push([this.OP.JUMP, loopTopFI]);
1135
+ this.emit(
1136
+ bc,
1137
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
1138
+ node,
1139
+ );
1140
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
1141
+
1142
+ this._loopStack.pop();
1143
+ break;
1144
+ }
1145
+
1146
+ case "TryStatement": {
1147
+ if (node.finalizer) {
1148
+ throw new Error(
1149
+ "try..finally is not supported. Use a helper function instead",
1150
+ );
1151
+ }
1152
+ if (!node.handler) {
1153
+ // try without catch requires finally — not supported
1154
+ throw new Error(
1155
+ "try without catch is not supported (requires finally).",
1156
+ );
1157
+ }
974
1158
 
975
- const exitTargetFI = bc.length;
976
- bc[forInNextPatch][1] = exitTargetFI;
977
- for (const idx of loopCtxFI.breakJumps) bc[idx][1] = exitTargetFI;
1159
+ const catchLabel = this._makeLabel("catch");
1160
+ const afterCatchLabel = this._makeLabel("after_catch");
978
1161
 
1162
+ // Emit TRY_SETUP with the catch block's label as the handler PC.
1163
+ // At runtime: saves stack depth + frame stack depth, pushes handler.
1164
+ this.emit(
1165
+ bc,
1166
+ [this.OP.TRY_SETUP, { type: "label", label: catchLabel }],
1167
+ node,
1168
+ );
1169
+
1170
+ // Track the open try block so that break/continue/return inside the
1171
+ // try body can emit the matching TRY_END before their jump.
1172
+ this._loopStack.push({
1173
+ type: "try" as any,
1174
+ label: null,
1175
+ breakLabel: "", // unused
1176
+ continueLabel: "", // unused
1177
+ });
1178
+
1179
+ // Compile try body
1180
+ for (const stmt of node.block.body) {
1181
+ this._compileStatement(stmt, scope, bc);
1182
+ }
1183
+
1184
+ // Done compiling the try body — pop the tracking entry.
979
1185
  this._loopStack.pop();
1186
+
1187
+ // Normal exit: disarm the exception handler.
1188
+ this.emit(bc, [this.OP.TRY_END], node);
1189
+
1190
+ // Jump over the catch block on normal path.
1191
+ this.emit(
1192
+ bc,
1193
+ [this.OP.JUMP, { type: "label", label: afterCatchLabel }],
1194
+ node,
1195
+ );
1196
+
1197
+ // Catch block: exception is on top of the stack (pushed by the VM).
1198
+ this.emit(bc, [null, { type: "defineLabel", label: catchLabel }], node);
1199
+
1200
+ const handler = node.handler;
1201
+ if (handler.param) {
1202
+ // Bind the exception value to the catch variable.
1203
+ const name = (handler.param as t.Identifier).name;
1204
+ if (scope) {
1205
+ const slot = scope.define(name);
1206
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
1207
+ } else {
1208
+ this.emit(
1209
+ bc,
1210
+ [this.OP.STORE_GLOBAL, b.constantOperand(name)],
1211
+ node,
1212
+ );
1213
+ }
1214
+ } else {
1215
+ // Optional catch binding (catch without a variable — ES2019+)
1216
+ this.emit(bc, [this.OP.POP], node);
1217
+ }
1218
+
1219
+ // Compile catch body
1220
+ for (const stmt of handler.body.body) {
1221
+ this._compileStatement(stmt, scope, bc);
1222
+ }
1223
+
1224
+ // Normal-path jump lands here (after the catch block).
1225
+ this.emit(
1226
+ bc,
1227
+ [null, { type: "defineLabel", label: afterCatchLabel }],
1228
+ node,
1229
+ );
980
1230
  break;
981
1231
  }
982
1232
 
@@ -994,17 +1244,25 @@ class Compiler {
994
1244
  switch (node.type) {
995
1245
  case "NumericLiteral":
996
1246
  case "StringLiteral": {
997
- bc.push([this.OP.LOAD_CONST, this.constants.intern(node.value)]);
1247
+ this.emit(
1248
+ bc,
1249
+ [this.OP.LOAD_CONST, b.constantOperand(node.value)],
1250
+ node,
1251
+ );
998
1252
  break;
999
1253
  }
1000
1254
 
1001
1255
  case "BooleanLiteral": {
1002
- bc.push([this.OP.LOAD_CONST, this.constants.intern(node.value)]);
1256
+ this.emit(
1257
+ bc,
1258
+ [this.OP.LOAD_CONST, b.constantOperand(node.value)],
1259
+ node,
1260
+ );
1003
1261
  break;
1004
1262
  }
1005
1263
 
1006
1264
  case "NullLiteral": {
1007
- bc.push([this.OP.LOAD_CONST, this.constants.intern(null)]);
1265
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(null)], node);
1008
1266
  break;
1009
1267
  }
1010
1268
 
@@ -1012,17 +1270,21 @@ class Compiler {
1012
1270
  // scope=null means we're at the top-level -> always global
1013
1271
  const res = this._resolve(node.name, this._currentCtx);
1014
1272
  if (res.kind === "local") {
1015
- bc.push([this.OP.LOAD_LOCAL, res.slot]);
1273
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1016
1274
  } else if (res.kind === "upvalue") {
1017
- bc.push([this.OP.LOAD_UPVALUE, res.index]);
1275
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1018
1276
  } else {
1019
- bc.push([this.OP.LOAD_GLOBAL, this.constants.intern(node.name)]);
1277
+ this.emit(
1278
+ bc,
1279
+ [this.OP.LOAD_GLOBAL, b.constantOperand(node.name)],
1280
+ node,
1281
+ );
1020
1282
  }
1021
1283
  break;
1022
1284
  }
1023
1285
 
1024
1286
  case "ThisExpression": {
1025
- bc.push([this.OP.LOAD_THIS]);
1287
+ this.emit(bc, [this.OP.LOAD_THIS], node);
1026
1288
  break;
1027
1289
  }
1028
1290
 
@@ -1030,16 +1292,15 @@ class Compiler {
1030
1292
  // Push callee, then args -- identical layout to CALL but uses NEW opcode
1031
1293
  this._compileExpr(node.callee, scope, bc);
1032
1294
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1033
- bc.push([this.OP.NEW, node.arguments.length]);
1295
+ this.emit(bc, [this.OP.NEW, node.arguments.length], node);
1034
1296
  break;
1035
1297
  }
1036
1298
 
1037
1299
  case "SequenceExpression": {
1038
1300
  // (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
1039
- // Matches CPython's BINARY_OP / POP_TOP pattern for comma expressions.
1040
1301
  for (let i = 0; i < node.expressions.length - 1; i++) {
1041
1302
  this._compileExpr(node.expressions[i], scope, bc);
1042
- bc.push([this.OP.POP]); // discard intermediate result
1303
+ this.emit(bc, [this.OP.POP], node); // discard intermediate result
1043
1304
  }
1044
1305
  // Last expression -- its value is the result of the whole sequence
1045
1306
  this._compileExpr(
@@ -1052,21 +1313,23 @@ class Compiler {
1052
1313
 
1053
1314
  case "ConditionalExpression": {
1054
1315
  // test ? consequent : alternate
1055
- // Identical to IfStatement codegen, just lives in expression context.
1056
- this._compileExpr(node.test, scope, bc);
1316
+ const elseLabel = this._makeLabel("ternary_else");
1317
+ const endLabel = this._makeLabel("ternary_end");
1057
1318
 
1058
- bc.push([this.OP.JUMP_IF_FALSE, 0]);
1059
- const jumpToElse = bc.length - 1;
1319
+ this._compileExpr(node.test, scope, bc);
1320
+ this.emit(
1321
+ bc,
1322
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: elseLabel }],
1323
+ node,
1324
+ );
1060
1325
 
1061
1326
  this._compileExpr(node.consequent, scope, bc);
1327
+ this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
1062
1328
 
1063
- bc.push([this.OP.JUMP, 0]);
1064
- const jumpToEnd = bc.length - 1;
1065
-
1066
- bc[jumpToElse][1] = bc.length; // patch: false -> alternate
1329
+ this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
1067
1330
  this._compileExpr(node.alternate, scope, bc);
1068
1331
 
1069
- bc[jumpToEnd][1] = bc.length; // patch: after consequent -> end
1332
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1070
1333
  break;
1071
1334
  }
1072
1335
 
@@ -1081,16 +1344,24 @@ class Compiler {
1081
1344
 
1082
1345
  if (node.operator === "||") {
1083
1346
  // Short-circuit if LHS is TRUTHY -- keep it, skip RHS
1084
- bc.push([this.OP.JUMP_IF_TRUE_OR_POP, 0]);
1085
- const jumpIdx = bc.length - 1;
1347
+ const endLabel = this._makeLabel("or_end");
1348
+ this.emit(
1349
+ bc,
1350
+ [this.OP.JUMP_IF_TRUE_OR_POP, { type: "label", label: endLabel }],
1351
+ node,
1352
+ );
1086
1353
  this._compileExpr(node.right, scope, bc);
1087
- bc[jumpIdx][1] = bc.length; // patch target to after RHS
1354
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1088
1355
  } else if (node.operator === "&&") {
1089
1356
  // Short-circuit if LHS is FALSY -- keep it, skip RHS
1090
- bc.push([this.OP.JUMP_IF_FALSE_OR_POP, 0]);
1091
- const jumpIdx = bc.length - 1;
1357
+ const endLabel = this._makeLabel("and_end");
1358
+ this.emit(
1359
+ bc,
1360
+ [this.OP.JUMP_IF_FALSE_OR_POP, { type: "label", label: endLabel }],
1361
+ node,
1362
+ );
1092
1363
  this._compileExpr(node.right, scope, bc);
1093
- bc[jumpIdx][1] = bc.length; // patch target to after RHS
1364
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1094
1365
  } else {
1095
1366
  throw new Error(`Unsupported logical operator: ${node.operator}`);
1096
1367
  }
@@ -1129,7 +1400,7 @@ class Compiler {
1129
1400
  const resolvedOp = arithOp ?? cmpOp;
1130
1401
  if (resolvedOp === undefined)
1131
1402
  throw new Error(`Unsupported operator: ${node.operator}`);
1132
- bc.push([resolvedOp]);
1403
+ this.emit(bc, [resolvedOp], node);
1133
1404
 
1134
1405
  break;
1135
1406
  }
@@ -1137,34 +1408,38 @@ class Compiler {
1137
1408
  case "UpdateExpression": {
1138
1409
  const res = this._resolve(node.argument.name, this._currentCtx);
1139
1410
  const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
1140
- const one = this.constants.intern(1);
1411
+ const one = b.constantOperand(1);
1141
1412
 
1142
1413
  // Helper closures: emit load / store for whichever resolution kind we have
1143
1414
  const emitLoad = () => {
1144
- if (res.kind === "local") bc.push([this.OP.LOAD_LOCAL, res.slot]);
1415
+ if (res.kind === "local")
1416
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1145
1417
  else if (res.kind === "upvalue")
1146
- bc.push([this.OP.LOAD_UPVALUE, res.index]);
1418
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1147
1419
  else
1148
- bc.push([
1149
- this.OP.LOAD_GLOBAL,
1150
- this.constants.intern(node.argument.name),
1151
- ]);
1420
+ this.emit(
1421
+ bc,
1422
+ [this.OP.LOAD_GLOBAL, b.constantOperand(node.argument.name)],
1423
+ node,
1424
+ );
1152
1425
  };
1153
1426
  const emitStore = () => {
1154
- if (res.kind === "local") bc.push([this.OP.STORE_LOCAL, res.slot]);
1427
+ if (res.kind === "local")
1428
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
1155
1429
  else if (res.kind === "upvalue")
1156
- bc.push([this.OP.STORE_UPVALUE, res.index]);
1430
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
1157
1431
  else
1158
- bc.push([
1159
- this.OP.STORE_GLOBAL,
1160
- this.constants.intern(node.argument.name),
1161
- ]);
1432
+ this.emit(
1433
+ bc,
1434
+ [this.OP.STORE_GLOBAL, b.constantOperand(node.argument.name)],
1435
+ node,
1436
+ );
1162
1437
  };
1163
1438
 
1164
1439
  emitLoad();
1165
- if (!node.prefix) bc.push([this.OP.DUP]); // post: save old value before mutating
1166
- bc.push([this.OP.LOAD_CONST, one]);
1167
- bc.push([bumpOp]);
1440
+ if (!node.prefix) this.emit(bc, [this.OP.DUP], node); // post: save old value before mutating
1441
+ this.emit(bc, [this.OP.LOAD_CONST, one], node);
1442
+ this.emit(bc, [bumpOp], node);
1168
1443
  emitStore();
1169
1444
  if (node.prefix) emitLoad(); // pre: reload new value as result
1170
1445
 
@@ -1199,10 +1474,11 @@ class Compiler {
1199
1474
  if (node.left.computed) {
1200
1475
  this._compileExpr(node.left.property, scope, bc); // push key (runtime)
1201
1476
  } else {
1202
- bc.push([
1203
- this.OP.LOAD_CONST,
1204
- this.constants.intern(node.left.property.name),
1205
- ]);
1477
+ this.emit(
1478
+ bc,
1479
+ [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)],
1480
+ node,
1481
+ );
1206
1482
  }
1207
1483
 
1208
1484
  if (isCompound) {
@@ -1215,19 +1491,23 @@ class Compiler {
1215
1491
  if (node.left.computed) {
1216
1492
  this._compileExpr(node.left.property, scope, bc);
1217
1493
  } else {
1218
- bc.push([
1219
- this.OP.LOAD_CONST,
1220
- this.constants.intern(node.left.property.name),
1221
- ]);
1494
+ this.emit(
1495
+ bc,
1496
+ [
1497
+ this.OP.LOAD_CONST,
1498
+ b.constantOperand(node.left.property.name),
1499
+ ],
1500
+ node,
1501
+ );
1222
1502
  }
1223
- bc.push([this.OP.GET_PROP_COMPUTED]); // [..., obj, key, currentVal]
1503
+ this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
1224
1504
  this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
1225
- bc.push([compoundOp]); // [..., obj, key, newVal]
1505
+ this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
1226
1506
  } else {
1227
1507
  this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
1228
1508
  }
1229
1509
 
1230
- bc.push([this.OP.SET_PROP]); // obj[key] = val, leaves val on stack
1510
+ this.emit(bc, [this.OP.SET_PROP], node); // obj[key] = val, leaves val on stack
1231
1511
  break;
1232
1512
  }
1233
1513
 
@@ -1237,34 +1517,35 @@ class Compiler {
1237
1517
  if (isCompound) {
1238
1518
  // Load the current value of the target first
1239
1519
  if (res.kind === "local") {
1240
- bc.push([this.OP.LOAD_LOCAL, res.slot]);
1520
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1241
1521
  } else if (res.kind === "upvalue") {
1242
- bc.push([this.OP.LOAD_UPVALUE, res.index]);
1522
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1243
1523
  } else {
1244
- bc.push([
1245
- this.OP.LOAD_GLOBAL,
1246
- this.constants.intern(node.left.name),
1247
- ]);
1524
+ this.emit(
1525
+ bc,
1526
+ [this.OP.LOAD_GLOBAL, b.constantOperand(node.left.name)],
1527
+ node,
1528
+ );
1248
1529
  }
1249
1530
  }
1250
1531
 
1251
1532
  this._compileExpr(node.right, scope, bc); // push RHS
1252
1533
 
1253
1534
  if (isCompound) {
1254
- bc.push([compoundOp]); // apply binary op -> leaves newVal on stack
1535
+ this.emit(bc, [compoundOp], node); // apply binary op -> leaves newVal on stack
1255
1536
  }
1256
1537
 
1257
1538
  // Store & leave value on stack (assignment is an expression)
1258
1539
  if (res.kind === "local") {
1259
- bc.push([this.OP.STORE_LOCAL, res.slot]);
1260
- bc.push([this.OP.LOAD_LOCAL, res.slot]);
1540
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
1541
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1261
1542
  } else if (res.kind === "upvalue") {
1262
- bc.push([this.OP.STORE_UPVALUE, res.index]);
1263
- bc.push([this.OP.LOAD_UPVALUE, res.index]);
1543
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
1544
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1264
1545
  } else {
1265
- const nameIdx = this.constants.intern(node.left.name);
1266
- bc.push([this.OP.STORE_GLOBAL, nameIdx]);
1267
- bc.push([this.OP.LOAD_GLOBAL, nameIdx]);
1546
+ const nameIdx = b.constantOperand(node.left.name);
1547
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx], node);
1548
+ this.emit(bc, [this.OP.LOAD_GLOBAL, nameIdx], node);
1268
1549
  }
1269
1550
  break;
1270
1551
  }
@@ -1275,16 +1556,16 @@ class Compiler {
1275
1556
  // Push receiver first (GET_PROP leaves it; CALL_METHOD pops it as `this`)
1276
1557
  this._compileExpr(node.callee.object, scope, bc);
1277
1558
  const prop = node.callee.property.name;
1278
- const propIdx = this.constants.intern(prop);
1279
- bc.push([this.OP.LOAD_CONST, propIdx]);
1280
- bc.push([this.OP.GET_PROP]);
1559
+ const propIdx = b.constantOperand(prop);
1560
+ this.emit(bc, [this.OP.LOAD_CONST, propIdx], node);
1561
+ this.emit(bc, [this.OP.GET_PROP], node);
1281
1562
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1282
- bc.push([this.OP.CALL_METHOD, node.arguments.length]);
1563
+ this.emit(bc, [this.OP.CALL_METHOD, node.arguments.length], node);
1283
1564
  } else {
1284
1565
  // ── Plain call: add(5, 10)
1285
1566
  this._compileExpr(node.callee, scope, bc);
1286
1567
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1287
- bc.push([this.OP.CALL, node.arguments.length]);
1568
+ this.emit(bc, [this.OP.CALL, node.arguments.length], node);
1288
1569
  }
1289
1570
  break;
1290
1571
  }
@@ -1297,19 +1578,18 @@ class Compiler {
1297
1578
  const res = this._resolve(node.argument.name, this._currentCtx);
1298
1579
  if (res.kind === "global") {
1299
1580
  // Potentially undeclared -- let VM guard it
1300
- bc.push([
1301
- this.OP.LOAD_CONST,
1302
- this.constants.intern(node.argument.name),
1303
- ]);
1304
- bc.push([this.OP.TYPEOF_SAFE]);
1581
+ this.emit(
1582
+ bc,
1583
+ [this.OP.LOAD_CONST, b.constantOperand(node.argument.name)],
1584
+ node,
1585
+ );
1586
+ this.emit(bc, [this.OP.TYPEOF_SAFE], node);
1305
1587
  break;
1306
1588
  }
1307
1589
  // Known local or upvalue -- safe to load first, then typeof
1308
1590
  }
1309
1591
 
1310
1592
  // Special case: delete -- argument must NOT be pre-evaluated.
1311
- // The generic path below compiles the argument first, which would leave
1312
- // a stale value on the stack before the delete result, corrupting it.
1313
1593
  if (node.operator === "delete") {
1314
1594
  const arg = node.argument;
1315
1595
  if (arg.type === "MemberExpression") {
@@ -1317,15 +1597,16 @@ class Compiler {
1317
1597
  if (arg.computed) {
1318
1598
  this._compileExpr(arg.property, scope, bc);
1319
1599
  } else {
1320
- bc.push([
1321
- this.OP.LOAD_CONST,
1322
- this.constants.intern(arg.property.name),
1323
- ]);
1600
+ this.emit(
1601
+ bc,
1602
+ [this.OP.LOAD_CONST, b.constantOperand(arg.property.name)],
1603
+ node,
1604
+ );
1324
1605
  }
1325
- bc.push([this.OP.DELETE_PROP]);
1606
+ this.emit(bc, [this.OP.DELETE_PROP], node);
1326
1607
  } else {
1327
1608
  // delete x, delete 0, etc. -- always true in non-strict, just push true
1328
- bc.push([this.OP.LOAD_CONST, this.constants.intern(true)]);
1609
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(true)], node);
1329
1610
  }
1330
1611
  break;
1331
1612
  }
@@ -1334,22 +1615,22 @@ class Compiler {
1334
1615
  this._compileExpr(node.argument, scope, bc);
1335
1616
  switch (node.operator) {
1336
1617
  case "-":
1337
- bc.push([this.OP.UNARY_NEG]);
1618
+ this.emit(bc, [this.OP.UNARY_NEG], node);
1338
1619
  break;
1339
1620
  case "+":
1340
- bc.push([this.OP.UNARY_POS]);
1621
+ this.emit(bc, [this.OP.UNARY_POS], node);
1341
1622
  break;
1342
1623
  case "!":
1343
- bc.push([this.OP.UNARY_NOT]);
1624
+ this.emit(bc, [this.OP.UNARY_NOT], node);
1344
1625
  break;
1345
1626
  case "~":
1346
- bc.push([this.OP.UNARY_BITNOT]);
1627
+ this.emit(bc, [this.OP.UNARY_BITNOT], node);
1347
1628
  break;
1348
1629
  case "typeof":
1349
- bc.push([this.OP.TYPEOF]);
1630
+ this.emit(bc, [this.OP.TYPEOF], node);
1350
1631
  break;
1351
1632
  case "void":
1352
- bc.push([this.OP.VOID]);
1633
+ this.emit(bc, [this.OP.VOID], node);
1353
1634
  break;
1354
1635
 
1355
1636
  default:
@@ -1361,10 +1642,18 @@ class Compiler {
1361
1642
  case "RegExpLiteral": {
1362
1643
  // Emit: new RegExp(pattern, flags)
1363
1644
  // Fresh object per evaluation -- correct for stateful g/y flags.
1364
- bc.push([this.OP.LOAD_GLOBAL, this.constants.intern("RegExp")]);
1365
- bc.push([this.OP.LOAD_CONST, this.constants.intern(node.pattern)]);
1366
- bc.push([this.OP.LOAD_CONST, this.constants.intern(node.flags)]);
1367
- bc.push([this.OP.NEW, 2]);
1645
+ this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand("RegExp")], node);
1646
+ this.emit(
1647
+ bc,
1648
+ [this.OP.LOAD_CONST, b.constantOperand(node.pattern)],
1649
+ node,
1650
+ );
1651
+ this.emit(
1652
+ bc,
1653
+ [this.OP.LOAD_CONST, b.constantOperand(node.flags)],
1654
+ node,
1655
+ );
1656
+ this.emit(bc, [this.OP.NEW, 2], node);
1368
1657
  break;
1369
1658
  }
1370
1659
 
@@ -1373,7 +1662,12 @@ class Compiler {
1373
1662
  // but leave the resulting closure ON THE STACK -- no store.
1374
1663
  // The surrounding expression (assignment, call arg, return) consumes it.
1375
1664
  const desc = this._compileFunctionDecl(node);
1376
- bc.push([this.OP.MAKE_CLOSURE, desc._constIdx]);
1665
+ this._emitClosureMetadata(desc, node, bc);
1666
+ this.emit(
1667
+ bc,
1668
+ [this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
1669
+ node,
1670
+ );
1377
1671
  break;
1378
1672
  }
1379
1673
 
@@ -1384,16 +1678,17 @@ class Compiler {
1384
1678
  this._compileExpr(node.property, scope, bc);
1385
1679
  } else {
1386
1680
  // point.x -- push key as string, same opcode handles both
1387
- bc.push([
1388
- this.OP.LOAD_CONST,
1389
- this.constants.intern(node.property.name),
1390
- ]);
1681
+ this.emit(
1682
+ bc,
1683
+ [this.OP.LOAD_CONST, b.constantOperand(node.property.name)],
1684
+ node,
1685
+ );
1391
1686
  }
1392
1687
 
1393
1688
  // GET_PROP_COMPUTED pops the object -- correct for value access.
1394
1689
  // GET_PROP (peek) is only used in CallExpression's method call path
1395
1690
  // where the receiver must survive on the stack for CALL_METHOD.
1396
- bc.push([this.OP.GET_PROP_COMPUTED]);
1691
+ this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
1397
1692
  break;
1398
1693
  }
1399
1694
 
@@ -1403,45 +1698,114 @@ class Compiler {
1403
1698
  for (const el of node.elements) {
1404
1699
  if (el === null) {
1405
1700
  // hole: e.g. [1,,3]
1406
- bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
1701
+ this.emit(
1702
+ bc,
1703
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
1704
+ node,
1705
+ );
1407
1706
  } else {
1408
1707
  this._compileExpr(el, scope, bc);
1409
1708
  }
1410
1709
  }
1411
- bc.push([this.OP.BUILD_ARRAY, node.elements.length]);
1710
+ this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
1412
1711
  break;
1413
1712
  }
1414
1713
  case "ObjectExpression": {
1415
- // For each property: push key (always as string), push value.
1416
- // BUILD_OBJECT pops pairs right->left and assembles the object.
1714
+ // Separate regular data properties from ES5 accessor methods (get/set).
1715
+ const regularProps: t.ObjectProperty[] = [];
1716
+ const accessorProps: t.ObjectMethod[] = [];
1717
+
1417
1718
  for (const prop of node.properties) {
1418
1719
  if (prop.type === "SpreadElement") {
1419
1720
  throw new Error("Object spread not supported");
1420
1721
  }
1421
- // Key -- identifier shorthand (`{x:1}`) or string/number literal
1722
+ if (prop.type === "ObjectMethod") {
1723
+ if (prop.kind === "get" || prop.kind === "set") {
1724
+ if (prop.computed) {
1725
+ throw new Error(
1726
+ "Computed getter/setter keys are not supported",
1727
+ );
1728
+ }
1729
+ accessorProps.push(prop);
1730
+ } else {
1731
+ throw new Error(`Shorthand method syntax is not supported`);
1732
+ }
1733
+ } else {
1734
+ regularProps.push(prop as t.ObjectProperty);
1735
+ }
1736
+ }
1737
+
1738
+ // Build the base object from data properties.
1739
+ for (const prop of regularProps) {
1422
1740
  const key = prop.key;
1423
- let keyStr;
1741
+ let keyStr: string;
1424
1742
  if (key.type === "Identifier") {
1425
- keyStr = key.name; // {x: 1} -> key is "x"
1743
+ keyStr = key.name;
1426
1744
  } else if (
1427
1745
  key.type === "StringLiteral" ||
1428
1746
  key.type === "NumericLiteral"
1429
1747
  ) {
1430
- keyStr = String(key.value); // {"x": 1} or {0: 1}
1748
+ keyStr = String(key.value);
1431
1749
  } else {
1432
1750
  throw new Error(`Unsupported object key type: ${key.type}`);
1433
1751
  }
1434
- bc.push([this.OP.LOAD_CONST, this.constants.intern(keyStr)]);
1435
- // Value -- any expression, including FunctionExpression
1752
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1436
1753
  this._compileExpr(prop.value, scope, bc);
1437
1754
  }
1438
- bc.push([this.OP.BUILD_OBJECT, node.properties.length]);
1755
+ this.emit(bc, [this.OP.BUILD_OBJECT, regularProps.length], node);
1756
+
1757
+ // Define each accessor on the object that is now on top of the stack.
1758
+ // Stack after BUILD_OBJECT: [..., obj]
1759
+ // For each accessor: DUP obj, push key, compile fn, DEFINE_GETTER/DEFINE_SETTER
1760
+ // DEFINE_GETTER/DEFINE_SETTER pops fn+key+obj, leaving the original obj.
1761
+ for (const prop of accessorProps) {
1762
+ const key = prop.key;
1763
+ let keyStr: string;
1764
+ if (key.type === "Identifier") {
1765
+ keyStr = key.name;
1766
+ } else if (
1767
+ key.type === "StringLiteral" ||
1768
+ key.type === "NumericLiteral"
1769
+ ) {
1770
+ keyStr = String(key.value);
1771
+ } else {
1772
+ throw new Error(`Unsupported object key type: ${key.type}`);
1773
+ }
1774
+
1775
+ this.emit(bc, [this.OP.DUP], node); // dup so the original obj stays after the define
1776
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1777
+
1778
+ // Compile the accessor body as an anonymous function descriptor.
1779
+ const desc = this._compileFunctionDecl(prop as any);
1780
+ this._emitClosureMetadata(desc, prop as any, bc);
1781
+ this.emit(
1782
+ bc,
1783
+ [
1784
+ this.OP.MAKE_CLOSURE,
1785
+ {
1786
+ type: "label",
1787
+ label: desc.entryLabel,
1788
+ },
1789
+ ],
1790
+ node,
1791
+ );
1792
+
1793
+ this.emit(
1794
+ bc,
1795
+ [
1796
+ prop.kind === "get"
1797
+ ? this.OP.DEFINE_GETTER
1798
+ : this.OP.DEFINE_SETTER,
1799
+ ],
1800
+ node,
1801
+ );
1802
+ }
1803
+
1439
1804
  break;
1440
1805
  }
1441
1806
 
1442
1807
  default: {
1443
- const src = generate(node).code;
1444
- throw new Error(`Unsupported expression: ${node.type}\n -> ${src}`);
1808
+ throw new Error(`Unsupported expression: ${node.type}`);
1445
1809
  }
1446
1810
  }
1447
1811
  }
@@ -1449,6 +1813,8 @@ class Compiler {
1449
1813
 
1450
1814
  // Serializer
1451
1815
  // Turns the compiled output into a commented JS source string.
1816
+ // Expects fully-resolved bytecode (all label refs and constant refs already
1817
+ // converted to plain integers by resolveLabels + resolveConstants passes).
1452
1818
  class Serializer {
1453
1819
  compiler: Compiler;
1454
1820
 
@@ -1472,43 +1838,52 @@ class Serializer {
1472
1838
  return this.compiler.JUMP_OPS;
1473
1839
  }
1474
1840
 
1475
- get constants() {
1476
- return this.compiler.constants.items;
1477
- }
1478
-
1479
- get fnDescriptors() {
1480
- return this.compiler.fnDescriptors;
1481
- }
1482
-
1483
1841
  // Produce a JS literal for a constant pool entry
1484
1842
  _serializeConst(val) {
1485
1843
  if (val === null) return "null";
1486
1844
  if (val === undefined) return "undefined";
1487
- if (typeof val === "object" && val._fnIdx !== undefined) {
1488
- return `FN[${val._fnIdx}]`; // fn descriptor -> reference by FN index
1489
- }
1490
1845
  return JSON.stringify(val); // number / string / bool
1491
1846
  }
1492
1847
 
1493
1848
  // One instruction -> "[op, operand] // MNEMONIC description"
1494
- _serializeInstr(instr) {
1495
- const constants = this.constants;
1849
+ // Expects a fully-resolved instruction: operand is a plain number or undefined.
1850
+ _serializeInstr(instr: b.Instruction, constants: any[]) {
1851
+ const [op, rawOperand] = instr;
1852
+
1853
+ ok(
1854
+ rawOperand === undefined || typeof rawOperand === "number",
1855
+ "Unresolved operand: " + JSON.stringify(rawOperand),
1856
+ );
1857
+ const operand = rawOperand as number | undefined;
1496
1858
 
1497
- const [op, operand] = instr;
1498
1859
  const name = this.OP_NAME[op] || `OP_${op}`;
1499
1860
  let comment = name;
1500
1861
 
1862
+ const sourceNode = instr[SOURCE_NODE_SYM];
1863
+ const sourceLocation = sourceNode
1864
+ ? sourceNode.loc.start?.line +
1865
+ ":" +
1866
+ sourceNode.loc.start?.column +
1867
+ "-" +
1868
+ (sourceNode.loc.end?.line + ":" + sourceNode.loc.end?.column)
1869
+ : "";
1870
+
1501
1871
  // Annotate operand with its meaning
1502
1872
  if (operand !== undefined) {
1503
1873
  switch (op) {
1504
- case this.OP.LOAD_CONST:
1505
- case this.OP.MAKE_CLOSURE: {
1874
+ case this.OP.LOAD_CONST: {
1506
1875
  const val = constants[operand];
1507
- if (val && typeof val === "object" && val.name) {
1508
- comment += ` FN[${val._fnIdx}] -> fn:${val.name}`;
1509
- } else {
1510
- comment += ` ${JSON.stringify(val)}`;
1511
- }
1876
+ comment += ` ${this._serializeConst(val)}`;
1877
+ break;
1878
+ }
1879
+ case this.OP.MAKE_CLOSURE: {
1880
+ // operand is the absolute PC of the function body's first instruction
1881
+ comment += ` PC ${operand}`;
1882
+ break;
1883
+ }
1884
+ case this.OP.DATA: {
1885
+ // Inline function header word — value is a raw integer
1886
+ comment += ` ${operand}`;
1512
1887
  break;
1513
1888
  }
1514
1889
  case this.OP.LOAD_LOCAL:
@@ -1544,15 +1919,17 @@ class Serializer {
1544
1919
  }
1545
1920
  }
1546
1921
 
1922
+ comment = comment.padEnd(40) + sourceLocation;
1923
+
1547
1924
  // Pack a [op, operand?] instruction pair into a single 32-bit word.
1548
1925
  // Shared between the Serializer and the obfuscation path in _compileMain.
1549
1926
 
1550
- if (!this.options.encodeBytecode) {
1551
- const instrText =
1552
- operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
1927
+ const instrText = operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
1928
+ const text = `${(instrText + ",").padEnd(12)} ${comment}`;
1553
1929
 
1930
+ if (!this.options.encodeBytecode) {
1554
1931
  return {
1555
- text: ` ${instrText.padEnd(12)}, // ${comment}`,
1932
+ text: text,
1556
1933
  value: operand !== undefined ? [op, operand] : [op],
1557
1934
  };
1558
1935
  }
@@ -1569,44 +1946,35 @@ class Serializer {
1569
1946
  }
1570
1947
 
1571
1948
  return {
1572
- text: "",
1949
+ text: text,
1573
1950
  value: packInstr(instr),
1574
1951
  };
1575
1952
  }
1576
1953
 
1577
- // Serialize one fn descriptor into its FN[n] block
1578
- _serializeFn(desc) {
1579
- const lines = [
1580
- ` { // FN[${desc._fnIdx}] -- ${desc.name}`,
1581
- ` paramCount: ${desc.paramCount},`,
1582
- ` localCount: ${desc.localCount},`,
1583
- ` upvalueDescriptors: ${JSON5.stringify(desc.upvalueDescriptors)},`,
1584
- ` startPc: ${desc.startPc},`,
1585
- ` },`,
1586
- ];
1587
- return lines.join("\n");
1588
- }
1589
-
1590
- // Serialize the CONSTANTS array, showing FN[n] references
1591
- _serializeConstants() {
1954
+ // Serialize the CONSTANTS array
1955
+ _serializeConstants(constants: any[]) {
1592
1956
  const lines = ["var CONSTANTS = ["];
1593
- this.constants.forEach((val, idx) => {
1957
+ constants.forEach((val, idx) => {
1594
1958
  lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
1595
1959
  });
1596
1960
  lines.push("];");
1597
1961
  return lines.join("\n");
1598
1962
  }
1599
1963
 
1600
- _serializeBytecode(bytecode) {
1601
- if (!this.options.encodeBytecode) {
1602
- return bytecode.map((instr) => this._serializeInstr(instr).value);
1603
- }
1964
+ // Filter out any remaining null-opcode pseudo-instructions.
1965
+ // (defineLabel pseudo-ops are already stripped by resolveLabels.)
1966
+ _serializeBytecode(bytecode: b.Bytecode): { bytecode: b.Bytecode } {
1967
+ return {
1968
+ bytecode: bytecode.filter((instr) => instr[0] !== null),
1969
+ };
1970
+ }
1604
1971
 
1972
+ __serializeBytecode(bytecode: b.Bytecode, constants: any[]) {
1605
1973
  let words = [];
1606
1974
 
1607
1975
  // BYTECODE
1608
1976
  for (const instr of bytecode) {
1609
- words.push(this._serializeInstr(instr).value);
1977
+ words.push(this._serializeInstr(instr, constants).value);
1610
1978
  }
1611
1979
 
1612
1980
  // Convert packed words -> raw 4-byte little-endian binary -> base64
@@ -1617,30 +1985,32 @@ class Serializer {
1617
1985
  buf[i * 4 + 2] = (w >>> 16) & 0xff;
1618
1986
  buf[i * 4 + 3] = (w >>> 24) & 0xff;
1619
1987
  });
1620
- const b64 = Buffer.from(buf).toString("base64");
1621
-
1622
- return b64;
1988
+ return Buffer.from(buf).toString("base64");
1623
1989
  }
1624
1990
 
1625
- serialize(bytecode, mainStartPc) {
1626
- const sections = [];
1991
+ serialize(bytecode: b.Bytecode, constants: any[], compiler: Compiler) {
1992
+ const mainStartPc = compiler.mainStartPc;
1993
+ let sections = [];
1994
+
1995
+ var textForm = [];
1996
+ var initBody = [];
1627
1997
 
1628
- // ── FN array
1629
- const fnLines = ["var FN = ["];
1630
- for (const desc of this.fnDescriptors) {
1631
- fnLines.push(this._serializeFn(desc));
1998
+ var bytecodeResult = this._serializeBytecode(bytecode);
1999
+
2000
+ for (const instr of bytecodeResult.bytecode) {
2001
+ const serialized = this._serializeInstr(instr, constants);
2002
+ textForm.push(serialized.text);
1632
2003
  }
1633
- fnLines.push("];");
1634
- sections.push(fnLines.join("\n"));
1635
2004
 
1636
- // ── CONSTANTS
1637
- sections.push(this._serializeConstants());
2005
+ initBody.push(textForm.map((line) => `// ${line}`).join("\n"));
1638
2006
 
1639
2007
  if (this.options.encodeBytecode) {
1640
- sections.push(`var BYTECODE = "${this._serializeBytecode(bytecode)}";`);
2008
+ sections.push(
2009
+ `var BYTECODE = "${this.__serializeBytecode(bytecodeResult.bytecode, constants)}";`,
2010
+ );
1641
2011
  } else {
1642
2012
  sections.push(
1643
- `var BYTECODE = [\n ${bytecode.map((instr) => this._serializeInstr(instr).text).join(",\n ")}\n];`,
2013
+ `var BYTECODE = [${bytecodeResult.bytecode.map((v) => "[" + v[0] + ", " + v[1] + "]").join(",")}]`,
1644
2014
  );
1645
2015
  }
1646
2016
 
@@ -1651,6 +2021,11 @@ class Serializer {
1651
2021
  // Opcodes
1652
2022
  sections.push(`var OP = ${JSON5.stringify(this.OP)};`);
1653
2023
 
2024
+ // Constants must be defined before the bytecode
2025
+ initBody.push(this._serializeConstants(constants));
2026
+
2027
+ sections = [...initBody, ...sections];
2028
+
1654
2029
  // VM runtime
1655
2030
  sections.push(VM_RUNTIME);
1656
2031
 
@@ -1663,10 +2038,24 @@ export async function compileAndSerialize(
1663
2038
  options: Options,
1664
2039
  ) {
1665
2040
  const compiler = new Compiler(options);
1666
- const result = compiler.compile(sourceCode);
2041
+ let bytecode = compiler.compile(sourceCode);
2042
+
2043
+ // User transform passes (operate on unresolved IR with label/constant refs)
2044
+ const passes = [...(options.selfModifying ? [selfModifying] : [])];
2045
+ for (const pass of passes) {
2046
+ const passResult = pass(bytecode, compiler);
2047
+ bytecode = passResult.bytecode;
2048
+ }
2049
+
2050
+ // Assembler phases: resolve IR operands to plain integers before printing
2051
+ const { bytecode: labelResolved } = resolveLabels(bytecode, compiler);
2052
+ const { bytecode: finalBytecode, constants } =
2053
+ resolveConstants(labelResolved);
2054
+
1667
2055
  const output = compiler.serializer.serialize(
1668
- result.bytecode,
1669
- result.mainStartPc,
2056
+ finalBytecode,
2057
+ constants,
2058
+ compiler,
1670
2059
  );
1671
2060
 
1672
2061
  const finalOutput = await obfuscateRuntime(output, options);