js-confuser-vm 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +75 -94
  3. package/bench.ts +146 -0
  4. package/disassemble.ts +12 -0
  5. package/dist/build-runtime.js +41 -15
  6. package/dist/compiler.js +134 -60
  7. package/dist/disassembler.js +317 -0
  8. package/dist/index.js +7 -2
  9. package/dist/runtime.js +68 -46
  10. package/dist/template.js +116 -0
  11. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  12. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  13. package/dist/transforms/bytecode/dispatcher.js +13 -109
  14. package/dist/transforms/bytecode/macroOpcodes.js +2 -2
  15. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  16. package/dist/transforms/bytecode/resolveRegisters.js +4 -0
  17. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  18. package/dist/transforms/bytecode/specializedOpcodes.js +18 -10
  19. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  20. package/dist/transforms/runtime/classObfuscation.js +43 -0
  21. package/dist/transforms/runtime/handlerTable.js +91 -0
  22. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  23. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  24. package/dist/types.js +1 -1
  25. package/dist/utils/ast-utils.js +14 -0
  26. package/dist/utils/op-utils.js +0 -2
  27. package/dist/utils/pass-utils.js +100 -0
  28. package/dist/utils/profile-utils.js +3 -0
  29. package/index.ts +22 -17
  30. package/jest.config.js +14 -2
  31. package/output.disassembled.js +41 -0
  32. package/package.json +2 -1
  33. package/src/build-runtime.ts +113 -78
  34. package/src/compiler.ts +2703 -2593
  35. package/src/disassembler.ts +329 -0
  36. package/src/index.ts +12 -2
  37. package/src/options.ts +7 -1
  38. package/src/runtime.ts +84 -51
  39. package/src/template.ts +125 -1
  40. package/src/transforms/bytecode/aliasedOpcodes.ts +4 -1
  41. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  42. package/src/transforms/bytecode/dispatcher.ts +19 -125
  43. package/src/transforms/bytecode/macroOpcodes.ts +2 -2
  44. package/src/transforms/bytecode/resolveRegisters.ts +5 -0
  45. package/src/transforms/bytecode/specializedOpcodes.ts +22 -11
  46. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  47. package/src/transforms/runtime/classObfuscation.ts +59 -0
  48. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  49. package/src/types.ts +42 -1
  50. package/src/utils/ast-utils.ts +19 -0
  51. package/src/utils/op-utils.ts +0 -2
  52. package/src/utils/pass-utils.ts +126 -0
  53. package/src/utils/profile-utils.ts +3 -0
  54. package/tsconfig.json +1 -1
  55. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  56. package/src/transforms/runtime/internalVariables.ts +0 -270
  57. package/src/transforms/runtime/microOpcodes.ts +0 -93
  58. /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
package/src/runtime.ts CHANGED
@@ -8,18 +8,27 @@ const TIMING_CHECKS = false;
8
8
  // The text above is not included in the compiled output - for type intellisense only
9
9
  // @START
10
10
 
11
+ function base64ToBytes(s) {
12
+ return typeof Buffer !== "undefined"
13
+ ? Buffer.from(s, "base64")
14
+ : Uint8Array.from(atob(s), function (c) {
15
+ return c.charCodeAt(0);
16
+ });
17
+ }
18
+
11
19
  function decodeBytecode(s) {
12
20
  if (!ENCODE_BYTECODE) return s;
13
21
 
14
- var b =
15
- typeof Buffer !== "undefined"
16
- ? Buffer.from(s, "base64")
17
- : Uint8Array.from(atob(s), function (c) {
18
- return c.charCodeAt(0);
19
- });
20
- // Each slot is a u16 stored as 2 little-endian bytes.
21
- var r = new Uint16Array(b.length / 2);
22
- for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);
22
+ var b = base64ToBytes(s);
23
+ // Each slot is a u32 stored as 4 little-endian bytes.
24
+ var r = new Uint32Array(b.length / 4);
25
+ for (var i = 0; i < r.length; i++)
26
+ r[i] =
27
+ (b[i * 4] |
28
+ (b[i * 4 + 1] << 8) |
29
+ (b[i * 4 + 2] << 16) |
30
+ (b[i * 4 + 3] << 24)) >>>
31
+ 0;
23
32
  return r;
