js-confuser-vm 0.0.9 → 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 (64) hide show
  1. package/.gitmodules +4 -0
  2. package/CHANGELOG.md +125 -2
  3. package/README.md +128 -53
  4. package/bench.ts +146 -0
  5. package/disassemble.ts +12 -0
  6. package/dist/build-runtime.js +41 -15
  7. package/dist/compiler.js +328 -181
  8. package/dist/disassembler.js +317 -0
  9. package/dist/index.js +7 -2
  10. package/dist/runtime.js +255 -176
  11. package/dist/template.js +258 -0
  12. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  13. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  14. package/dist/transforms/bytecode/dispatcher.js +266 -0
  15. package/dist/transforms/bytecode/macroOpcodes.js +3 -3
  16. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  17. package/dist/transforms/bytecode/resolveLabels.js +21 -18
  18. package/dist/transforms/bytecode/resolveRegisters.js +216 -0
  19. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  20. package/dist/transforms/bytecode/specializedOpcodes.js +22 -12
  21. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  22. package/dist/transforms/runtime/classObfuscation.js +43 -0
  23. package/dist/transforms/runtime/handlerTable.js +91 -0
  24. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  25. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  26. package/dist/types.js +42 -1
  27. package/dist/utils/ast-utils.js +14 -0
  28. package/dist/utils/op-utils.js +1 -2
  29. package/dist/utils/pass-utils.js +100 -0
  30. package/dist/utils/profile-utils.js +3 -0
  31. package/index.ts +22 -16
  32. package/jest.config.js +19 -2
  33. package/output.disassembled.js +41 -0
  34. package/package.json +2 -1
  35. package/src/build-runtime.ts +113 -78
  36. package/src/compiler.ts +2703 -2482
  37. package/src/disassembler.ts +329 -0
  38. package/src/index.ts +12 -2
  39. package/src/options.ts +8 -1
  40. package/src/runtime.ts +294 -180
  41. package/src/template.ts +265 -0
  42. package/src/transforms/bytecode/aliasedOpcodes.ts +5 -2
  43. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  44. package/src/transforms/bytecode/dispatcher.ts +292 -0
  45. package/src/transforms/bytecode/macroOpcodes.ts +4 -4
  46. package/src/transforms/bytecode/resolveLabels.ts +31 -27
  47. package/src/transforms/bytecode/resolveRegisters.ts +226 -0
  48. package/src/transforms/bytecode/specializedOpcodes.ts +27 -20
  49. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  50. package/src/transforms/runtime/classObfuscation.ts +59 -0
  51. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  52. package/src/types.ts +106 -5
  53. package/src/utils/ast-utils.ts +19 -0
  54. package/src/utils/op-utils.ts +2 -2
  55. package/src/utils/pass-utils.ts +126 -0
  56. package/src/utils/profile-utils.ts +3 -0
  57. package/tsconfig.json +1 -1
  58. package/dist/transforms/utils/op-utils.js +0 -25
  59. package/dist/transforms/utils/random-utils.js +0 -27
  60. package/dist/utilts.js +0 -3
  61. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  62. package/src/transforms/runtime/internalVariables.ts +0 -270
  63. package/src/transforms/runtime/microOpcodes.ts +0 -93
  64. /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
 
