js-confuser-vm 0.0.8 → 0.1.0

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
@@ -10,6 +10,7 @@ import { ok } from "assert";
10
10
  import { obfuscateRuntime } from "./build-runtime.ts";
11
11
  import { DEFAULT_OPTIONS, type Options } from "./options.ts";
12
12
  import { resolveLabels } from "./transforms/bytecode/resolveLabels.ts";
13
+ import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.ts";
13
14
  import { resolveConstants } from "./transforms/bytecode/resolveContants.ts";
14
15
  import { selfModifying } from "./transforms/bytecode/selfModifying.ts";
15
16
  import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.ts";
@@ -19,6 +20,7 @@ import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.ts";
19
20
  import { getRandomInt } from "./utils/random-utils.ts";
20
21
  import { U16_MAX } from "./utils/op-utils.ts";
21
22
  import { concealConstants } from "./transforms/bytecode/concealConstants.ts";
23
+ import { dispatcher } from "./transforms/bytecode/dispatcher.ts";
22
24
 
23
25
  const traverse = (traverseImport.default ||
24
26
  traverseImport) as typeof traverseImport.default;
@@ -139,78 +141,123 @@ export const OP_ORIGINAL = {
139
141
 
140
142
  // ── Debug ─────────────────────────────────────────────────────────────────
141
143
  DEBUGGER: 57,
144
+
145
+ // ── Indirect jump (register-addressed) ───────────────────────────────────
146
+ // Used by the jumpDispatcher pass. The target PC is read from a register
147
+ // rather than encoded as a bytecode immediate, so static analysis cannot
148
+ // determine the destination without tracking register values at runtime.
149
+ JUMP_REG: 58, // src — frame._pc = regs[src]
142
150
  };
143
151
 
144
152
  // ── Scope ─────────────────────────────────────────────────────────────────────