24
33
  }
25
34
 
@@ -100,7 +109,6 @@ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
100
109
  0,
101
110
  0,
102
111
  );
103
- this._internals = {};
104
112
  }
105
113
 
106
114
  // Consume the next slot from the flat bytecode stream and advance the PC.
@@ -138,12 +146,7 @@ VM.prototype._constant = function (idxIn, keyIn) {
138
146
  if (!key) return v;
139
147
  if (typeof v === "number") return v ^ key;
140
148
  // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).
141
- var b =
142
- typeof Buffer !== "undefined"
143
- ? Buffer.from(v, "base64")
144
- : Uint8Array.from(atob(v), function (c) {
145
- return c.charCodeAt(0);
146
- });
149
+ var b = base64ToBytes(v);
147
150
  var out = "";
148
151
  for (var i = 0; i < b.length / 2; i++) {
149
152
  var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE
@@ -212,6 +215,7 @@ VM.prototype.run = function () {
212
215
  try {
213
216
  var regs = this._regs;
214
217
  var base = frame._base;
218
+
215
219
  /* @SWITCH */
216
220
  switch (op) {
217
221
  case OP.LOAD_CONST: {
@@ -257,8 +261,7 @@ VM.prototype.run = function () {
257
261
  }
258
262
 
259
263
  case OP.STORE_GLOBAL: {
260
- // nameIdx and key are consumed inline so the concealConstants runtime
261
- // transform can rewrite this._constant() consistently.
264
+ // globals[globalName] = regs[src]
262
265
  this.globals[this._constant()] = regs[base + this._operand()];
263
266
  break;
264
267
  }
@@ -283,13 +286,14 @@ VM.prototype.run = function () {
283
286
  var obj = regs[base + this._operand()];
284
287
  var key = regs[base + this._operand()];
285
288
  var val = regs[base + this._operand()];
286
- // Reflect.set performs [[Set]] without throwing on failure,
287
- // correctly simulating sloppy-mode assignment from a strict-mode host.
289
+ // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)
288
290
  Reflect.set(obj, key, val);
289
291
  break;
290
292
  }
291
293
 
292
294
  case OP.DELETE_PROP: {
295
+ // regs[dst] = delete regs[obj][regs[key]]
296
+ // The delete operator returns true if successful which is most cases
293
297
  var dst = this._operand();
294
298
  var obj = regs[base + this._operand()];
295
299
  var key = regs[base + this._operand()];
@@ -297,7 +301,7 @@ VM.prototype.run = function () {
297
301
  break;
298
302
  }
299
303
 
300
- // ── Arithmetic (dst, src1, src2) ────────────────────────────────────
304
+ // Arithmetic (dst, src1, src2)
301
305
  case OP.ADD: {
302
306
  var dst = this._operand();
303
307
  var a = regs[base + this._operand()];
@@ -365,7 +369,7 @@ VM.prototype.run = function () {
365
369
  break;
366
370
  }
367
371
 
368
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────
372
+ // Comparison (dst, src1, src2)
369
373
  case OP.LT: {
370
374
  var dst = this._operand();
371
375
  var a = regs[base + this._operand()];
@@ -421,13 +425,14 @@ VM.prototype.run = function () {
421
425
  break;
422
426
  }
423
427
  case OP.INSTANCEOF: {
428
+ // regs[dst] = regs[obj] instanceof regs[ctor]
424
429
  var dst = this._operand();
425
430
  var obj = regs[base + this._operand()];
426
431
  var ctor = regs[base + this._operand()];
427
432
  if (typeof ctor === "function") {
428
433
  regs[base + dst] = obj instanceof ctor;
429
434
  } else {
430
- // VM Closure - walk prototype chain for identity with ctor.prototype.
435
+ // TODO: Why is this needed?
431
436
  var proto = ctor.prototype;
432
437
  var target = Object.getPrototypeOf(obj);
433
438
  var result = false;
@@ -443,7 +448,7 @@ VM.prototype.run = function () {
443
448
  break;
444
449
  }
445
450
 
446
- // ── Unary (dst, src) ─────────────────────────────────────────────────
451
+ // Unary (dst, src)
447
452
  case OP.UNARY_NEG: {
448
453
  var dst = this._operand();
449
454
  regs[base + dst] = -regs[base + this._operand()];
@@ -471,12 +476,13 @@ VM.prototype.run = function () {
471
476
  }
472
477
  case OP.VOID: {
473
478
  var dst = this._operand();
474
- this._operand(); // consume src — evaluated for side-effects by compiler
479
+ this._operand(); // consumes argument (intended)
475
480
  regs[base + dst] = undefined;
476
481
  break;
477
482
  }
478
483
  case OP.TYPEOF_SAFE: {
479
- // dst, nameConstIdx — safe typeof for potentially-undeclared globals.
484
+ // regs[dst] = typeof window[name]
485
+ // Never throws ReferenceError, instead returns undefined for undeclared variables
480
486
  var dst = this._operand();
481
487
  var name = this._constant();
482
488
  var val = Object.prototype.hasOwnProperty.call(this.globals, name)
@@ -486,7 +492,7 @@ VM.prototype.run = function () {
486
492
  break;
487
493
  }
488
494
 
489
- // ── Control flow ──────────────────────────────────────────────────────
495
+ // Control flow
490
496
  case OP.JUMP:
491
497
  frame._pc = this._operand();
492
498
  break;
@@ -506,7 +512,7 @@ VM.prototype.run = function () {
506
512
  break;
507
513
  }
508
514
 
509
- // ── Calls ─────────────────────────────────────────────────────────────
515
+ // Calls
510
516
  case OP.CALL: {
511
517
  // dst, calleeReg, argc, [argReg...]
512
518
  var dst = this._operand();
@@ -528,8 +534,14 @@ VM.prototype.run = function () {
528
534
  dst,
529
535
  newBase,
530
536
  );
531
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
532
- this._regs[newBase + i] = args[i];
537
+ if (closure.fn.hasRest) {
538
+ var restSlot = closure.fn.paramCount - 1;
539
+ for (var i = 0; i < restSlot; i++)
540
+ this._regs[newBase + i] = i < args.length ? args[i] : undefined;
541
+ this._regs[newBase + restSlot] = args.slice(restSlot);
542
+ } else {
543
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++)
544
+ this._regs[newBase + i] = args[i];
533
545
  }
534
546
  if (closure.fn.paramCount < closure.fn.regCount) {
535
547
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -564,8 +576,14 @@ VM.prototype.run = function () {
564
576
  dst,
565
577
  newBase,
566
578
  );
567
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
568
- this._regs[newBase + i] = args[i];
579
+ if (closure.fn.hasRest) {
580
+ var restSlot = closure.fn.paramCount - 1;
581
+ for (var i = 0; i < restSlot; i++)
582
+ this._regs[newBase + i] = i < args.length ? args[i] : undefined;
583
+ this._regs[newBase + restSlot] = args.slice(restSlot);
584
+ } else {
585
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++)
586
+ this._regs[newBase + i] = args[i];
569
587
  }
570
588
  if (closure.fn.paramCount < closure.fn.regCount) {
571
589
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -593,8 +611,14 @@ VM.prototype.run = function () {
593
611
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
594
612
  this._regsTop = newBase + closure.fn.regCount;
595
613
  var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);
596
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
597
- this._regs[newBase + i] = args[i];
614
+ if (closure.fn.hasRest) {
615
+ var restSlot = closure.fn.paramCount - 1;
616
+ for (var i = 0; i < restSlot; i++)
617
+ this._regs[newBase + i] = i < args.length ? args[i] : undefined;
618
+ this._regs[newBase + restSlot] = args.slice(restSlot);
619
+ } else {
620
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++)
621
+ this._regs[newBase + i] = args[i];
598
622
  }
599
623
  if (closure.fn.paramCount < closure.fn.regCount) {
600
624
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -613,11 +637,16 @@ VM.prototype.run = function () {
613
637
  case OP.RETURN: {
614
638
  var retVal = regs[base + this._operand()];
615
639
  this._closeUpvaluesFor(frame); // must happen before frame is abandoned
640
+
641
+ // Zero out callee's register window to limit exposing runtime values
642
+ var hi = frame._base + frame.closure.fn.regCount;
643
+ for (var i = frame._base as number; i < hi; i++)
644
+ this._regs[i] = undefined;
616
645
  this._regsTop = frame._base;
617
646
 
618
647
  if (this._frameStack.length === 0) return retVal; // main script returning
619
648
 
620
- // new-call rule: primitive return -> discard, use the constructed object instead
649
+ // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)
621
650
  if (frame._newObj !== null) {
622
651
  if (typeof retVal !== "object" || retVal === null)
623
652
  retVal = frame._newObj;
@@ -632,14 +661,15 @@ VM.prototype.run = function () {
632
661
  case OP.THROW:
633
662
  throw regs[base + this._operand()];
634
663
 
635
- // ── Closures ──────────────────────────────────────────────────────────
664
+ // Closures
636
665
  case OP.MAKE_CLOSURE: {
637
- // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]
666
+ // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]
638
667
  var dst = this._operand();
639
668
  var startPc = this._operand();
640
669
  var paramCount = this._operand();
641
670
  var regCount = this._operand();
642
671
  var uvCount = this._operand();
672
+ var hasRest = this._operand(); // 1 if last param is a rest element
643
673
 
644
674
  var uvDescs = new Array(uvCount);
645
675
  for (var i = 0; i < uvCount; i++) {
@@ -653,6 +683,7 @@ VM.prototype.run = function () {
653
683
  regCount: regCount,
654
684
  startPc: startPc,
655
685
  upvalueDescriptors: uvDescs,
686
+ hasRest: hasRest,
656
687
  };
657
688
 
658
689
  var closure = new Closure(fn);
@@ -688,8 +719,14 @@ VM.prototype.run = function () {
688
719
  0,
689
720
  );
690
721
  sub._currentFrame = f;
691
- for (var i = 0; i < args.length && i < c.fn.regCount; i++) {
692
- sub._regs[i] = args[i];
722
+ if (c.fn.hasRest) {
723
+ var restSlot = c.fn.paramCount - 1;
724
+ for (var i = 0; i < restSlot; i++)
725
+ sub._regs[i] = i < args.length ? args[i] : undefined;
726
+ sub._regs[restSlot] = args.slice(restSlot);
727
+ } else {
728
+ for (var i = 0; i < args.length && i < c.fn.regCount; i++)
729
+ sub._regs[i] = args[i];
693
730
  }
694
731
  if (c.fn.paramCount < c.fn.regCount) {
695
732
  sub._regs[c.fn.paramCount] = args;
@@ -703,7 +740,7 @@ VM.prototype.run = function () {
703
740
  break;
704
741
  }
705
742
 
706
- // ── Collections ───────────────────────────────────────────────────────
743
+ // Collections
707
744
  case OP.BUILD_ARRAY: {
708
745
  // dst, count, [elemReg...]
709
746
  var dst = this._operand();
@@ -729,7 +766,7 @@ VM.prototype.run = function () {
729
766
  break;
730
767
  }
731
768
 
732
- // ── Property definitions (getters / setters) ──────────────────────────
769
+ // Object methods (getters / setters)
733
770
  case OP.DEFINE_GETTER: {
734
771
  // obj, key, fn
735
772
  var obj = regs[base + this._operand()];
@@ -825,7 +862,7 @@ VM.prototype.run = function () {
825
862
  break;
826
863
  }
827
864
 
828
- // ── Self-modifying bytecode ───────────────────────────────────────────
865
+ // Self-modifying bytecode
829
866
  case OP.PATCH: {
830
867
  // destPc, sliceStart, sliceEnd
831
868
  var destPc = this._operand();
@@ -838,10 +875,7 @@ VM.prototype.run = function () {
838
875
  }
839
876
 
840
877
  case OP.JUMP_REG: {
841
- // Indirect jump: target PC is read from a register rather than a
842
- // bytecode immediate. Used by the jumpDispatcher pass so that static
843
- // analysis cannot determine the jump destination without tracking the
844
- // register value (which contains an encoded PC resolved at runtime).
878
+ // Indirect jump: allows VM to jump based on runtime values.
845
879
  frame._pc = regs[base + this._operand()];
846
880
  break;
847
881
  }
@@ -857,9 +891,8 @@ VM.prototype.run = function () {
857
891
  );
858
892
  }
859
893
  } catch (err) {
860
- // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).
861
- // Walk from the current frame upward until we find a frame that has an open
862
- // exception handler (TRY_SETUP without a matching TRY_END).
894
+ // Exception handler unwinding
895
+ // Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).
863
896
  // For every frame we abandon along the way, close its captured upvalues.
864
897
  var handledFrame = null;
865
898
  var searchFrame = this._currentFrame;
@@ -876,7 +909,7 @@ VM.prototype.run = function () {
876
909
  this._currentFrame = searchFrame;
877
910
  }
878
911
 
879
- if (!handledFrame) throw err; // no handler anywhere propagate to host
912
+ if (!handledFrame) throw err; // if there's no handler, propagate back to host
880
913
 
881
914
  var h = handledFrame._handlerStack.pop();
882
915
  // Discard any call-frames that were pushed inside the try body.
@@ -891,7 +924,7 @@ VM.prototype.run = function () {
891
924
  }
892
925
  };
893
926
 
894
- // Boot
927
+ /* @BOOT */ // <- This comment can't be removed!
895
928
  var globals: any = {}; // global object for globals
896
929
 
897
930
  // Always pull built-ins from globalThis so eval() scoping can't shadow them
package/src/template.ts CHANGED
@@ -47,7 +47,7 @@
47
47
 
48
48
  import { Compiler } from "./compiler.ts";
49
49
  import { DEFAULT_OPTIONS } from "./options.ts";
50
- import type { Bytecode, Instruction } from "./types.ts";
50
+ import type { Bytecode, Instruction, RegisterOperand } from "./types.ts";
51
51
 
52
52
  export class Template {
53
53
  private readonly _source: string;
@@ -138,4 +138,128 @@ export class Template {
138
138
 
139
139
  return { functions: innerDescs, bytecode: innerBytecode };
140
140
  }
141
+
142
+ // ── Inline compilation ───────────────────────────────────────────────────
143
+ /**
144
+ * Compile the template and return the **main scope** bytecode, with all
145
+ * register operands remapped to belong to `targetFnId`. This allows
146
+ * bytecode transforms to express high-level JS control flow (while-loops,
147
+ * if-chains, variable declarations) via Template and splice the result
148
+ * directly into an existing function's instruction stream.
149
+ *
150
+ * The implicit trailing RETURN added by _compileFunctionDecl is stripped —
151
+ * inline code should flow into the surrounding bytecode, not return.
152
+ *
153
+ * @param variables Substitution map for {name} placeholders.
154
+ * @param parentCompiler The Compiler whose OP table, label counter, and
155
+ * fnDescriptors are shared.
156
+ * @param targetFnId The function whose register file the template's
157
+ * registers should be remapped into.
158
+ * @param maxId Live map of max register id per fnId — updated
159
+ * in-place as new registers are allocated.
160
+ *
161
+ * @returns
162
+ * bytecode — main-scope IR (no entry defineLabel, no trailing RETURN),
163
+ * ready to splice into the target function's instruction stream.
164
+ * registers — mapping of JS variable names → remapped RegisterOperands,
165
+ * so the caller can reference template-declared variables
166
+ * (e.g. the `state` variable in CFF).
167
+ * functions — inner function descriptors (same as compile()).
168
+ * innerBytecode — inner function bytecode blocks (same as compile()).
169
+ */
170
+ compileInline(
171
+ variables: Record<string, string | number>,
172
+ parentCompiler: Compiler,
173
+ targetFnId: number,
174
+ maxId: Map<number, number>,
175
+ ): {
176
+ bytecode: Bytecode;
177
+ registers: Map<string, RegisterOperand>;
178
+ functions: any[];
179
+ innerBytecode: Bytecode;
180
+ } {
181
+ const code = this._interpolate(variables);
182
+
183
+ const child = new Compiler({ ...DEFAULT_OPTIONS, randomizeOpcodes: false });
184
+ child.OP = { ...parentCompiler.OP };
185
+ child.OP_NAME = { ...parentCompiler.OP_NAME };
186
+ child.JUMP_OPS = new Set(parentCompiler.JUMP_OPS);
187
+ child._makeLabel = parentCompiler._makeLabel.bind(parentCompiler);
188
+
189
+ const startIdx = parentCompiler.fnDescriptors.length;
190
+ child.fnDescriptors = parentCompiler.fnDescriptors;
191
+
192
+ child.compile(code);
193
+
194
+ const mainDesc = parentCompiler.fnDescriptors[startIdx] as any;
195
+ const mainFnId: number = mainDesc._fnIdx;
196
+ const mainBc = mainDesc.bytecode as Bytecode;
197
+
198
+ // ── Remap registers from the template's main fnId → targetFnId ────────
199
+ // Build a mapping: old register id → new RegisterOperand in targetFnId.
200
+ const regRemap = new Map<number, RegisterOperand>();
201
+ const remapReg = (id: number): RegisterOperand => {
202
+ if (!regRemap.has(id)) {
203
+ const next = (maxId.get(targetFnId) ?? -1) + 1;
204
+ maxId.set(targetFnId, next);
205
+ regRemap.set(id, { type: "register", id: next, fnId: targetFnId });
206
+ }
207
+ return regRemap.get(id)!;
208
+ };
209
+
210
+ for (const instr of mainBc) {
211
+ for (let j = 1; j < instr.length; j++) {
212
+ const op = instr[j] as any;
213
+ if (op && typeof op === "object" && op.type === "register" && op.fnId === mainFnId) {
214
+ const mapped = remapReg(op.id);
215
+ op.id = mapped.id;
216
+ op.fnId = mapped.fnId;
217
+ }
218
+ }
219
+ }
220
+
221
+ // ── Build variable name → remapped register mapping ───────────────────
222
+ const registers = new Map<string, RegisterOperand>();
223
+ const locals: Map<string, RegisterOperand> = mainDesc.ctx.scope._locals;
224
+ for (const [name, reg] of locals) {
225
+ const mapped = regRemap.get(reg.id);
226
+ if (mapped) registers.set(name, mapped);
227
+ }
228
+
229
+ // ── Strip entry defineLabel and trailing implicit RETURN ───────────────
230
+ let bytecode = mainBc.filter((instr) => {
231
+ const op0 = instr[1] as any;
232
+ return !(
233
+ instr[0] === null &&
234
+ op0?.type === "defineLabel" &&
235
+ op0.label === mainDesc.entryLabel
236
+ );
237
+ });
238
+
239
+ // Remove trailing LOAD_CONST undefined + RETURN (implicit return added
240
+ // by _compileFunctionDecl).
241
+ const OP = parentCompiler.OP;
242
+ if (
243
+ bytecode.length >= 2 &&
244
+ bytecode[bytecode.length - 1][0] === OP.RETURN &&
245
+ bytecode[bytecode.length - 2][0] === OP.LOAD_CONST
246
+ ) {
247
+ bytecode = bytecode.slice(0, -2);
248
+ }
249
+
250
+ // ── Inner function bytecode (same as compile()) ───────────────────────
251
+ const innerDescs = parentCompiler.fnDescriptors.slice(startIdx + 1);
252
+ const innerBytecode: Bytecode = [];
253
+ for (const desc of innerDescs) {
254
+ innerBytecode.push([
255
+ null,
256
+ { type: "defineLabel", label: desc.entryLabel },
257
+ ] as Instruction);
258
+ for (const instr of (desc as any).bytecode as Bytecode) {
259
+ innerBytecode.push(instr);
260
+ }
261
+ }
262
+
263
+ return { bytecode, registers, functions: innerDescs, innerBytecode };
264
+ }
141
265
  }
@@ -1,5 +1,5 @@
1
1
  import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
2
- import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
2
+ import { Compiler, OP_ORIGINAL, SOURCE_NODE_SYM } from "../../compiler.ts";
3
3
  import { nextFreeSlot } from "../../utils/op-utils.ts";
4
4
  import { shuffle } from "../../utils/random-utils.ts";
5
5
 
@@ -58,6 +58,9 @@ export function aliasedOpcodes(
58
58
  const arity = instr.length - 1;
59
59
  if (arity < 1) continue; // 0-operand opcodes have nothing to permute
60
60
 
61
+ const opName = compiler.OP_NAME[op];
62
+ if (!OP_ORIGINAL[opName]) continue; // only consider original ops, not already-specialized ones
63
+
61
64
  const existing = opStats.get(op);
62
65
  if (!existing) {
63
66
  opStats.set(op, { freq: 1, arity });