@@ -28,24 +37,25 @@ function decodeBytecode(s) {
28
37
  // inner Closure instead of going through a sub-VM on internal calls.
29
38
  var CLOSURE_SYM = Symbol(); // Nameless for obfuscation
30
39
 
31
- // Upvalue
32
- // While the outer frame is alive: reads/writes go to frame.regs[slot].
40
+ // Upvalue — Lua/CPython style.
41
+ // While the outer frame is alive: reads/writes go to vm._regs[_absSlot].
33
42
  // After the outer frame returns (closed): reads/writes hit this._value.
34
- function Upvalue(frame, slot) {
35
- this._frame = frame;
36
- this._slot = slot;
43
+ // _absSlot is the absolute index in VM._regs (frame._base + local slot).
44
+ function Upvalue(regs, absSlot) {
45
+ this._regs = regs; // shared reference to VM._regs flat array
46
+ this._absSlot = absSlot; // absolute index; stable as long as frame is alive
37
47
  this._closed = false;
38
48
  this._value = undefined;
39
49
  }
40
50
  Upvalue.prototype._read = function () {
41
- return this._closed ? this._value : this._frame.regs[this._slot];
51
+ return this._closed ? this._value : this._regs[this._absSlot];
42
52
  };
43
53
  Upvalue.prototype._write = function (v) {
44
54
  if (this._closed) this._value = v;
45
- else this._frame.regs[this._slot] = v;
55
+ else this._regs[this._absSlot] = v;
46
56
  };
47
57
  Upvalue.prototype._close = function () {
48
- this._value = this._frame.regs[this._slot];
58
+ this._value = this._regs[this._absSlot];
49
59
  this._closed = true;
50
60
  };
51
61
 
@@ -56,16 +66,18 @@ function Closure(fn) {
56
66
  this.prototype = {}; // <- default prototype object for `new`
57
67
  }
58
68
 
59
- function Frame(closure, returnPc, parent, thisVal, retDstReg) {
69
+ // Frame analogous to Lua CallInfo / CPython PyFrameObject.
70
+ // Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).
71
+ function Frame(closure, returnPc, parent, thisVal, retDstReg, base) {
60
72
  this.closure = closure;
61
- this.regs = new Array(closure.fn.regCount).fill(undefined);
62
- this._pc = closure.fn.startPc; // <- initialize from fn descriptor
63
- this._returnPc = returnPc; // pc to resume in parent frame after RETURN
73
+ this._base = base; // absolute offset into VM._regs for this frame's r0
74
+ this._pc = closure.fn.startPc;
75
+ this._returnPc = returnPc;
64
76
  this._parent = parent;
65
77
  this.thisVal = thisVal !== undefined ? thisVal : undefined;
66
- this._retDstReg = retDstReg !== undefined ? retDstReg : 0; // register in parent to write return value
67
- this._newObj = null; // <- set by NEW so RETURN can see it
68
- this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP
78
+ this._retDstReg = retDstReg !== undefined ? retDstReg : 0;
79
+ this._newObj = null;
80
+ this._handlerStack = [];
69
81
  }
70
82
 
71
83
  // VM
@@ -74,15 +86,29 @@ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
74
86
  this.constants = constants;
75
87
  this.globals = globals;
76
88
  this._frameStack = [];
77
- this._openUpvalues = []; // all currently open Upvalue objects across all frames
89
+ this._openUpvalues = [];
90
+
91
+ // ── Flat register file (Lua-style) ────────────────────────────────────────
92
+ // All frames share a single array. Each Frame records its _base offset.
93
+ // _regsTop is the next free slot (= base of the hypothetical next frame).
94
+ // On CALL: newBase = _regsTop; _regsTop += fn.regCount
95
+ // On RETURN: _regsTop = frame._base (pop the frame's register window)
96
+ this._regs = new Array(mainRegCount).fill(undefined);
97
+ this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)
78
98
 
79
99
  var mainFn = {
80
100
  paramCount: 0,
81
101
  regCount: mainRegCount,
82
- startPc: mainStartPc, // <- where main begins
102
+ startPc: mainStartPc,
83
103
  };
84
- this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0);
85
- this._internals = {};
104
+ this._currentFrame = new Frame(
105
+ new Closure(mainFn),
106
+ null,
107
+ null,
108
+ undefined,
109
+ 0,
110
+ 0,
111
+ );
86
112
  }
87
113
 
88
114
  // Consume the next slot from the flat bytecode stream and advance the PC.
@@ -92,13 +118,13 @@ VM.prototype._operand = function () {
92
118
  };
93
119
 
94
120
  VM.prototype.captureUpvalue = function (frame, slot) {
95
- // Reuse existing open upvalue for this frame+slot if one exists.
96
- // This is what makes two closures share the same mutable cell.
121
+ // Dedup by absolute slot two closures capturing the same local share one Upvalue.
122
+ var absSlot = frame._base + slot;
97
123
  for (var i = 0; i < this._openUpvalues.length; i++) {
98
124
  var uv = this._openUpvalues[i];
99
- if (uv._frame === frame && uv._slot === slot) return uv;
125
+ if (!uv._closed && uv._absSlot === absSlot) return uv;
100
126
  }
101
- var uv = new Upvalue(frame, slot);
127
+ var uv = new Upvalue(this._regs, absSlot);
102
128
  this._openUpvalues.push(uv);
103
129
  return uv;
104
130
  };
