js-confuser-vm 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +281 -147
  2. package/dist/build-runtime.js +41 -15
  3. package/dist/compiler.js +714 -265
  4. package/dist/disassembler.js +367 -0
  5. package/dist/index.js +7 -2
  6. package/dist/runtime.js +160 -119
  7. package/dist/template.js +163 -42
  8. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  9. package/dist/transforms/bytecode/concealConstants.js +2 -2
  10. package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
  11. package/dist/transforms/bytecode/dispatcher.js +15 -111
  12. package/dist/transforms/bytecode/macroOpcodes.js +2 -2
  13. package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
  14. package/dist/transforms/bytecode/resolveRegisters.js +23 -4
  15. package/dist/transforms/bytecode/selfModifying.js +88 -21
  16. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  17. package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
  18. package/dist/transforms/bytecode/stringConcealing.js +288 -0
  19. package/dist/transforms/runtime/classObfuscation.js +43 -0
  20. package/dist/transforms/runtime/handlerTable.js +91 -0
  21. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  22. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  23. package/dist/types.js +1 -1
  24. package/dist/utils/ast-utils.js +75 -0
  25. package/dist/utils/op-utils.js +1 -2
  26. package/dist/utils/pass-utils.js +100 -0
  27. package/dist/utils/profile-utils.js +3 -0
  28. package/package.json +8 -1
  29. package/.gitmodules +0 -4
  30. package/.prettierignore +0 -1
  31. package/CHANGELOG.md +0 -335
  32. package/babel-plugin-inline-runtime.cjs +0 -34
  33. package/babel.config.json +0 -23
  34. package/index.ts +0 -38
  35. package/jest-strip-types.js +0 -10
  36. package/jest.config.js +0 -52
  37. package/src/build-runtime.ts +0 -78
  38. package/src/compiler.ts +0 -2593
  39. package/src/index.ts +0 -14
  40. package/src/minify.ts +0 -21
  41. package/src/options.ts +0 -18
  42. package/src/runtime.ts +0 -923
  43. package/src/template.ts +0 -141
  44. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
  45. package/src/transforms/bytecode/concealConstants.ts +0 -52
  46. package/src/transforms/bytecode/dispatcher.ts +0 -398
  47. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  48. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  49. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  50. package/src/transforms/bytecode/resolveRegisters.ts +0 -221
  51. package/src/transforms/bytecode/selfModifying.ts +0 -121
  52. package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
  53. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  54. package/src/transforms/runtime/internalVariables.ts +0 -270
  55. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  56. package/src/transforms/runtime/microOpcodes.ts +0 -93
  57. package/src/transforms/runtime/minify.ts +0 -1
  58. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  59. package/src/transforms/runtime/specializedOpcodes.ts +0 -156
  60. package/src/types.ts +0 -93
  61. package/src/utils/op-utils.ts +0 -48
  62. package/src/utils/random-utils.ts +0 -31
  63. package/tsconfig.json +0 -12
package/dist/runtime.js CHANGED
@@ -5,29 +5,34 @@ const MAIN_REG_COUNT = 0;
5
5
  const CONSTANTS = [];
6
6
  const ENCODE_BYTECODE = false;
7
7
  const TIMING_CHECKS = false;
8
+ const SENTINELS = {
9
+ CALL_SPREAD: 0
10
+ };
8
11
  // The text above is not included in the compiled output - for type intellisense only
9
12
  // @START
10
13
 
11
- function decodeBytecode(s) {
12
- if (!ENCODE_BYTECODE) return s;
13
- var b = typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) {
14
+ function base64ToBytes(s) {
15
+ return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) {
14
16
  return c.charCodeAt(0);
15
17
  });
16
- // Each slot is a u16 stored as 2 little-endian bytes.
17
- var r = new Uint16Array(b.length / 2);
18
- for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | b[i * 2 + 1] << 8;
18
+ }
19
+ function decodeBytecode(s) {
20
+ if (!ENCODE_BYTECODE) return s;
21
+ var b = base64ToBytes(s);
22
+ // Each slot is a u32 stored as 4 little-endian bytes.
23
+ var r = new Uint32Array(b.length / 4);
24
+ for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0;
19
25
  return r;
20
26
  }
