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