@@ -120,12 +146,7 @@ VM.prototype._constant = function (idxIn, keyIn) {
120
146
  if (!key) return v;
121
147
  if (typeof v === "number") return v ^ key;
122
148
  // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).
123
- var b =
124
- typeof Buffer !== "undefined"
125
- ? Buffer.from(v, "base64")
126
- : Uint8Array.from(atob(v), function (c) {
127
- return c.charCodeAt(0);
128
- });
149
+ var b = base64ToBytes(v);
129
150
  var out = "";
130
151
  for (var i = 0; i < b.length / 2; i++) {
131
152
  var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE
@@ -135,10 +156,12 @@ VM.prototype._constant = function (idxIn, keyIn) {
135
156
  };
136
157
 
137
158
  VM.prototype._closeUpvaluesFor = function (frame) {
138
- // Called on RETURN - close every upvalue that was pointing into this frame.
139
- // After this, closures that captured from the frame read from upvalue.value.
159
+ // Called on RETURN close every upvalue whose absolute slot falls within
160
+ // this frame's register window [_base, _base + regCount).
161
+ var lo = frame._base;
162
+ var hi = frame._base + frame.closure.fn.regCount;
140
163
  this._openUpvalues = this._openUpvalues.filter(function (uv) {
141
- if (uv._frame === frame) {
164
+ if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {
142
165
  uv._close();
143
166
  return false;
144
167
  }
@@ -146,6 +169,12 @@ VM.prototype._closeUpvaluesFor = function (frame) {
146
169
  });
147
170
  };
148
171
 
172
+ VM.prototype._ensureRegisterWindow = function (base, regCount) {
173
+ var end = base + regCount;
174
+ while (this._regs.length < end) this._regs.push(undefined);
175
+ for (var i = base; i < end; i++) this._regs[i] = undefined;
176
+ };
177
+
149
178
  VM.prototype.run = function () {
150
179
  var now = () => {
151
180
  return performance.now();
@@ -176,24 +205,28 @@ VM.prototype.run = function () {
176
205
  // Poison the bytecode
177
206
  for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;
178
207
  // Break the current state
179
- frame.regs.fill(undefined);
208
+ for (var i2 = frame._base; i2 < this._regsTop; i2++)
209
+ this._regs[i2] = undefined;
180
210
  op = OP.JUMP;
181
211
  frame._pc = this.bytecode.length; // jump past end to halt
182
212
  }
183
213
  }
184
214
 
185
215
  try {
216
+ var regs = this._regs;
217
+ var base = frame._base;
218
+
186
219
  /* @SWITCH */
187
220
  switch (op) {
188
221
  case OP.LOAD_CONST: {
189
222
  var dst = this._operand();
190
- frame.regs[dst] = this._constant();
223
+ regs[base + dst] = this._constant();
191
224
  break;
192
225
  }
193
226
 
194
227
  case OP.LOAD_INT: {
195
228
  var dst = this._operand();
196
- frame.regs[dst] = this._operand();
229
+ regs[base + dst] = this._operand();
197
230
  break;
198
231
  }
199
232
 
@@ -205,200 +238,201 @@ VM.prototype.run = function () {
205
238
  throw new ReferenceError(`${globalName} is not defined`);
206
239
  }
207
240
 
208
- frame.regs[dst] = this.globals[globalName];
241
+ regs[base + dst] = this.globals[globalName];
209
242
  break;
210
243
  }
211
244
 
212
245
  case OP.LOAD_UPVALUE: {
213
246
  var dst = this._operand();
214
- frame.regs[dst] = frame.closure.upvalues[this._operand()]._read();
247
+ regs[base + dst] = frame.closure.upvalues[this._operand()]._read();
215
248
  break;
216
249
  }
217
250
 
218
251
  case OP.LOAD_THIS: {
219
252
  var dst = this._operand();
220
- frame.regs[dst] = frame.thisVal;
253
+ regs[base + dst] = frame.thisVal;
221
254
  break;
222
255
  }
223
256
 
224
257
  case OP.MOVE: {
225
258
  var dst = this._operand();
226
- frame.regs[dst] = frame.regs[this._operand()];
259
+ regs[base + dst] = regs[base + this._operand()];
227
260
  break;
228
261
  }
229
262
 
230
263
  case OP.STORE_GLOBAL: {
231
- // nameIdx and key are consumed inline so the concealConstants runtime
232
- // transform can rewrite this._constant() consistently.
233
- this.globals[this._constant()] = frame.regs[this._operand()];
264
+ // globals[globalName] = regs[src]
265
+ this.globals[this._constant()] = regs[base + this._operand()];
234
266
  break;
235
267
  }
236
268
 
237
269
  case OP.STORE_UPVALUE: {
238
270
  var uvIdx = this._operand();
239
- frame.closure.upvalues[uvIdx]._write(frame.regs[this._operand()]);
271
+ frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);
240
272
  break;
241
273
  }
242
274
 
243
275
  case OP.GET_PROP: {
244
276
  // dst = regs[obj][regs[key]]
245
277
  var dst = this._operand();
246
- var obj = frame.regs[this._operand()];
247
- var key = frame.regs[this._operand()];
248
- frame.regs[dst] = obj[key];
278
+ var obj = regs[base + this._operand()];
279
+ var key = regs[base + this._operand()];
280
+ regs[base + dst] = obj[key];
249
281
  break;
250
282
  }
251
283
 
252
284
  case OP.SET_PROP: {
253
285
  // regs[obj][regs[key]] = regs[val]
254
- var obj = frame.regs[this._operand()];
255
- var key = frame.regs[this._operand()];
256
- var val = frame.regs[this._operand()];
257
- // Reflect.set performs [[Set]] without throwing on failure,
258
- // correctly simulating sloppy-mode assignment from a strict-mode host.
286
+ var obj = regs[base + this._operand()];
287
+ var key = regs[base + this._operand()];
288
+ var val = regs[base + this._operand()];
289
+ // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)
259
290
  Reflect.set(obj, key, val);
260
291
  break;
261
292
  }
262
293
 
263
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
264
297
  var dst = this._operand();
265
- var obj = frame.regs[this._operand()];
266
- var key = frame.regs[this._operand()];
267
- frame.regs[dst] = delete obj[key];
298
+ var obj = regs[base + this._operand()];
299
+ var key = regs[base + this._operand()];
300
+ regs[base + dst] = delete obj[key];
268
301
  break;
269
302
  }
270
303
 
271
- // ── Arithmetic (dst, src1, src2) ────────────────────────────────────
304
+ // Arithmetic (dst, src1, src2)
272
305
  case OP.ADD: {
273
306
  var dst = this._operand();
274
- var a = frame.regs[this._operand()];
275
- frame.regs[dst] = a + frame.regs[this._operand()];
307
+ var a = regs[base + this._operand()];
308
+ regs[base + dst] = a + regs[base + this._operand()];
276
309
  break;
277
310
  }
278
311
  case OP.SUB: {
279
312
  var dst = this._operand();
280
- var a = frame.regs[this._operand()];
281
- frame.regs[dst] = a - frame.regs[this._operand()];
313
+ var a = regs[base + this._operand()];
314
+ regs[base + dst] = a - regs[base + this._operand()];
282
315
  break;
283
316
  }
284
317
  case OP.MUL: {
285
318
  var dst = this._operand();
286
- var a = frame.regs[this._operand()];
287
- frame.regs[dst] = a * frame.regs[this._operand()];
319
+ var a = regs[base + this._operand()];
320
+ regs[base + dst] = a * regs[base + this._operand()];
288
321
  break;
289
322
  }
290
323
  case OP.DIV: {
291
324
  var dst = this._operand();
292
- var a = frame.regs[this._operand()];
293
- frame.regs[dst] = a / frame.regs[this._operand()];
325
+ var a = regs[base + this._operand()];
326
+ regs[base + dst] = a / regs[base + this._operand()];
294
327
  break;
295
328
  }
296
329
  case OP.MOD: {
297
330
  var dst = this._operand();
298
- var a = frame.regs[this._operand()];
299
- frame.regs[dst] = a % frame.regs[this._operand()];
331
+ var a = regs[base + this._operand()];
332
+ regs[base + dst] = a % regs[base + this._operand()];
300
333
  break;
301
334
  }
302
335
  case OP.BAND: {
303
336
  var dst = this._operand();
304
- var a = frame.regs[this._operand()];
305
- frame.regs[dst] = a & frame.regs[this._operand()];
337
+ var a = regs[base + this._operand()];
338
+ regs[base + dst] = a & regs[base + this._operand()];
306
339
  break;
307
340
  }
308
341
  case OP.BOR: {
309
342
  var dst = this._operand();
310
- var a = frame.regs[this._operand()];
311
- frame.regs[dst] = a | frame.regs[this._operand()];
343
+ var a = regs[base + this._operand()];
344
+ regs[base + dst] = a | regs[base + this._operand()];
312
345
  break;
313
346
  }
314
347
  case OP.BXOR: {
315
348
  var dst = this._operand();
316
- var a = frame.regs[this._operand()];
317
- frame.regs[dst] = a ^ frame.regs[this._operand()];
349
+ var a = regs[base + this._operand()];
350
+ regs[base + dst] = a ^ regs[base + this._operand()];
318
351
  break;
319
352
  }
320
353
  case OP.SHL: {
321
354
  var dst = this._operand();
322
- var a = frame.regs[this._operand()];
323
- frame.regs[dst] = a << frame.regs[this._operand()];
355
+ var a = regs[base + this._operand()];
356
+ regs[base + dst] = a << regs[base + this._operand()];
324
357
  break;
325
358
  }
326
359
  case OP.SHR: {
327
360
  var dst = this._operand();
328
- var a = frame.regs[this._operand()];
329
- frame.regs[dst] = a >> frame.regs[this._operand()];
361
+ var a = regs[base + this._operand()];
362
+ regs[base + dst] = a >> regs[base + this._operand()];
330
363
  break;
331
364
  }
332
365
  case OP.USHR: {
333
366
  var dst = this._operand();
334
- var a = frame.regs[this._operand()];
335
- frame.regs[dst] = a >>> frame.regs[this._operand()];
367
+ var a = regs[base + this._operand()];
368
+ regs[base + dst] = a >>> regs[base + this._operand()];
336
369
  break;
337
370
  }
338
371
 
339
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────
372
+ // Comparison (dst, src1, src2)
340
373
  case OP.LT: {
341
374
  var dst = this._operand();
342
- var a = frame.regs[this._operand()];
343
- frame.regs[dst] = a < frame.regs[this._operand()];
375
+ var a = regs[base + this._operand()];
376
+ regs[base + dst] = a < regs[base + this._operand()];
344
377
  break;
345
378
  }
346
379
  case OP.GT: {
347
380
  var dst = this._operand();
348
- var a = frame.regs[this._operand()];
349
- frame.regs[dst] = a > frame.regs[this._operand()];
381
+ var a = regs[base + this._operand()];
382
+ regs[base + dst] = a > regs[base + this._operand()];
350
383
  break;
351
384
  }
352
385
  case OP.LTE: {
353
386
  var dst = this._operand();
354
- var a = frame.regs[this._operand()];
355
- frame.regs[dst] = a <= frame.regs[this._operand()];
387
+ var a = regs[base + this._operand()];
388
+ regs[base + dst] = a <= regs[base + this._operand()];
356
389
  break;
357
390
  }
358
391
  case OP.GTE: {
359
392
  var dst = this._operand();
360
- var a = frame.regs[this._operand()];
361
- frame.regs[dst] = a >= frame.regs[this._operand()];
393
+ var a = regs[base + this._operand()];
394
+ regs[base + dst] = a >= regs[base + this._operand()];
362
395
  break;
363
396
  }
364
397
  case OP.EQ: {
365
398
  var dst = this._operand();
366
- var a = frame.regs[this._operand()];
367
- frame.regs[dst] = a === frame.regs[this._operand()];
399
+ var a = regs[base + this._operand()];
400
+ regs[base + dst] = a === regs[base + this._operand()];
368
401
  break;
369
402
  }
370
403
  case OP.NEQ: {
371
404
  var dst = this._operand();
372
- var a = frame.regs[this._operand()];
373
- frame.regs[dst] = a !== frame.regs[this._operand()];
405
+ var a = regs[base + this._operand()];
406
+ regs[base + dst] = a !== regs[base + this._operand()];
374
407
  break;
375
408
  }
376
409
  case OP.LOOSE_EQ: {
377
410
  var dst = this._operand();
378
- var a = frame.regs[this._operand()];
379
- frame.regs[dst] = a == frame.regs[this._operand()];
411
+ var a = regs[base + this._operand()];
412
+ regs[base + dst] = a == regs[base + this._operand()];
380
413
  break;
381
414
  }
382
415
  case OP.LOOSE_NEQ: {
383
416
  var dst = this._operand();
384
- var a = frame.regs[this._operand()];
385
- frame.regs[dst] = a != frame.regs[this._operand()];
417
+ var a = regs[base + this._operand()];
418
+ regs[base + dst] = a != regs[base + this._operand()];
386
419
  break;
387
420
  }
388
421
  case OP.IN: {
389
422
  var dst = this._operand();
390
- var a = frame.regs[this._operand()];
391
- frame.regs[dst] = a in frame.regs[this._operand()];
423
+ var a = regs[base + this._operand()];
424
+ regs[base + dst] = a in regs[base + this._operand()];
392
425
  break;
393
426
  }
394
427
  case OP.INSTANCEOF: {
428
+ // regs[dst] = regs[obj] instanceof regs[ctor]
395
429
  var dst = this._operand();
396
- var obj = frame.regs[this._operand()];
397
- var ctor = frame.regs[this._operand()];
430
+ var obj = regs[base + this._operand()];
431
+ var ctor = regs[base + this._operand()];
398
432
  if (typeof ctor === "function") {
399
- frame.regs[dst] = obj instanceof ctor;
433
+ regs[base + dst] = obj instanceof ctor;
400
434
  } else {
401
- // VM Closure - walk prototype chain for identity with ctor.prototype.
435
+ // TODO: Why is this needed?
402
436
  var proto = ctor.prototype;
403
437
  var target = Object.getPrototypeOf(obj);
404
438
  var result = false;
@@ -409,55 +443,56 @@ VM.prototype.run = function () {
409
443
  }
410
444
  target = Object.getPrototypeOf(target);
411
445
  }
412
- frame.regs[dst] = result;
446
+ regs[base + dst] = result;
413
447
  }
414
448
  break;
415
449
  }
416
450
 
417
- // ── Unary (dst, src) ─────────────────────────────────────────────────
451
+ // Unary (dst, src)
418
452
  case OP.UNARY_NEG: {
419
453
  var dst = this._operand();
420
- frame.regs[dst] = -frame.regs[this._operand()];
454
+ regs[base + dst] = -regs[base + this._operand()];
421
455
  break;
422
456
  }
423
457
  case OP.UNARY_POS: {
424
458
  var dst = this._operand();
425
- frame.regs[dst] = +frame.regs[this._operand()];
459
+ regs[base + dst] = +regs[base + this._operand()];
426
460
  break;
427
461
  }
428
462
  case OP.UNARY_NOT: {
429
463
  var dst = this._operand();
430
- frame.regs[dst] = !frame.regs[this._operand()];
464
+ regs[base + dst] = !regs[base + this._operand()];
431
465
  break;
432
466
  }
433
467
  case OP.UNARY_BITNOT: {
434
468
  var dst = this._operand();
435
- frame.regs[dst] = ~frame.regs[this._operand()];
469
+ regs[base + dst] = ~regs[base + this._operand()];
436
470
  break;
437
471
  }
438
472
  case OP.TYPEOF: {
439
473
  var dst = this._operand();
440
- frame.regs[dst] = typeof frame.regs[this._operand()];
474
+ regs[base + dst] = typeof regs[base + this._operand()];
441
475
  break;
442
476
  }
443
477
  case OP.VOID: {
444
478
  var dst = this._operand();
445
- this._operand(); // consume src — evaluated for side-effects by compiler
446
- frame.regs[dst] = undefined;
479
+ this._operand(); // consumes argument (intended)
480
+ regs[base + dst] = undefined;
447
481
  break;
448
482
  }
449
483
  case OP.TYPEOF_SAFE: {
450
- // dst, nameConstIdx — safe typeof for potentially-undeclared globals.
484
+ // regs[dst] = typeof window[name]
485
+ // Never throws ReferenceError, instead returns undefined for undeclared variables
451
486
  var dst = this._operand();
452
487
  var name = this._constant();
453
488
  var val = Object.prototype.hasOwnProperty.call(this.globals, name)
454
489
  ? this.globals[name]
455
490
  : undefined;
456
- frame.regs[dst] = typeof val;
491
+ regs[base + dst] = typeof val;
457
492
  break;
458
493
  }
459
494
 
460
- // ── Control flow ──────────────────────────────────────────────────────
495
+ // Control flow
461
496
  case OP.JUMP:
462
497
  frame._pc = this._operand();
463
498
  break;
@@ -465,7 +500,7 @@ VM.prototype.run = function () {
465
500
  case OP.JUMP_IF_FALSE: {
466
501
  var src = this._operand();
467
502
  var target = this._operand();
468
- if (!frame.regs[src]) frame._pc = target;
503
+ if (!regs[base + src]) frame._pc = target;
469
504
  break;
470
505
  }
471
506
 
@@ -473,28 +508,48 @@ VM.prototype.run = function () {
473
508
  // || short-circuit: if truthy, jump over RHS.
474
509
  var src = this._operand();
475
510
  var target = this._operand();
476
- if (frame.regs[src]) frame._pc = target;
511
+ if (regs[base + src]) frame._pc = target;
477
512
  break;
478
513
  }
479
514
 
480
- // ── Calls ─────────────────────────────────────────────────────────────
515
+ // Calls
481
516
  case OP.CALL: {
482
517
  // dst, calleeReg, argc, [argReg...]
483
518
  var dst = this._operand();
484
- var callee = frame.regs[this._operand()];
519
+ var callee = regs[base + this._operand()];
485
520
  var argc = this._operand();
486
521
  var args = new Array(argc);
487
- for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];
522
+ for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
488
523
 
489
524
  if (callee && callee[CLOSURE_SYM]) {
490
- var c = callee[CLOSURE_SYM];
491
- var f = new Frame(c, frame._pc, frame, this.globals, dst);
492
- for (var i = 0; i < args.length; i++) f.regs[i] = args[i];
493
- f.regs[c.fn.paramCount] = args;
525
+ var closure = callee[CLOSURE_SYM];
526
+ var newBase = this._regsTop;
527
+ this._ensureRegisterWindow(newBase, closure.fn.regCount);
528
+ this._regsTop = newBase + closure.fn.regCount;
529
+ var f = new Frame(
530
+ closure,
531
+ frame._pc,
532
+ frame,
533
+ this.globals,
534
+ dst,
535
+ newBase,
536
+ );
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];
545
+ }
546
+ if (closure.fn.paramCount < closure.fn.regCount) {
547
+ this._regs[newBase + closure.fn.paramCount] = args;
548
+ }
494
549
  this._frameStack.push(this._currentFrame);
495
550
  this._currentFrame = f;
496
551
  } else {
497
- frame.regs[dst] = callee.apply(null, args);
552
+ regs[base + dst] = callee.apply(null, args);
498
553
  }
499
554
  break;
500
555
  }
@@ -502,21 +557,41 @@ VM.prototype.run = function () {
502
557
  case OP.CALL_METHOD: {
503
558
  // dst, receiverReg, calleeReg, argc, [argReg...]
504
559
  var dst = this._operand();
505
- var receiver = frame.regs[this._operand()];
506
- var callee = frame.regs[this._operand()];
560
+ var receiver = regs[base + this._operand()];
561
+ var callee = regs[base + this._operand()];
507
562
  var argc = this._operand();
508
563
  var args = new Array(argc);
509
- for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];
564
+ for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
510
565
 
511
566
  if (callee && callee[CLOSURE_SYM]) {
512
- var c = callee[CLOSURE_SYM];
513
- var f = new Frame(c, frame._pc, frame, receiver, dst);
514
- for (var i = 0; i < args.length; i++) f.regs[i] = args[i];
515
- f.regs[c.fn.paramCount] = args;
567
+ var closure = callee[CLOSURE_SYM];
568
+ var newBase = this._regsTop;
569
+ this._ensureRegisterWindow(newBase, closure.fn.regCount);
570
+ this._regsTop = newBase + closure.fn.regCount;
571
+ var f = new Frame(
572
+ closure,
573
+ frame._pc,
574
+ frame,
575
+ receiver,
576
+ dst,
577
+ newBase,
578
+ );
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];
587
+ }
588
+ if (closure.fn.paramCount < closure.fn.regCount) {
589
+ this._regs[newBase + closure.fn.paramCount] = args;
590
+ }
516
591
  this._frameStack.push(this._currentFrame);
517
592
  this._currentFrame = f;
518
593
  } else {
519
- frame.regs[dst] = callee.apply(receiver, args);
594
+ regs[base + dst] = callee.apply(receiver, args);
520
595
  }