21
27
 
22
- // Closure symbol
23
- // Used to tag shell functions so the VM can fast-path back to the
24
- // inner Closure instead of going through a sub-VM on internal calls.
25
- var CLOSURE_SYM = Symbol(); // Nameless for obfuscation
28
+ // Closure map
29
+ // Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls.
30
+ // A WeakMap is used over a Symbol to prevent leaking information to debuggers
31
+ var CLOSURE_MAP = new WeakMap();
26
32
 
27
- // Upvalue Lua/CPython style.
33
+ // Upvalue (Lua style)
28
34
  // While the outer frame is alive: reads/writes go to vm._regs[_absSlot].
29
35
  // After the outer frame returns (closed): reads/writes hit this._value.
30
- // _absSlot is the absolute index in VM._regs (frame._base + local slot).
31
36
  function Upvalue(regs, absSlot) {
32
37
  this._regs = regs; // shared reference to VM._regs flat array
33
38
  this._absSlot = absSlot; // absolute index; stable as long as frame is alive
@@ -52,7 +57,7 @@ function Closure(fn) {
52
57
  this.prototype = {}; // <- default prototype object for `new`
53
58
  }
54
59
 
55
- // Frame — analogous to Lua CallInfo / CPython PyFrameObject.
60
+ // Frame (Lua CallInfo / CPython PyFrameObject)
56
61
  // Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).
57
62
  function Frame(closure, returnPc, parent, thisVal, retDstReg, base) {
58
63
  this.closure = closure;
@@ -74,8 +79,8 @@ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
74
79
  this._frameStack = [];
75
80
  this._openUpvalues = [];
76
81
 
77
- // ── Flat register file (Lua-style) ────────────────────────────────────────
78
- // All frames share a single array. Each Frame records its _base offset.
82
+ // Flat register array (Lua-style)
83
+ // Each Frame records its _base offset.
79
84
  // _regsTop is the next free slot (= base of the hypothetical next frame).
80
85
  // On CALL: newBase = _regsTop; _regsTop += fn.regCount
81
86
  // On RETURN: _regsTop = frame._base (pop the frame's register window)
@@ -88,7 +93,6 @@ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
88
93
  startPc: mainStartPc
89
94
  };
90
95
  this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0, 0);
91
- this._internals = {};
92
96
  }
93
97
 
94
98
  // Consume the next slot from the flat bytecode stream and advance the PC.
@@ -109,8 +113,8 @@ VM.prototype.captureUpvalue = function (frame, slot) {
109
113
  };
110
114
 
111
115
  // Reads and decodes a constant from the pool.
112
- // idx pool index (first operand of the constant pair emitted by resolveConstants).
113
- // key conceal key (second operand). 0 means no concealment.
116
+ // idx: pool index (first operand of the constant pair emitted by resolveConstants).
117
+ // key: conceal key (second operand). 0 means no concealment.
114
118
  //
115
119
  // For integers: stored value is (original ^ key); XOR again to recover.
116
120
  // For strings: stored value is a base64 string containing u16 LE byte pairs.