145
- // Maps variable names to register indices (slot numbers).
146
- // Locals are allocated at compile time; zero name lookups at runtime.
153
+ // Maps variable names to virtual RegisterOperands.
154
+ // Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
155
+ // resolveRegisters() assigns concrete slot indices before serialisation.
147
156
  class Scope {
148
157
  parent: Scope | null;
149
- _locals: Map<string, number>;
150
- _next: number;
158
+ _locals: Map<string, b.RegisterOperand>;
151
159
 
152
160
  constructor(parent = null) {
153
161
  this.parent = parent;
154
162
  this._locals = new Map();
155
- this._next = 0;
156
163
  }
157
164
 
158
- define(name: string): number {
165
+ define(name: string, ctx: FnContext): b.RegisterOperand {
159
166
  if (!this._locals.has(name)) {
160
- this._locals.set(name, this._next++);
167
+ this._locals.set(name, ctx._newReg());
161
168
  }
162
169
  return this._locals.get(name)!;
163
170
  }
164
171
 
165
- resolve(name: string): { kind: "local"; slot: number } | { kind: "global" } {
172
+ resolve(
173
+ name: string,
174
+ ): { kind: "local"; reg: b.RegisterOperand } | { kind: "global" } {
166
175
  if (this._locals.has(name)) {
167
- return { kind: "local", slot: this._locals.get(name)! };
176
+ return { kind: "local", reg: this._locals.get(name)! };
168
177
  }
169
178
  if (this.parent) return this.parent.resolve(name);
170
179
  return { kind: "global" };
171
180
  }
172
-
173
- get localCount() {
174
- return this._next;
175
- }
176
181
  }
177
182
 
178
183
  // ── FnContext ─────────────────────────────────────────────────────────────────
179
184
  // Compiler-side state for the function currently being compiled.
180
185
  // Distinct from the runtime Frame — this is compile-time only.
186
+ //
187
+ // Virtual-register model (Lua/LLVM style):
188
+ // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
189
+ // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
190
+ // liveness-aware slot assignment and sets desc.regCount at the end of the
191
+ // pipeline, just like resolveLabels() fills in jump targets.
181
192
  class FnContext {
182
- upvalues: { name: string; isLocal: number; index: number }[];
193
+ // index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
194
+ upvalues: {
195
+ name: string;
196
+ isLocal: number;
197
+ index: number | b.RegisterOperand;
198
+ }[];
183
199
  parentCtx: FnContext | null;
184
200
  scope: Scope;
185
201
  compiler: Compiler;
186
202
  bc: b.Instruction[];
187
203
 
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;
204
+ // Unique ID for this function — matches the index in compiler.fnDescriptors.
205
+ _fnId: number;
206
+ // Monotonically increasing counter; each call to _newReg() bumps it.
207
+ _nextId: number = 0;
192
208
 
193
- constructor(compiler: Compiler, parentCtx: FnContext | null = null) {
209
+ constructor(
210
+ compiler: Compiler,
211
+ parentCtx: FnContext | null = null,
212
+ fnId: number = 0,
213
+ ) {
194
214
  this.compiler = compiler;
195
215
  this.parentCtx = parentCtx;
196
216
  this.scope = new Scope();
197
217
  this.bc = [];
198
218
  this.upvalues = [];
219
+ this._fnId = fnId;
199
220
  }
200
221
 
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;
222
+ /** Create a new virtual register owned by this function. */
223
+ _newReg(): b.RegisterOperand {
224
+ return b.registerOperand(this._nextId++, this._fnId);
206
225
  }
207
226
 
208
- /** Release all temps above the local region. Called at each statement boundary. */
209
- resetTemps(): void {
210
- this.regTop = this.scope.localCount;
227
+ /**
228
+ * Allocate a short-lived temporary register (pool "temp::").
229
+ * resolveRegisters() will reuse its concrete slot once its live range ends.
230
+ * Do NOT use for named locals or upvalue-captured variables — use _newReg()
231
+ * via scope.define() for those, so they stay in the stable "local::" pool.
232
+ */
233
+ allocReg(): b.RegisterOperand {
234
+ return b.registerOperand(this._nextId++, this._fnId, { kind: "temp" });
235
+ }
236
+
237
+ /**
238
+ * Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
239
+ *
240
+ * NOTE: This is extraneous for any programmatically generated IR.
241
+ * resolveRegisters() already computes lastUse as the last instruction index
242
+ * where the register appears as a real operand — which is always the tightest
243
+ * correct bound when you stop emitting a register after its last logical use.
244
+ * freeReg is only needed in the rare case where a register has a late syntactic
245
+ * appearance that does NOT represent its true logical death (e.g. a dummy read
246
+ * emitted for side-effects long after the value is logically dead). No current
247
+ * pass in this codebase uses it; it is kept as an extension point only.
248
+ */
249
+ freeReg(bc: b.Bytecode, reg: b.RegisterOperand): void {
250
+ bc.push([null, b.freeRegOperand(reg)]);
211
251
  }
212
252
 
213
- addUpvalue(name: string, isLocal: number, index: number): number {
253
+ /** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
254
+ resetTemps(): void {}
255
+
256
+ addUpvalue(
257
+ name: string,
258
+ isLocal: number,
259
+ index: number | b.RegisterOperand,
260
+ ): number {
214
261
  const existing = this.upvalues.findIndex((u) => u.name === name);
215
262
  if (existing !== -1) return existing;
216
263
  const idx = this.upvalues.length;
@@ -233,6 +280,7 @@ interface FnDescriptor {
233
280
  * Only populated AFTER resolveLabels
234
281
  */
235
282
  startPc?: number;
283
+ ctx?: FnContext;
236
284
  }
237
285
 
238
286
  // ── Compiler ──────────────────────────────────────────────────────────────────
@@ -285,7 +333,17 @@ export class Compiler {
285
333
 
286
334
  constants: any[];
287
335
 
336
+ _cloneRegisterOperand<T extends b.InstrOperand>(operand: T): T {
337
+ if (!operand || typeof operand !== "object") return operand;
338
+ if ((operand as any).type !== "register") return operand;
339
+
340
+ return JSON.parse(JSON.stringify(operand)) as T;
341
+ }
342
+
288
343
  emit(bc: b.Bytecode, instr: b.Instruction, node: t.Node) {
344
+ for (let i = 1; i < instr.length; i++) {
345
+ instr[i] = this._cloneRegisterOperand(instr[i]);
346
+ }
289
347
  bc.push(instr);
290
348
  instr[SOURCE_NODE_SYM] = node;
291
349
  }
@@ -344,13 +402,13 @@ export class Compiler {
344
402
  name: string,
345
403
  ctx: FnContext | null,
346
404
  ):
347
- | { kind: "local"; slot: number }
405
+ | { kind: "local"; reg: b.RegisterOperand }
348
406
  | { kind: "upvalue"; index: number }
349
407
  | { kind: "global" } {
350
408
  if (!ctx) return { kind: "global" };
351
409
 
352
410
  if (ctx.scope._locals.has(name)) {
353
- return { kind: "local", slot: ctx.scope._locals.get(name)! };
411
+ return { kind: "local", reg: ctx.scope._locals.get(name)! };
354
412
  }
355
413
 
356
414
  if (!ctx.parentCtx) return { kind: "global" };
@@ -359,31 +417,30 @@ export class Compiler {
359
417
  if (parentResult.kind === "global") return { kind: "global" };
360
418
 
361
419
  const isLocal = parentResult.kind === "local";
362
- const index = isLocal ? parentResult.slot : parentResult.index;
420
+ const index = isLocal ? parentResult.reg : parentResult.index;
363
421
  const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
364
422
  return { kind: "upvalue", index: uvIdx };
365
423
  }
366
424
 
367
425
  // ── 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 {
426
+ // Pre-scan a statement list and reserve virtual registers for every var
427
+ // declaration, function declaration, for-in iterator, and try-catch binding.
428
+ // Must be called before any emit so that locals are allocated before temps.
429
+ _hoistVars(stmts: t.Statement[], scope: Scope, ctx: FnContext): void {
373
430
  for (const stmt of stmts) {
374
431
  switch (stmt.type) {
375
432
  case "VariableDeclaration":
376
433
  for (const decl of stmt.declarations) {
377
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
434
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
378
435
  }
379
436
  break;
380
437
 
381
438
  case "FunctionDeclaration":
382
- if (stmt.id) scope.define(stmt.id.name);
439
+ if (stmt.id) scope.define(stmt.id.name, ctx);
383
440
  break;
384
441
 
385
442
  case "BlockStatement":
386
- this._hoistVars(stmt.body, scope);
443
+ this._hoistVars(stmt.body, scope, ctx);
387
444
  break;
388
445
 
389
446
  case "IfStatement": {
@@ -391,13 +448,13 @@ export class Compiler {
391
448
  stmt.consequent.type === "BlockStatement"
392
449
  ? stmt.consequent.body
393
450
  : [stmt.consequent];
394
- this._hoistVars(cons, scope);
451
+ this._hoistVars(cons, scope, ctx);
395
452
  if (stmt.alternate) {
396
453
  const alt =
397
454
  stmt.alternate.type === "BlockStatement"
398
455
  ? stmt.alternate.body
399
456
  : [stmt.alternate];
400
- this._hoistVars(alt, scope);
457
+ this._hoistVars(alt, scope, ctx);
401
458
  }
402
459
  break;
403
460
  }
@@ -406,56 +463,58 @@ export class Compiler {
406
463
  case "DoWhileStatement": {
407
464
  const body =
408
465
  stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
409
- this._hoistVars(body, scope);
466
+ this._hoistVars(body, scope, ctx);
410
467
  break;
411
468
  }
412
469
 
413
470
  case "ForStatement": {
414
471
  if (stmt.init?.type === "VariableDeclaration") {
415
472
  for (const decl of stmt.init.declarations) {
416
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
473
+ if (decl.id.type === "Identifier")
474
+ scope.define(decl.id.name, ctx);
417
475
  }
418
476
  }
419
477
  const body =
420
478
  stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
421
- this._hoistVars(body, scope);
479
+ this._hoistVars(body, scope, ctx);
422
480
  break;
423
481
  }
424
482
 
425
483
  case "ForInStatement": {
426
- // Reserve a hidden register for the iterator object.
427
- (stmt as any)._iterSlot = scope._next++;
484
+ // Reserve a hidden virtual register for the iterator object.
485
+ (stmt as any)._iterSlot = ctx._newReg();
428
486
  if (stmt.left.type === "VariableDeclaration") {
429
487
  for (const decl of stmt.left.declarations) {
430
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
488
+ if (decl.id.type === "Identifier")
489
+ scope.define(decl.id.name, ctx);
431
490
  }
432
491
  }
433
492
  const body =
434
493
  stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
435
- this._hoistVars(body, scope);
494
+ this._hoistVars(body, scope, ctx);
436
495
  break;
437
496
  }
438
497
 
439
498
  case "SwitchStatement":
440
- for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
499
+ for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
441
500
  break;
442
501
 
443
502
  case "TryStatement":
444
- this._hoistVars(stmt.block.body, scope);
503
+ this._hoistVars(stmt.block.body, scope, ctx);
445
504
  if (stmt.handler) {
446
505
  if (stmt.handler.param?.type === "Identifier") {
447
506
  // Catch parameter IS the exception register.
448
- scope.define((stmt.handler.param as t.Identifier).name);
507
+ scope.define((stmt.handler.param as t.Identifier).name, ctx);
449
508
  } else {
450
- // No catch binding – reserve a dummy slot for the exception value.
451
- (stmt as any)._exceptionSlot = scope._next++;
509
+ // No catch binding – reserve a dummy virtual register for the exception value.
510
+ (stmt as any)._exceptionSlot = ctx._newReg();
452
511
  }
453
- this._hoistVars(stmt.handler.body.body, scope);
512
+ this._hoistVars(stmt.handler.body.body, scope, ctx);
454
513
  }
455
514
  break;
456
515
 
457
516
  case "LabeledStatement":
458
- this._hoistVars([stmt.body], scope);
517
+ this._hoistVars([stmt.body], scope, ctx);
459
518
  break;
460
519
  }
461
520
  }
@@ -463,7 +522,10 @@ export class Compiler {
463
522
 
464
523
  // ── Entry point ───────────────────────────────────────────────────────────
465
524
  compile(source: string) {
466
- const ast = parse(source, { sourceType: "script" });
525
+ const ast = parse(source, {
526
+ sourceType: "script",
527
+ allowReturnOutsideFunction: true,
528
+ });
467
529
  return this.compileAST(ast);
468
530
  }
469
531
 
@@ -482,32 +544,28 @@ export class Compiler {
482
544
  var desc: FnDescriptor = {};
483
545
  this.fnDescriptors.push(desc);
484
546
 
485
- const ctx = new FnContext(this, this._currentCtx);
547
+ const ctx = new FnContext(this, this._currentCtx, fnIdx);
486
548
  const savedCtx = this._currentCtx;
487
549
  this._currentCtx = ctx;
488
550
 
489
551
  const savedLoopStack = this._loopStack;
490
552
  this._loopStack = [];
491
553
 
492
- // 1. Define parameters (occupy regs 0..paramCount-1).
554
+ // 1. Define parameters as virtual registers (occupy the first IDs in order).
493
555
  for (const param of node.params) {
494
556
  let identifier = param.type === "AssignmentPattern" ? param.left : param;
495
557
  ok(
496
558
  identifier.type === "Identifier",
497
559
  "Only simple identifiers allowed as parameters",
498
560
  );
499
- ctx.scope.define((identifier as t.Identifier).name);
561
+ ctx.scope.define((identifier as t.Identifier).name, ctx);
500
562
  }
501
563
 
502
- // 2. Reserve the `arguments` slot (reg index = paramCount).
503
- ctx.scope.define("arguments");
564
+ // 2. Reserve the `arguments` virtual register (immediately after params).
565
+ ctx.scope.define("arguments", ctx);
504
566
 
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;
567
+ // 3. Hoist all var declarations so locals are allocated before any temps.
568
+ this._hoistVars(node.body.body, ctx.scope, ctx);
511
569
 
512
570
  // 5. Emit default-value guards.
513
571
  for (const param of node.params) {
@@ -569,21 +627,23 @@ export class Compiler {
569
627
  desc.bytecode = ctx.bc as b.Bytecode;
570
628
  desc._fnIdx = fnIdx;
571
629
  desc.paramCount = node.params.length;
572
- desc.regCount = ctx.maxRegTop; // total registers needed at runtime
630
+ // regCount is NOT set here resolveRegisters() fills it after liveness analysis.
573
631
  desc.upvalues = ctx.upvalues.slice();
632
+ desc.ctx = ctx;
574
633
 
575
634
  return desc;
576
635
  }
577
636
 
578
637
  // Emit MAKE_CLOSURE with all metadata as inline operands.
579
638
  // Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
639
+ // regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
580
640
  _emitMakeClosure(desc: any, node: t.Node, bc: b.Bytecode) {
581
641
  const ctx = this._currentCtx!;
582
642
  const dst = ctx.allocReg();
583
- const uvOperands: (number | b.InstrOperand)[] = [];
643
+ const uvOperands: b.InstrOperand[] = [];
584
644
  for (const uv of desc.upvalues) {
585
645
  uvOperands.push(uv.isLocal ? 1 : 0);
586
- uvOperands.push(uv.index);
646
+ uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
587
647
  }
588
648
  this.emit(
589
649
  bc,
@@ -592,7 +652,7 @@ export class Compiler {
592
652
  dst,
593
653
  { type: "label", label: desc.entryLabel },
594
654
  desc.paramCount,
595
- desc.regCount,
655
+ b.fnRegCountOperand(desc._fnIdx), // resolved by resolveRegisters()
596
656
  desc.upvalues.length,
597
657
  ...uvOperands,
598
658
  ] as b.Instruction,
@@ -612,7 +672,7 @@ export class Compiler {
612
672
  async: false,
613
673
  generator: false,
614
674
  params: [],
615
- id: null,
675
+ id: t.identifier("main"),
616
676
  body: t.blockStatement([...body]),
617
677
  });
618
678
 
@@ -626,7 +686,7 @@ export class Compiler {
626
686
  }
627
687
  }
628
688
 
629
- this.mainRegCount = mainCtx.maxRegTop;
689
+ // mainRegCount is set by resolveRegisters() after the pipeline runs.
630
690
  this.mainFn = desc;
631
691
  this._currentCtx = savedCtx;
632
692
  }
@@ -689,7 +749,7 @@ export class Compiler {
689
749
  }
690
750
 
691
751
  case "ReturnStatement": {
692
- let reg: number;
752
+ let reg: b.RegisterOperand;
693
753
  if (node.argument) {
694
754
  reg = this._compileExpr(node.argument, scope, bc);
695
755
  } else {
@@ -730,18 +790,12 @@ export class Compiler {
730
790
  this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
731
791
  }
732
792
  } 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();
793
+ // No initializer: var x; → load undefined directly into the local's register.
739
794
  this.emit(
740
795
  bc,
741
- [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)],
796
+ [this.OP.LOAD_CONST, slot, b.constantOperand(undefined)],
742
797
  node,
743
798
  );
744
- if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
745
799
  }
746
800
  } else {
747
801
  if (decl.init) {
@@ -772,7 +826,6 @@ export class Compiler {
772
826
  case "IfStatement": {
773
827
  const elseOrEndLabel = this._makeLabel("if_else");
774
828
 
775
- const savedTop = ctx.regTop;
776
829
  const testReg = this._compileExpr(node.test, scope, bc);
777
830
  this.emit(
778
831
  bc,
@@ -783,7 +836,6 @@ export class Compiler {
783
836
  ],
784
837
  node,
785
838
  );
786
- ctx.regTop = savedTop; // free test temps
787
839
 
788
840
  const consequentBody =
789
841
  node.consequent.type === "BlockStatement"
@@ -843,14 +895,12 @@ export class Compiler {
843
895
  node,
844
896
  );
845
897
 
846
- const savedTop = ctx.regTop;
847
898
  const testReg = this._compileExpr(node.test, scope, bc);
848
899
  this.emit(
849
900
  bc,
850
901
  [this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
851
902
  node,
852
903
  );
853
- ctx.regTop = savedTop;
854
904
 
855
905
  const whileBody =
856
906
  node.body.type === "BlockStatement" ? node.body.body : [node.body];
@@ -902,14 +952,13 @@ export class Compiler {
902
952
  node,
903
953
  );
904
954
 
905
- const savedTop = ctx.regTop;
906
955
  const testReg = this._compileExpr(node.test, scope, bc);
907
956
  this.emit(
908
957
  bc,
909
958
  [this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
910
959
  node,
911
960
  );
912
- ctx.regTop = savedTop;
961
+
913
962
  this.emit(
914
963
  bc,
915
964
  [this.OP.JUMP, { type: "label", label: loopTopLabel }],
@@ -954,7 +1003,6 @@ export class Compiler {
954
1003
  );
955
1004
 
956
1005
  if (node.test) {
957
- const savedTop = ctx.regTop;
958
1006
  const testReg = this._compileExpr(node.test, scope, bc);
959
1007
  this.emit(
960
1008
  bc,
@@ -965,7 +1013,6 @@ export class Compiler {
965
1013
  ],
966
1014
  node,
967
1015
  );
968
- ctx.regTop = savedTop;
969
1016
  }
970
1017
 
971
1018
  const forBody =
@@ -1103,7 +1150,6 @@ export class Compiler {
1103
1150
  if (cas.test === null) continue;
1104
1151
 
1105
1152
  const nextCheckLabel = this._makeLabel("sw_next");
1106
- const savedTop = ctx.regTop;
1107
1153
  const caseValReg = this._compileExpr(cas.test, scope, bc);
1108
1154
  const cmpReg = ctx.allocReg();
1109
1155
  this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
@@ -1116,7 +1162,7 @@ export class Compiler {
1116
1162
  ],
1117
1163
  node,
1118
1164
  );
1119
- ctx.regTop = savedTop;
1165
+
1120
1166
  this.emit(
1121
1167
  bc,
1122
1168
  [this.OP.JUMP, { type: "label", label: caseLabels[i] }],
@@ -1202,7 +1248,7 @@ export class Compiler {
1202
1248
  this._pendingLabel = null;
1203
1249
 
1204
1250
  // Iterator register was reserved by _hoistVars.
1205
- const iterSlot: number = (node as any)._iterSlot;
1251
+ const iterSlot: b.RegisterOperand = (node as any)._iterSlot;
1206
1252
 
1207
1253
  // FOR_IN_SETUP dst, src
1208
1254
  const objReg = this._compileExpr(node.right, scope, bc);
@@ -1259,8 +1305,8 @@ export class Compiler {
1259
1305
  } else if (node.left.type === "Identifier") {
1260
1306
  const res = this._resolve(node.left.name, this._currentCtx);
1261
1307
  if (res.kind === "local") {
1262
- if (keyReg !== res.slot)
1263
- this.emit(bc, [this.OP.MOVE, res.slot, keyReg], node);
1308
+ if (keyReg !== res.reg)
1309
+ this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
1264
1310
  } else if (res.kind === "upvalue") {
1265
1311
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
1266
1312
  } else {
@@ -1366,17 +1412,37 @@ export class Compiler {
1366
1412
  }
1367
1413
 
1368
1414
  // ── 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),
1415
+ // Returns the virtual RegisterOperand that holds the result.
1416
+ // For local variables: returns their RegisterOperand directly (no instruction emitted).
1417
+ // For all others: allocates a fresh virtual register, emits the instruction(s),
1372
1418
  // and returns the allocated register.
1373
1419
  _compileExpr(
1374
1420
  node: t.Expression | t.Node,
1375
1421
  scope: Scope | null,
1376
1422
  bc: b.Bytecode,
1377
- ): number {
1423
+ ): b.RegisterOperand {
1378
1424
  const ctx = this._currentCtx!;
1379
1425
 
1426
+ // Intrinsic for emitting raw bytecode, useful for emitting register address
1427
+ if (
1428
+ node.type === "CallExpression" &&
1429
+ node.callee.type === "Identifier" &&
1430
+ node.callee.name === "_VM_"
1431
+ ) {
1432
+ const argJSONStrng = (node.arguments[0] as t.StringLiteral).value;
1433
+ console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
1434
+ const arg = JSON.parse(argJSONStrng);
1435
+ console.log("Parsed bytecode:", arg);
1436
+
1437
+ const dst = ctx.allocReg();
1438
+
1439
+ let operand = arg[0];
1440
+
1441
+ this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
1442
+
1443
+ return dst;
1444
+ }
1445
+
1380
1446
  switch ((node as any).type) {
1381
1447
  case "NumericLiteral":
1382
1448
  case "StringLiteral":
@@ -1401,7 +1467,7 @@ export class Compiler {
1401
1467
  (node as t.Identifier).name,
1402
1468
  this._currentCtx,
1403
1469
  );
1404
- if (res.kind === "local") return res.slot; // register IS the local
1470
+ if (res.kind === "local") return res.reg; // register IS the local
1405
1471
  if (res.kind === "upvalue") {
1406
1472
  const dst = ctx.allocReg();
1407
1473
  this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
@@ -1454,9 +1520,7 @@ export class Compiler {
1454
1520
  case "SequenceExpression": {
1455
1521
  const exprs = (node as t.SequenceExpression).expressions;
1456
1522
  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
1523
+ this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
1460
1524
  }
1461
1525
  return this._compileExpr(exprs[exprs.length - 1], scope, bc);
1462
1526
  }
@@ -1466,17 +1530,14 @@ export class Compiler {
1466
1530
  const elseLabel = this._makeLabel("ternary_else");
1467
1531
  const endLabel = this._makeLabel("ternary_end");
1468
1532
 
1469
- // Compile test; free its temps after the jump is emitted.
1470
- const baseTop = ctx.regTop;
1471
1533
  const testReg = this._compileExpr(n.test, scope, bc);
1472
1534
  this.emit(
1473
1535
  bc,
1474
1536
  [this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: elseLabel }],
1475
1537
  node,
1476
1538
  );
1477
- ctx.regTop = baseTop; // free test temps
1478
1539
 
1479
- // Reserve reg_result at the base of the temp space.
1540
+ // reg_result is a stable virtual register both branches write into.
1480
1541
  const reg_result = ctx.allocReg();
1481
1542
 
1482
1543
  // Consequent branch.
@@ -1485,18 +1546,14 @@ export class Compiler {
1485
1546
  this.emit(bc, [this.OP.MOVE, reg_result, consReg], node);
1486
1547
  this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
1487
1548
 
1488
- // Alternate branch: reset to baseTop then re-reserve reg_result.
1549
+ // Alternate branch each allocReg() gets a unique virtual ID so no
1550
+ // slot collision is possible; no need to "re-occupy" reg_result.
1489
1551
  this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
1490
- ctx.regTop = baseTop;
1491
- ctx.allocReg(); // re-occupy reg_result slot
1492
1552
  const altReg = this._compileExpr(n.alternate, scope, bc);
1493
1553
  if (altReg !== reg_result)
1494
1554
  this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
1495
1555
 
1496
1556
  this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1497
-
1498
- // Leave reg_result allocated above baseTop.
1499
- ctx.regTop = baseTop + 1;
1500
1557
  return reg_result;
1501
1558
  }
1502
1559
 
@@ -1507,9 +1564,7 @@ export class Compiler {
1507
1564
  if (!isOr && n.operator !== "&&")
1508
1565
  throw new Error(`Unsupported logical operator: ${n.operator}`);
1509
1566
 
1510
- const baseTop = ctx.regTop;
1511
1567
  const lhsReg = this._compileExpr(n.left, scope, bc);
1512
- ctx.regTop = baseTop;
1513
1568
  const reg_result = ctx.allocReg();
1514
1569
  if (lhsReg !== reg_result)
1515
1570
  this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
@@ -1527,15 +1582,11 @@ export class Compiler {
1527
1582
  );
1528
1583
 
1529
1584
  // Compile RHS into reg_result.
1530
- ctx.regTop = baseTop;
1531
- ctx.allocReg(); // re-occupy reg_result
1532
1585
  const rhsReg = this._compileExpr(n.right, scope, bc);
1533
1586
  if (rhsReg !== reg_result)
1534
1587
  this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
1535
1588
 
1536
1589
  this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1537
-
1538
- ctx.regTop = baseTop + 1;
1539
1590
  return reg_result;
1540
1591
  }
1541
1592
 
@@ -1622,9 +1673,11 @@ export class Compiler {
1622
1673
  const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
1623
1674
 
1624
1675
  // Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
1625
- const applyBump = (curReg: number): [number, number] => {
1676
+ const applyBump = (
1677
+ curReg: b.RegisterOperand,
1678
+ ): [b.RegisterOperand, b.RegisterOperand] => {
1626
1679
  const postfixReg = n.prefix
1627
- ? -1
1680
+ ? curReg // prefix: postfix copy unused; caller returns newReg instead
1628
1681
  : (() => {
1629
1682
  const r = ctx.allocReg();
1630
1683
  this.emit(bc, [this.OP.MOVE, r, curReg], node as t.Node);
@@ -1644,7 +1697,7 @@ export class Compiler {
1644
1697
  if (n.argument.type === "MemberExpression") {
1645
1698
  const mem = n.argument as t.MemberExpression;
1646
1699
  const objReg = this._compileExpr(mem.object, scope, bc);
1647
- let keyReg: number;
1700
+ let keyReg: b.RegisterOperand;
1648
1701
  if (mem.computed) {
1649
1702
  keyReg = this._compileExpr(mem.property as t.Expression, scope, bc);
1650
1703
  } else {
@@ -1681,9 +1734,9 @@ export class Compiler {
1681
1734
  const name = (n.argument as t.Identifier).name;
1682
1735
  const res = this._resolve(name, this._currentCtx);
1683
1736
 
1684
- let curReg: number;
1737
+ let curReg: b.RegisterOperand;
1685
1738
  if (res.kind === "local") {
1686
- curReg = res.slot;
1739
+ curReg = res.reg;
1687
1740
  } else if (res.kind === "upvalue") {
1688
1741
  curReg = ctx.allocReg();
1689
1742
  this.emit(
@@ -1703,7 +1756,7 @@ export class Compiler {
1703
1756
  const [postfixReg, newReg] = applyBump(curReg);
1704
1757
 
1705
1758
  if (res.kind === "local") {
1706
- this.emit(bc, [this.OP.MOVE, res.slot, newReg], node as t.Node);
1759
+ this.emit(bc, [this.OP.MOVE, res.reg, newReg], node as t.Node);
1707
1760
  } else if (res.kind === "upvalue") {
1708
1761
  this.emit(
1709
1762
  bc,
@@ -1747,7 +1800,7 @@ export class Compiler {
1747
1800
  if (n.left.type === "MemberExpression") {
1748
1801
  const objReg = this._compileExpr(n.left.object, scope, bc);
1749
1802
 
1750
- let keyReg: number;
1803
+ let keyReg: b.RegisterOperand;
1751
1804
  if (n.left.computed) {
1752
1805
  keyReg = this._compileExpr(
1753
1806
  n.left.property as t.Expression,
@@ -1767,7 +1820,7 @@ export class Compiler {
1767
1820
  );
1768
1821
  }
1769
1822
 
1770
- let valReg: number;
1823
+ let valReg: b.RegisterOperand;
1771
1824
  if (isCompound) {
1772
1825
  const curReg = ctx.allocReg();
1773
1826
  this.emit(bc, [this.OP.GET_PROP, curReg, objReg, keyReg], node);
@@ -1788,12 +1841,12 @@ export class Compiler {
1788
1841
  this._currentCtx,
1789
1842
  );
1790
1843
 
1791
- let rhsReg: number;
1844
+ let rhsReg: b.RegisterOperand;
1792
1845
  if (isCompound) {
1793
1846
  // Load current value of the variable.
1794
- let curReg: number;
1847
+ let curReg: b.RegisterOperand;
1795
1848
  if (res.kind === "local") {
1796
- curReg = res.slot;
1849
+ curReg = res.reg;
1797
1850
  } else if (res.kind === "upvalue") {
1798
1851
  curReg = ctx.allocReg();
1799
1852
  this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
@@ -1818,9 +1871,9 @@ export class Compiler {
1818
1871
 
1819
1872
  // Store result and return it.
1820
1873
  if (res.kind === "local") {
1821
- if (rhsReg !== res.slot)
1822
- this.emit(bc, [this.OP.MOVE, res.slot, rhsReg], node);
1823
- return res.slot;
1874
+ if (rhsReg !== res.reg)
1875
+ this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
1876
+ return res.reg;
1824
1877
  } else if (res.kind === "upvalue") {
1825
1878
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
1826
1879
  return rhsReg;
@@ -1838,7 +1891,7 @@ export class Compiler {
1838
1891
  // Method call: receiver.method(args)
1839
1892
  const receiverReg = this._compileExpr(n.callee.object, scope, bc);
1840
1893
 
1841
- let methodKeyReg: number;
1894
+ let methodKeyReg: b.RegisterOperand;
1842
1895
  if (n.callee.computed) {
1843
1896
  methodKeyReg = this._compileExpr(
1844
1897
  n.callee.property as t.Expression,
@@ -1924,7 +1977,7 @@ export class Compiler {
1924
1977
  const arg = n.argument;
1925
1978
  if (arg.type === "MemberExpression") {
1926
1979
  const objReg = this._compileExpr(arg.object, scope, bc);
1927
- let keyReg: number;
1980
+ let keyReg: b.RegisterOperand;
1928
1981
  if (arg.computed) {
1929
1982
  keyReg = this._compileExpr(
1930
1983
  arg.property as t.Expression,
@@ -2017,7 +2070,7 @@ export class Compiler {
2017
2070
  case "MemberExpression": {
2018
2071
  const n = node as t.MemberExpression;
2019
2072
  const objReg = this._compileExpr(n.object, scope, bc);
2020
- let keyReg: number;
2073
+ let keyReg: b.RegisterOperand;
2021
2074
  if (n.computed) {
2022
2075
  keyReg = this._compileExpr(n.property as t.Expression, scope, bc);
2023
2076
  } else {
@@ -2084,7 +2137,7 @@ export class Compiler {
2084
2137
  }
2085
2138
 
2086
2139
  // Build flat [key, val, key, val, …] register list.
2087
- const pairRegs: number[] = [];
2140
+ const pairRegs: b.RegisterOperand[] = [];
2088
2141
  for (const prop of regularProps) {
2089
2142
  let keyStr: string;
2090
2143
  const key = prop.key;
@@ -2197,6 +2250,7 @@ class Serializer {
2197
2250
  const v = constants[idx];
2198
2251
  if (!key) return v;
2199
2252
  if (typeof v === "number") return v ^ key;
2253
+ if (typeof v !== "string") return v;
2200
2254
  // String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
2201
2255
  const bytes = Buffer.from(v as string, "base64");
2202
2256
  let out = "";
@@ -2207,21 +2261,36 @@ class Serializer {
2207
2261
  return out;
2208
2262
  }
2209
2263
 
2210
- _serializeInstr(instr: b.Instruction): { text: string; values: number[] } {
2264
+ _generateComment(instr: b.Instruction) {
2211
2265
  const op = instr[0] as number;
2212
2266
  const operands = instr.slice(1) as number[];
2213
2267
 
2268
+ if (op === null && (operands[0] as any)?.type === "defineLabel") {
2269
+ const label = (operands[0] as any).label;
2270
+ return `${label}:`;
2271
+ }
2272
+
2214
2273
  const constants = this.compiler.constants;
2215
2274
 
2216
- const resolvedOperands = operands
2217
- .filter((operand) => (operand as any)?.placeholder !== true)
2218
- .map((o) => (o as any)?.resolvedValue ?? o);
2275
+ const emittedOperands = operands.filter(
2276
+ (operand) => (operand as any)?.placeholder !== true,
2277
+ );
2219
2278
 
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}`);
2279
+ const resolvedOperands = emittedOperands.map(
2280
+ (o) => (o as any)?.resolvedValue ?? o,
2281
+ );
2282
+
2283
+ const displayOperands = operands.map((o, i) => {
2284
+ const resolvedValue = resolvedOperands[i];
2285
+ const label = (o as any)?.label;
2286
+
2287
+ let displayOperand = resolvedValue;
2288
+ if (label) {
2289
+ return label;
2290
+ }
2291
+
2292
+ return displayOperand;
2293
+ });
2225
2294
 
2226
2295
  let name = this.OP_NAME[op];
2227
2296
  if (!name || name.includes("{")) {
@@ -2241,71 +2310,85 @@ class Serializer {
2241
2310
  .join("-")
2242
2311
  : "";
2243
2312
 
2244
- if (resolvedOperands.length > 0) {
2313
+ if (displayOperands.length > 0) {
2245
2314
  // Operand[0] is always `dst` for instruction types that produce a value.
2246
- const dst = resolvedOperands[0];
2315
+ const dst = displayOperands[0];
2247
2316
 
2248
2317
  switch (op) {
2249
2318
  case this.OP.LOAD_CONST: {
2250
2319
  // resolvedOperands: [dst, constIdx, concealKey]
2251
2320
  const val = this._decryptConst(
2252
2321
  constants,
2253
- resolvedOperands[1],
2254
- resolvedOperands[2],
2322
+ displayOperands[1],
2323
+ displayOperands[2],
2255
2324
  );
2256
2325
  comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
2257
2326
  break;
2258
2327
  }
2328
+
2329
+ case this.OP.LOAD_INT: {
2330
+ // resolvedOperands: [dst, intValue]
2331
+ comment += ` reg[${dst}] = ${displayOperands[1]}`;
2332
+ break;
2333
+ }
2334
+
2259
2335
  case this.OP.LOAD_GLOBAL:
2260
2336
  // resolvedOperands: [dst, constIdx, concealKey]
2261
- comment += ` reg[${dst}] = ${this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2])}`;
2337
+ comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
2262
2338
  break;
2263
2339
  case this.OP.STORE_GLOBAL:
2264
2340
  // resolvedOperands: [constIdx, concealKey, srcReg]
2265
- comment += ` ${this._decryptConst(constants, resolvedOperands[0], resolvedOperands[1])} = reg[${resolvedOperands[2]}]`;
2341
+ comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
2266
2342
  break;
2267
2343
  case this.OP.LOAD_UPVALUE:
2268
- comment += ` reg[${dst}] = upvalue[${resolvedOperands[1]}]`;
2344
+ comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
2269
2345
  break;
2270
2346
  case this.OP.STORE_UPVALUE:
2271
- comment += ` upvalue[${resolvedOperands[0]}] = reg[${resolvedOperands[1]}]`;
2347
+ comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
2272
2348
  break;
2273
2349
  case this.OP.MOVE:
2274
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}]`;
2350
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
2275
2351
  break;
2276
2352
  case this.OP.MAKE_CLOSURE:
2277
- comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
2353
+ comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
2278
2354
  break;
2279
2355
  case this.OP.CALL:
2280
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
2356
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands
2357
+ .slice(3)
2358
+ .map((v) => `reg[${v}]`)
2359
+ .join(", ")})`;
2281
2360
  break;
2282
2361
  case this.OP.CALL_METHOD:
2283
- comment += ` reg[${dst}] = reg[${resolvedOperands[2]}](recv=reg[${resolvedOperands[1]}], ${resolvedOperands[3]} args)`;
2362
+ comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
2284
2363
  break;
2285
2364
  case this.OP.NEW:
2286
- comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
2365
+ comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
2287
2366
  break;
2288
2367
  case this.OP.RETURN:
2289
- comment += ` reg[${resolvedOperands[0]}]`;
2368
+ comment += ` reg[${displayOperands[0]}]`;
2290
2369
  break;
2291
2370
  case this.OP.BUILD_ARRAY:
2292
- comment += ` reg[${dst}] = [${resolvedOperands[2]} elems]`;
2371
+ comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
2293
2372
  break;
2294
2373
  case this.OP.BUILD_OBJECT:
2295
- comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
2374
+ comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
2296
2375
  break;
2297
2376
  case this.OP.GET_PROP:
2298
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}][reg[${resolvedOperands[2]}]]`;
2377
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
2299
2378
  break;
2300
2379
  case this.OP.SET_PROP:
2301
- comment += ` reg[${resolvedOperands[0]}][reg[${resolvedOperands[1]}]] = reg[${resolvedOperands[2]}]`;
2380
+ comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
2381
+ break;
2382
+
2383
+ case this.OP.JUMP_REG:
2384
+ comment += ` PC = reg[${displayOperands[0]}]`;
2302
2385
  break;
2303
2386
 
2304
2387
  default:
2305
2388
  comment +=
2306
- resolvedOperands.length === 1
2307
- ? ` ${resolvedOperands[0]}`
2308
- : ` [${resolvedOperands.join(", ")}]`;
2389
+ displayOperands.length === 1
2390
+ ? ` ${displayOperands[0]}`
2391
+ : ` [${displayOperands.join(", ")}]`;
2309
2392
  }
2310
2393
  }
2311
2394
 
@@ -2315,7 +2398,7 @@ class Serializer {
2315
2398
  const instrText = `[${values.join(", ")}]`;
2316
2399
  const text = `${(instrText + ",").padEnd(20)} ${comment}`;
2317
2400
 
2318
- return { text, values };
2401
+ return text;
2319
2402
  }
2320
2403
 
2321
2404
  _serializeConstants(constants: any[]) {
@@ -2333,20 +2416,29 @@ class Serializer {
2333
2416
  ): { bytecode: b.Bytecode } {
2334
2417
  const serialized = [];
2335
2418
  for (const instr of bytecode) {
2336
- if (instr[0] === null) continue;
2419
+ const op = instr[0];
2420
+ const operands = instr.slice(1);
2421
+
2422
+ if (instr[0] === null) continue; // null opcodes are not emitted
2423
+
2424
+ const resolvedValues = operands.map(
2425
+ (o) => (o as any)?.resolvedValue ?? o,
2426
+ );
2337
2427
 
2338
2428
  const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
2339
2429
  if (specializedOpInfo) {
2340
- const operands = instr.slice(1);
2341
-
2342
- const resolvedValues = operands.map(
2343
- (o) => (o as any)?.resolvedValue ?? o,
2344
- );
2345
2430
  const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
2346
2431
  compiler.OP_NAME[instr[0]] =
2347
2432
  `${originalName}_${resolvedValues.join("_")}`;
2348
2433
  }
2349
2434
 
2435
+ // Validate no opcode or operand exceeds u16 limit
2436
+ for (const o of resolvedValues) {
2437
+ ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
2438
+ ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
2439
+ }
2440
+ ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
2441
+
2350
2442
  serialized.push(instr);
2351
2443
  }
2352
2444
  return { bytecode: serialized };
@@ -2409,6 +2501,14 @@ export async function compileAndSerialize(
2409
2501
  const compiler = new Compiler(options);
2410
2502
  let bytecode = compiler.compile(sourceCode);
2411
2503
 
2504
+ // jumpDispatcher must run before resolveRegisters so that the new rDisp/rKey
2505
+ // RegisterOperand objects it injects are visible to the liveness analysis.
2506
+ // It must also run before resolveLabels since it emits encodedLabel IR operands.
2507
+ if (options.dispatcher) {
2508
+ const dispatcherResult = dispatcher(bytecode, compiler);
2509
+ bytecode = dispatcherResult.bytecode;
2510
+ }
2511
+
2412
2512
  const passes = [];
2413
2513
 
2414
2514
  passes.push(concealConstants);
@@ -2425,10 +2525,6 @@ export async function compileAndSerialize(
2425
2525
  passes.push(macroOpcodes);
2426
2526
  }
2427
2527
 
2428
- if (options.selfModifying) {
2429
- passes.push(selfModifying);
2430
- }
2431
-
2432
2528
  if (options.aliasedOpcodes) {
2433
2529
  passes.push(aliasedOpcodes);
2434
2530
  }
@@ -2438,6 +2534,21 @@ export async function compileAndSerialize(
2438
2534
  bytecode = passResult.bytecode;
2439
2535
  }
2440
2536
 
2537
+ // Resolve virtual registers to concrete slot indices and set regCount per fn.
2538
+ // Must run BEFORE selfModifying: that pass moves body instructions to the end
2539
+ // of the bytecode while leaving RETURN in place, splitting a function's code
2540
+ // into two non-contiguous regions. Linear-scan liveness then sees incorrect
2541
+ // firstUse/lastUse for registers that span the gap, causing slot collisions.
2542
+ const regsResult = resolveRegisters(bytecode, compiler);
2543
+ bytecode = regsResult.bytecode;
2544
+
2545
+ // selfModifying runs after register resolution so concrete slot indices are
2546
+ // already in place; only label operands remain unresolved at this stage.
2547
+ if (options.selfModifying) {
2548
+ const smResult = selfModifying(bytecode, compiler);
2549
+ bytecode = smResult.bytecode;
2550
+ }
2551
+
2441
2552
  // Resolve label references to flat bytecode indices.
2442
2553
  const labelsResult = resolveLabels(bytecode, compiler);
2443
2554
  bytecode = labelsResult.bytecode;
@@ -2463,8 +2574,8 @@ export async function compileAndSerialize(
2463
2574
  const generateBytecodeComment = () => {
2464
2575
  var lines = [];
2465
2576
  for (const instr of bytecode) {
2466
- const serialized = compiler.serializer._serializeInstr(instr);
2467
- lines.push("// " + serialized.text);
2577
+ const comment = compiler.serializer._generateComment(instr);
2578
+ lines.push("// " + comment);
2468
2579
  }
2469
2580
 
2470
2581
  return lines.join("\n");