521
596
  break;
522
597
  }
@@ -524,57 +599,77 @@ VM.prototype.run = function () {
524
599
  case OP.NEW: {
525
600
  // dst, calleeReg, argc, [argReg...]
526
601
  var dst = this._operand();
527
- var callee = frame.regs[this._operand()];
602
+ var callee = regs[base + this._operand()];
528
603
  var argc = this._operand();
529
604
  var args = new Array(argc);
530
- for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];
605
+ for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];
531
606
 
532
607
  if (callee && callee[CLOSURE_SYM]) {
533
- var c = callee[CLOSURE_SYM];
534
- var newObj = Object.create(c.prototype || null);
535
- var f = new Frame(c, frame._pc, frame, newObj, dst);
608
+ var closure = callee[CLOSURE_SYM];
609
+ var newObj = Object.create(closure.prototype || null);
610
+ var newBase = this._regsTop;
611
+ this._ensureRegisterWindow(newBase, closure.fn.regCount);
612
+ this._regsTop = newBase + closure.fn.regCount;
613
+ var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);
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];
622
+ }
623
+ if (closure.fn.paramCount < closure.fn.regCount) {
624
+ this._regs[newBase + closure.fn.paramCount] = args;
625
+ }
536
626
  f._newObj = newObj;
537
- for (var i = 0; i < args.length; i++) f.regs[i] = args[i];
538
- f.regs[c.fn.paramCount] = args;
539
627
  this._frameStack.push(this._currentFrame);