@@ -124,9 +128,7 @@ VM.prototype._constant = function (idxIn, keyIn) {
124
128
  if (!key) return v;
125
129
  if (typeof v === "number") return v ^ key;
126
130
  // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).
127
- var b = typeof Buffer !== "undefined" ? Buffer.from(v, "base64") : Uint8Array.from(atob(v), function (c) {
128
- return c.charCodeAt(0);
129
- });
131
+ var b = base64ToBytes(v);
130
132
  var out = "";
131
133
  for (var i = 0; i < b.length / 2; i++) {
132
134
  var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE
@@ -164,11 +166,7 @@ VM.prototype.run = function () {
164
166
  var pc = frame._pc++;
165
167
  var op = this.bytecode[pc];
166
168
  var opcode = this.bytecode[pc];
167
- // console.log(
168
- // "pc=" + pc,
169
- // "opcode=" + opcode,
170
- // Object.keys(OP).find((key) => OP[key] === opcode),
171
- // );
169
+ // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`);
172
170
 
173
171
  // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks
174
172
  if (TIMING_CHECKS) {
@@ -187,6 +185,7 @@ VM.prototype.run = function () {
187
185
  try {
188
186
  var regs = this._regs;
189
187
  var base = frame._base;
188
+
190
189
  /* @SWITCH */
191
190
  switch (op) {
192
191
  case OP.LOAD_CONST:
@@ -231,8 +230,7 @@ VM.prototype.run = function () {
231
230
  }
232
231
  case OP.STORE_GLOBAL:
233
232
  {
234
- // nameIdx and key are consumed inline so the concealConstants runtime
235
- // transform can rewrite this._constant() consistently.
233
+ // globals[globalName] = regs[src]
236
234
  this.globals[this._constant()] = regs[base + this._operand()];
237
235
  break;
238
236
  }
@@ -257,13 +255,14 @@ VM.prototype.run = function () {
257
255
  var obj = regs[base + this._operand()];
258
256
  var key = regs[base + this._operand()];
259
257
  var val = regs[base + this._operand()];
260
- // Reflect.set performs [[Set]] without throwing on failure,
261
- // correctly simulating sloppy-mode assignment from a strict-mode host.
258
+ // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)
262
259
  Reflect.set(obj, key, val);
263
260
  break;
264
261
  }
265
262
  case OP.DELETE_PROP:
266
263
  {
264
+ // regs[dst] = delete regs[obj][regs[key]]
265
+ // The delete operator returns true if successful which is most cases
267
266
  var dst = this._operand();
268
267
  var obj = regs[base + this._operand()];
269
268
  var key = regs[base + this._operand()];
@@ -271,7 +270,7 @@ VM.prototype.run = function () {
271
270
  break;
272
271
  }
273
272
 
274
- // ── Arithmetic (dst, src1, src2) ────────────────────────────────────
273
+ // Arithmetic (dst, src1, src2)
275
274
  case OP.ADD:
276
275
  {
277
276
  var dst = this._operand();
@@ -307,6 +306,14 @@ VM.prototype.run = function () {
307
306
  regs[base + dst] = a % regs[base + this._operand()];
308
307
  break;
309
308
  }
309
+ case OP.EXP:
310
+ {
311
+ // Math.pow instead of `**`
312
+ var dst = this._operand();
313
+ var a = regs[base + this._operand()];
314
+ regs[base + dst] = Math.pow(a, regs[base + this._operand()]);
315
+ break;
316
+ }
310
317
  case OP.BAND:
311
318
  {
312
319
  var dst = this._operand();
@@ -350,7 +357,7 @@ VM.prototype.run = function () {
350
357
  break;
351
358
  }
352
359
 
353
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────
360
+ // Comparison (dst, src1, src2)
354
361
  case OP.LT:
355
362
  {
356
363
  var dst = this._operand();
@@ -416,29 +423,15 @@ VM.prototype.run = function () {
416
423
  }
417
424
  case OP.INSTANCEOF:
418
425
  {
426
+ // regs[dst] = regs[obj] instanceof regs[ctor]
427
+ // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works
419
428
  var dst = this._operand();
420
429
  var obj = regs[base + this._operand()];
421
- var ctor = regs[base + this._operand()];
422
- if (typeof ctor === "function") {
423
- regs[base + dst] = obj instanceof ctor;
424
- } else {
425
- // VM Closure - walk prototype chain for identity with ctor.prototype.
426
- var proto = ctor.prototype;
427
- var target = Object.getPrototypeOf(obj);
428
- var result = false;
429
- while (target !== null) {
430
- if (target === proto) {
431
- result = true;
432
- break;
433
- }
434
- target = Object.getPrototypeOf(target);
435
- }
436
- regs[base + dst] = result;
437
- }
430
+ regs[base + dst] = obj instanceof regs[base + this._operand()];
438
431
  break;
439
432
  }
440
433
 
441
- // ── Unary (dst, src) ─────────────────────────────────────────────────
434
+ // Unary (dst, src)
442
435
  case OP.UNARY_NEG:
443
436
  {
444
437
  var dst = this._operand();
@@ -472,13 +465,14 @@ VM.prototype.run = function () {
472
465
  case OP.VOID:
473
466
  {
474
467
  var dst = this._operand();
475
- this._operand(); // consume src — evaluated for side-effects by compiler
468
+ this._operand(); // consumes argument (intended)
476
469
  regs[base + dst] = undefined;
477
470
  break;
478
471
  }
479
472
  case OP.TYPEOF_SAFE:
480
473
  {
481
- // dst, nameConstIdx — safe typeof for potentially-undeclared globals.
474
+ // regs[dst] = typeof window[name]
475
+ // Never throws ReferenceError, instead returns undefined for undeclared variables
482
476
  var dst = this._operand();
483
477
  var name = this._constant();
484
478
  var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined;
@@ -486,7 +480,7 @@ VM.prototype.run = function () {
486
480
  break;
487
481
  }
488
482
 
489
- // ── Control flow ──────────────────────────────────────────────────────
483
+ // Control flow
490
484
  case OP.JUMP:
491
485
  frame._pc = this._operand();
492
486
  break;
@@ -506,23 +500,32 @@ VM.prototype.run = function () {
506
500
  break;
507
501
  }
508
502
 
509
- // ── Calls ─────────────────────────────────────────────────────────────
503
+ // Calls
510
504
  case OP.CALL:
511
505
  {
512
- // dst, calleeReg, argc, [argReg...]
506
+ // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg)
513
507
  var dst = this._operand();
514
508
  var callee = regs[base + this._operand()];
515
509
  var argc = this._operand();
516
- var args = new Array(argc);
517
- for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
518
- if (callee && callee[CLOSURE_SYM]) {
519
- var closure = callee[CLOSURE_SYM];
510
+ var args;
511
+ if (argc === SENTINELS.CALL_SPREAD) {
512
+ args = regs[base + this._operand()];
513
+ } else {
514
+ args = new Array(argc);
515
+ for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
516
+ }
517
+ var closure = callee && CLOSURE_MAP.get(callee);
518
+ if (closure) {
520
519
  var newBase = this._regsTop;
521
520
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
522
521
  this._regsTop = newBase + closure.fn.regCount;
523
522
  var f = new Frame(closure, frame._pc, frame, this.globals, dst, newBase);
524
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
525
- this._regs[newBase + i] = args[i];
523
+ if (closure.fn.hasRest) {
524
+ var restSlot = closure.fn.paramCount - 1;
525
+ for (var i = 0; i < restSlot; i++) this._regs[newBase + i] = i < args.length ? args[i] : undefined;
526
+ this._regs[newBase + restSlot] = args.slice(restSlot);
527
+ } else {
528
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++) this._regs[newBase + i] = args[i];
526
529
  }
527
530
  if (closure.fn.paramCount < closure.fn.regCount) {
528
531
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -536,21 +539,30 @@ VM.prototype.run = function () {
536
539
  }
537
540
  case OP.CALL_METHOD:
538
541
  {
539
- // dst, receiverReg, calleeReg, argc, [argReg...]
542
+ // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg)
540
543
  var dst = this._operand();
541
544
  var receiver = regs[base + this._operand()];
542
545
  var callee = regs[base + this._operand()];
543
546
  var argc = this._operand();
544
- var args = new Array(argc);
545
- for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
546
- if (callee && callee[CLOSURE_SYM]) {
547
- var closure = callee[CLOSURE_SYM];
547
+ var args;
548
+ if (argc === SENTINELS.CALL_SPREAD) {
549
+ args = regs[base + this._operand()];
550
+ } else {
551
+ args = new Array(argc);
552
+ for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
553
+ }
554
+ var closure = callee && CLOSURE_MAP.get(callee);
555
+ if (closure) {
548
556
  var newBase = this._regsTop;
549
557
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
550
558
  this._regsTop = newBase + closure.fn.regCount;
551
559
  var f = new Frame(closure, frame._pc, frame, receiver, dst, newBase);
552
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
553
- this._regs[newBase + i] = args[i];
560
+ if (closure.fn.hasRest) {
561
+ var restSlot = closure.fn.paramCount - 1;
562
+ for (var i = 0; i < restSlot; i++) this._regs[newBase + i] = i < args.length ? args[i] : undefined;
563
+ this._regs[newBase + restSlot] = args.slice(restSlot);
564
+ } else {
565
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++) this._regs[newBase + i] = args[i];
554
566
  }
555
567
  if (closure.fn.paramCount < closure.fn.regCount) {
556
568
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -564,21 +576,30 @@ VM.prototype.run = function () {
564
576
  }
565
577
  case OP.NEW:
566
578
  {
567
- // dst, calleeReg, argc, [argReg...]
579
+ // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg)
568
580
  var dst = this._operand();
569
581
  var callee = regs[base + this._operand()];
570
582
  var argc = this._operand();
571
- var args = new Array(argc);
572
- for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
573
- if (callee && callee[CLOSURE_SYM]) {
574
- var closure = callee[CLOSURE_SYM];
583
+ var args;
584
+ if (argc === SENTINELS.CALL_SPREAD) {
585
+ args = regs[base + this._operand()];
586
+ } else {
587
+ args = new Array(argc);
588
+ for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
589
+ }
590
+ var closure = callee && CLOSURE_MAP.get(callee);
591
+ if (closure) {
575
592
  var newObj = Object.create(closure.prototype || null);
576
593
  var newBase = this._regsTop;
577
594
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
578
595
  this._regsTop = newBase + closure.fn.regCount;
579
596
  var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);
580
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
581
- this._regs[newBase + i] = args[i];
597
+ if (closure.fn.hasRest) {
598
+ var restSlot = closure.fn.paramCount - 1;
599
+ for (var i = 0; i < restSlot; i++) this._regs[newBase + i] = i < args.length ? args[i] : undefined;
600
+ this._regs[newBase + restSlot] = args.slice(restSlot);
601
+ } else {
602
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++) this._regs[newBase + i] = args[i];
582
603
  }
583
604
  if (closure.fn.paramCount < closure.fn.regCount) {
584
605
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -597,10 +618,14 @@ VM.prototype.run = function () {
597
618
  {
598
619
  var retVal = regs[base + this._operand()];
599
620
  this._closeUpvaluesFor(frame); // must happen before frame is abandoned
621
+
622
+ // Zero out callee's register window to limit exposing runtime values
623
+ var hi = frame._base + frame.closure.fn.regCount;
624
+ for (var i = frame._base; i < hi; i++) this._regs[i] = undefined;
600
625
  this._regsTop = frame._base;
601
626
  if (this._frameStack.length === 0) return retVal; // main script returning
602
627
 
603
- // new-call rule: primitive return -> discard, use the constructed object instead
628
+ // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)
604
629
  if (frame._newObj !== null) {
605
630
  if (typeof retVal !== "object" || retVal === null) retVal = frame._newObj;
606
631
  }
@@ -612,15 +637,17 @@ VM.prototype.run = function () {
612
637
  case OP.THROW:
613
638
  throw regs[base + this._operand()];
614
639
 
615
- // ── Closures ──────────────────────────────────────────────────────────
640
+ // Closures
616
641
  case OP.MAKE_CLOSURE:
617
642
  {
618
- // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]
643
+ // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]
619
644
  var dst = this._operand();
620
645
  var startPc = this._operand();
621
646
  var paramCount = this._operand();
622
647
  var regCount = this._operand();
623
648
  var uvCount = this._operand();
649
+ var hasRest = this._operand(); // 1 if last param is a rest element
650
+
624
651
  var uvDescs = new Array(uvCount);
625
652
  for (var i = 0; i < uvCount; i++) {
626
653
  var isLocalRaw = this._operand();
@@ -634,7 +661,8 @@ VM.prototype.run = function () {
634
661
  paramCount: paramCount,
635
662
  regCount: regCount,
636
663
  startPc: startPc,
637
- upvalueDescriptors: uvDescs
664
+ upvalueDescriptors: uvDescs,
665
+ hasRest: hasRest
638
666
  };
639
667
  var closure = new Closure(fn);
640
668
  for (var i = 0; i < uvDescs.length; i++) {
@@ -648,7 +676,7 @@ VM.prototype.run = function () {
648
676
 
649
677
  // Wrap in a native callable shell so host code (array methods,
650
678
  // test assertions, setTimeout, etc.) can invoke VM closures.
651
- // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.
679
+ // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely.
652
680
  var self = this;
653
681
  var shell = function (c) {
654
682
  return function () {
@@ -656,8 +684,12 @@ VM.prototype.run = function () {
656
684
  var sub = new VM(self.bytecode, 0, c.fn.regCount, self.constants, self.globals);
657
685
  var f = new Frame(c, null, null, this == null ? self.globals : this, 0, 0);
658
686
  sub._currentFrame = f;
659
- for (var i = 0; i < args.length && i < c.fn.regCount; i++) {
660
- sub._regs[i] = args[i];
687
+ if (c.fn.hasRest) {
688
+ var restSlot = c.fn.paramCount - 1;
689
+ for (var i = 0; i < restSlot; i++) sub._regs[i] = i < args.length ? args[i] : undefined;
690
+ sub._regs[restSlot] = args.slice(restSlot);
691
+ } else {
692
+ for (var i = 0; i < args.length && i < c.fn.regCount; i++) sub._regs[i] = args[i];
661
693
  }
662
694
  if (c.fn.paramCount < c.fn.regCount) {
663
695
  sub._regs[c.fn.paramCount] = args;
@@ -665,13 +697,13 @@ VM.prototype.run = function () {
665
697
  return sub.run();
666
698
  };
667
699
  }(closure);
668
- shell[CLOSURE_SYM] = closure;
700
+ CLOSURE_MAP.set(shell, closure);
669
701
  shell.prototype = closure.prototype; // unified prototype for new/instanceof
670
702
  regs[base + dst] = shell;
671
703
  break;
672
704
  }
673
705
 
674
- // ── Collections ───────────────────────────────────────────────────────
706
+ // Collections
675
707
  case OP.BUILD_ARRAY:
676
708
  {
677
709
  // dst, count, [elemReg...]
@@ -697,7 +729,7 @@ VM.prototype.run = function () {
697
729
  break;
698
730
  }
699
731
 
700
- // ── Property definitions (getters / setters) ──────────────────────────
732
+ // Object methods (getters / setters)
701
733
  case OP.DEFINE_GETTER:
702
734
  {
703
735
  // obj, key, fn
@@ -794,12 +826,28 @@ VM.prototype.run = function () {
794
826
  }
795
827
  case OP.TRY_END:
796
828
  {
797
- // Normal exit from a try block — disarm the exception handler.
829
+ // Normal exit from a try block — disarm the top handler record
830
+ // (works for both catch and finally regions; they share the stack).
798
831
  frame._handlerStack.pop();
799
832
  break;
800
833
  }
834
+ case OP.FINALLY_SETUP:
835
+ {
836
+ // finallyPc, contReg, payloadReg, throwPad
837
+ // Arm a finalizer for the current region. Unlike a catch record this
838
+ // carries no exceptionReg; instead the continuation register (contReg)
839
+ // receives the resume PC and payloadReg carries the in-flight value.
840
+ frame._handlerStack.push({
841
+ finallyPc: this._operand(),
842
+ contReg: this._operand(),
843
+ payloadReg: this._operand(),
844
+ throwPad: this._operand(),
845
+ frameStackDepth: this._frameStack.length
846
+ });
847
+ break;
848
+ }
801
849
 
802
- // ── Self-modifying bytecode ───────────────────────────────────────────
850
+ // Self-modifying bytecode
803
851
  case OP.PATCH:
804
852
  {
805
853
  // destPc, sliceStart, sliceEnd
@@ -813,10 +861,7 @@ VM.prototype.run = function () {
813
861
  }
814
862
  case OP.JUMP_REG:
815
863
  {
816
- // Indirect jump: target PC is read from a register rather than a
817
- // bytecode immediate. Used by the jumpDispatcher pass so that static
818
- // analysis cannot determine the jump destination without tracking the
819
- // register value (which contains an encoded PC resolved at runtime).
864
+ // Indirect jump: allows VM to jump based on runtime values.
820
865
  frame._pc = regs[base + this._operand()];
821
866
  break;
822
867
  }
@@ -829,9 +874,8 @@ VM.prototype.run = function () {
829
874
  throw new Error("Unknown opcode: " + op + " at pc " + (frame._pc - 1));
830
875
  }
831
876
  } catch (err) {
832
- // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).
833
- // Walk from the current frame upward until we find a frame that has an open
834
- // exception handler (TRY_SETUP without a matching TRY_END).
877
+ // Exception handler unwinding
878
+ // Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).
835
879
  // For every frame we abandon along the way, close its captured upvalues.
836
880
  var handledFrame = null;
837
881
  var searchFrame = this._currentFrame;
@@ -847,41 +891,38 @@ VM.prototype.run = function () {
847
891
  searchFrame = this._frameStack.pop();
848
892
  this._currentFrame = searchFrame;
849
893
  }
850
- if (!handledFrame) throw err; // no handler anywhere propagate to host
894
+ if (!handledFrame) throw err; // if there's no handler, propagate back to host
851
895
 
852
896
  var h = handledFrame._handlerStack.pop();
853
- // Discard any call-frames that were pushed inside the try body.
897
+ // Discard any call-frames that were pushed inside the protected region.
854
898
  this._frameStack.length = h.frameStackDepth;
855
- // Write the caught exception directly into the designated register.
856
- this._regs[handledFrame._base + h.exceptionReg] = err;
857
- // Jump to the catch block.
858
- handledFrame._pc = h.handlerPc;
859
- this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;
899
+ var hBase = handledFrame._base;
900
+ if (h.exceptionReg !== undefined) {
901
+ // catch region — deliver the exception to the catch binding and run it.
902
+ this._regs[hBase + h.exceptionReg] = err;
903
+ handledFrame._pc = h.handlerPc;
904
+ } else {
905
+ // finally region: run the finalizer with the exception pending, then
906
+ // resume at its throw pad (which re-raises and continues unwinding).
907
+ this._regs[hBase + h.contReg] = h.throwPad;
908
+ this._regs[hBase + h.payloadReg] = err;
909
+ handledFrame._pc = h.finallyPc;
910
+ }
911
+ this._regsTop = hBase + handledFrame.closure.fn.regCount;
860
912
  this._currentFrame = handledFrame;
861
913
  }
862
914
  }
863
915
  };
864
916
 
865
- // Boot
866
- var globals = {}; // global object for globals
867
-
868
- // Always pull built-ins from globalThis so eval() scoping can't shadow them
869
- // with a local `window` variable (e.g. the test harness fake window).
870
- for (var k of Object.getOwnPropertyNames(globalThis)) {
871
- globals[k] = globalThis[k];
872
- }
873
- // If a window object is in scope (browser or test harness), capture it
874
- // explicitly so VM code can read/write window.TEST_OUTPUT etc.
917
+ /* @BOOT */ // <- This comment can't be removed!
918
+ var globals = globalThis;
875
919
  if (typeof window !== "undefined") {
876
920
  globals.window = window;
877
- for (var k of Object.getOwnPropertyNames(window)) {
878
- globals[k] = window[k];
879
- }
921
+ globals.document = typeof document !== "undefined" ? document : undefined;
922
+ }
923
+ if (typeof module !== "undefined") {
924
+ globals.module = module;
925
+ globals.exports = typeof exports !== "undefined" ? exports : undefined;
880
926
  }
881
-
882
- // Transfer common primitives
883
- globals.undefined = undefined;
884
- globals.Infinity = Infinity;
885
- globals.NaN = NaN;
886
927
  var vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, MAIN_REG_COUNT, CONSTANTS, globals);
887
928
  vm.run();