540
628
  this._currentFrame = f;
541
629
  } else {
542
630
  // Reflect.construct is required - Object.create+apply does NOT set
543
631
  // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.
544
- frame.regs[dst] = Reflect.construct(callee, args);
632
+ regs[base + dst] = Reflect.construct(callee, args);
545
633
  }
546
634
  break;
547
635
  }
548
636
 
549
637
  case OP.RETURN: {
550
- var retVal = frame.regs[this._operand()];
638
+ var retVal = regs[base + this._operand()];
551
639
  this._closeUpvaluesFor(frame); // must happen before frame is abandoned
552
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;
645
+ this._regsTop = frame._base;
646
+
553
647
  if (this._frameStack.length === 0) return retVal; // main script returning
554
648
 
555
- // 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)
556
650
  if (frame._newObj !== null) {
557
651
  if (typeof retVal !== "object" || retVal === null)
558
652
  retVal = frame._newObj;
559
653
  }
560
654
 
561
655
  var parentFrame = this._frameStack.pop();
562
- parentFrame.regs[frame._retDstReg] = retVal;
656
+ this._regs[parentFrame._base + frame._retDstReg] = retVal;
563
657
  this._currentFrame = parentFrame;
564
658
  break;
565
659
  }
566
660
 
567
661
  case OP.THROW:
568
- throw frame.regs[this._operand()];
662
+ throw regs[base + this._operand()];
569
663
 
570
- // ── Closures ──────────────────────────────────────────────────────────
664
+ // Closures
571
665
  case OP.MAKE_CLOSURE: {
572
- // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]
666
+ // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]
573
667
  var dst = this._operand();
574
668
  var startPc = this._operand();
575
669
  var paramCount = this._operand();
576
670
  var regCount = this._operand();
577
671
  var uvCount = this._operand();
672
+ var hasRest = this._operand(); // 1 if last param is a rest element
578
673
 
579
674
  var uvDescs = new Array(uvCount);
580
675
  for (var i = 0; i < uvCount; i++) {
@@ -588,6 +683,7 @@ VM.prototype.run = function () {
588
683
  regCount: regCount,
589
684
  startPc: startPc,
590
685
  upvalueDescriptors: uvDescs,
686
+ hasRest: hasRest,
591
687
  };
592
688
 
593
689
  var closure = new Closure(fn);
@@ -620,28 +716,39 @@ VM.prototype.run = function () {
620
716
  null,
621
717
  this == null ? self.globals : this,
622
718
  0,
719
+ 0,
623
720
  );
624
- for (var i = 0; i < args.length; i++) f.regs[i] = args[i];
625
- f.regs[c.fn.paramCount] = args;
626
721
  sub._currentFrame = f;
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];
730
+ }
731
+ if (c.fn.paramCount < c.fn.regCount) {
732
+ sub._regs[c.fn.paramCount] = args;
733
+ }
627
734
  return sub.run();
628
735
  };
629
736
  })(closure);
630
737
  shell[CLOSURE_SYM] = closure;
631
738
  shell.prototype = closure.prototype; // unified prototype for new/instanceof
632
- frame.regs[dst] = shell;
739
+ regs[base + dst] = shell;
633
740
  break;
634
741
  }
635
742
 
636
- // ── Collections ───────────────────────────────────────────────────────
743
+ // Collections
637
744
  case OP.BUILD_ARRAY: {
638
745
  // dst, count, [elemReg...]
639
746
  var dst = this._operand();
640
747
  var count = this._operand();
641
748
  var elems = new Array(count);
642
749
  for (var i = 0; i < count; i++)
643
- elems[i] = frame.regs[this._operand()];
644
- frame.regs[dst] = elems;
750
+ elems[i] = regs[base + this._operand()];
751
+ regs[base + dst] = elems;
645
752
  break;
646
753
  }
647
754
 
@@ -651,20 +758,20 @@ VM.prototype.run = function () {
651
758
  var pairCount = this._operand();
652
759
  var o = {};
653
760
  for (var i = 0; i < pairCount; i++) {
654
- var key = frame.regs[this._operand()];
655
- var val = frame.regs[this._operand()];
761
+ var key = regs[base + this._operand()];
762
+ var val = regs[base + this._operand()];
656
763
  o[key] = val;
657
764
  }
658
- frame.regs[dst] = o;
765
+ regs[base + dst] = o;
659
766
  break;
660
767
  }
661
768
 
662
- // ── Property definitions (getters / setters) ──────────────────────────
769
+ // Object methods (getters / setters)
663
770
  case OP.DEFINE_GETTER: {
664
771
  // obj, key, fn
665
- var obj = frame.regs[this._operand()];
666
- var key = frame.regs[this._operand()];
667
- var getterFn = frame.regs[this._operand()];
772
+ var obj = regs[base + this._operand()];
773
+ var key = regs[base + this._operand()];
774
+ var getterFn = regs[base + this._operand()];
668
775
  var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
669
776
  var getDesc: PropertyDescriptor = {
670
777
  get: getterFn,
@@ -680,9 +787,9 @@ VM.prototype.run = function () {
680
787
 
681
788
  case OP.DEFINE_SETTER: {
682
789
  // obj, key, fn
683
- var obj = frame.regs[this._operand()];
684
- var key = frame.regs[this._operand()];
685
- var setterFn = frame.regs[this._operand()];
790
+ var obj = regs[base + this._operand()];
791
+ var key = regs[base + this._operand()];
792
+ var setterFn = regs[base + this._operand()];
686
793
  var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
687
794
  var setDesc: PropertyDescriptor = {
688
795
  set: setterFn,
@@ -700,7 +807,7 @@ VM.prototype.run = function () {
700
807
  case OP.FOR_IN_SETUP: {
701
808
  // dst, src — build iterator object from enumerable keys of regs[src]
702
809
  var dst = this._operand();
703
- var obj = frame.regs[this._operand()];
810
+ var obj = regs[base + this._operand()];
704
811
  var keys = [];
705
812
  if (obj !== null && obj !== undefined) {
706
813
  var seen = Object.create(null);
@@ -720,7 +827,7 @@ VM.prototype.run = function () {
720
827
  cur = Object.getPrototypeOf(cur);
721
828
  }
722
829
  }
723
- frame.regs[dst] = { _keys: keys, i: 0 };
830
+ regs[base + dst] = { _keys: keys, i: 0 };
724
831
  break;
725
832
  }
726
833
 
@@ -728,12 +835,12 @@ VM.prototype.run = function () {
728
835
  // dst, iterReg, exitTarget
729
836
  // Advances iterator; writes next key to dst, or jumps to exitTarget when done.
730
837
  var dst = this._operand();
731
- var iter = frame.regs[this._operand()];
838
+ var iter = regs[base + this._operand()];
732
839
  var exitTarget = this._operand();
733
840
  if (iter.i >= iter._keys.length) {
734
841
  frame._pc = exitTarget;
735
842
  } else {
736
- frame.regs[dst] = iter._keys[iter.i++];
843
+ regs[base + dst] = iter._keys[iter.i++];
737
844
  }
738
845
  break;
739
846
  }
@@ -755,7 +862,7 @@ VM.prototype.run = function () {
755
862
  break;
756
863
  }
757
864
 
758
- // ── Self-modifying bytecode ───────────────────────────────────────────
865
+ // Self-modifying bytecode
759
866
  case OP.PATCH: {
760
867
  // destPc, sliceStart, sliceEnd
761
868
  var destPc = this._operand();
@@ -767,6 +874,12 @@ VM.prototype.run = function () {
767
874
  break;
768
875
  }
769
876
 
877
+ case OP.JUMP_REG: {
878
+ // Indirect jump: allows VM to jump based on runtime values.
879
+ frame._pc = regs[base + this._operand()];
880
+ break;
881
+ }
882
+
770
883
  case OP.DEBUGGER: {
771
884
  debugger;
772
885
  break;
@@ -778,9 +891,8 @@ VM.prototype.run = function () {
778
891
  );
779
892
  }
780
893
  } catch (err) {
781
- // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).
782
- // Walk from the current frame upward until we find a frame that has an open
783
- // 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).
784
896
  // For every frame we abandon along the way, close its captured upvalues.
785
897
  var handledFrame = null;
786
898
  var searchFrame = this._currentFrame;
@@ -791,26 +903,28 @@ VM.prototype.run = function () {
791
903
  }
792
904
  // No handler in this frame — abandon it and walk up.
793
905
  this._closeUpvaluesFor(searchFrame);
906
+ this._regsTop = searchFrame._base;
794
907
  if (this._frameStack.length === 0) break;
795
908
  searchFrame = this._frameStack.pop();
796
909
  this._currentFrame = searchFrame;
797
910
  }
798
911
 
799
- if (!handledFrame) throw err; // no handler anywhere propagate to host
912
+ if (!handledFrame) throw err; // if there's no handler, propagate back to host
800
913
 
801
914
  var h = handledFrame._handlerStack.pop();
802
915
  // Discard any call-frames that were pushed inside the try body.
803
916
  this._frameStack.length = h.frameStackDepth;
804
917
  // Write the caught exception directly into the designated register.
805
- handledFrame.regs[h.exceptionReg] = err;
918
+ this._regs[handledFrame._base + h.exceptionReg] = err;
806
919
  // Jump to the catch block.
807
920
  handledFrame._pc = h.handlerPc;
921
+ this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;
808
922
  this._currentFrame = handledFrame;
809
923
  }
810
924
  }
811
925
  };
812
926
 
813
- // Boot
927
+ /* @BOOT */ // <- This comment can't be removed!
814
928
  var globals: any = {}; // global object for globals
815
929
 
816
930
  // Always pull built-ins from globalThis so eval() scoping can't shadow them