js-confuser-vm 0.0.5 → 0.0.7

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 (47) hide show
  1. package/CHANGELOG.md +112 -2
  2. package/README.MD +249 -106
  3. package/dist/build-runtime.js +22 -3
  4. package/dist/compiler.js +864 -801
  5. package/dist/runtime.js +414 -333
  6. package/dist/transforms/bytecode/aliasedOpcodes.js +134 -0
  7. package/dist/transforms/bytecode/concealConstants.js +31 -0
  8. package/dist/transforms/bytecode/macroOpcodes.js +37 -23
  9. package/dist/transforms/bytecode/microOpcodes.js +236 -0
  10. package/dist/transforms/bytecode/resolveContants.js +69 -12
  11. package/dist/transforms/bytecode/resolveLabels.js +5 -3
  12. package/dist/transforms/bytecode/selfModifying.js +3 -2
  13. package/dist/transforms/bytecode/specializedOpcodes.js +54 -39
  14. package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
  15. package/dist/transforms/runtime/internalVariables.js +202 -0
  16. package/dist/transforms/runtime/macroOpcodes.js +30 -18
  17. package/dist/transforms/runtime/microOpcodes.js +76 -0
  18. package/dist/transforms/runtime/shuffleOpcodes.js +1 -1
  19. package/dist/transforms/runtime/specializedOpcodes.js +36 -29
  20. package/dist/utils/op-utils.js +36 -0
  21. package/dist/utils/random-utils.js +27 -0
  22. package/index.ts +11 -8
  23. package/jest.config.js +12 -0
  24. package/package.json +1 -1
  25. package/src/build-runtime.ts +25 -4
  26. package/src/compiler.ts +2482 -2069
  27. package/src/options.ts +3 -0
  28. package/src/runtime.ts +842 -771
  29. package/src/transforms/bytecode/aliasedOpcodes.ts +148 -0
  30. package/src/transforms/bytecode/concealConstants.ts +52 -0
  31. package/src/transforms/bytecode/macroOpcodes.ts +49 -33
  32. package/src/transforms/bytecode/microOpcodes.ts +291 -0
  33. package/src/transforms/bytecode/resolveContants.ts +82 -18
  34. package/src/transforms/bytecode/resolveLabels.ts +5 -4
  35. package/src/transforms/bytecode/selfModifying.ts +3 -3
  36. package/src/transforms/bytecode/specializedOpcodes.ts +85 -46
  37. package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
  38. package/src/transforms/runtime/internalVariables.ts +270 -0
  39. package/src/transforms/runtime/macroOpcodes.ts +47 -20
  40. package/src/transforms/runtime/microOpcodes.ts +93 -0
  41. package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
  42. package/src/transforms/runtime/specializedOpcodes.ts +56 -46
  43. package/src/types.ts +1 -1
  44. package/src/utils/op-utils.ts +46 -0
  45. package/src/transforms/utils/op-utils.ts +0 -26
  46. package/src/utilts.ts +0 -3
  47. /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
package/dist/runtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { OP_ORIGINAL as OP } from "./compiler.js";
2
2
  const BYTECODE = [];
3
3
  const MAIN_START_PC = 0;
4
+ const MAIN_REG_COUNT = 0;
4
5
  const CONSTANTS = [];
5
6
  const ENCODE_BYTECODE = false;
6
7
  const TIMING_CHECKS = false;
@@ -24,8 +25,8 @@ function decodeBytecode(s) {
24
25
  var CLOSURE_SYM = Symbol(); // Nameless for obfuscation
25
26
 
26
27
  // Upvalue
27
- // While the outer frame is alive: reads/writes go to frame.locals[slot].
28
- // After the outer frame returns (closed): reads/writes hit this.value.
28
+ // While the outer frame is alive: reads/writes go to frame.regs[slot].
29
+ // After the outer frame returns (closed): reads/writes hit this._value.
29
30
  function Upvalue(frame, slot) {
30
31
  this._frame = frame;
31
32
  this._slot = slot;
@@ -33,13 +34,13 @@ function Upvalue(frame, slot) {
33
34
  this._value = undefined;
34
35
  }
35
36
  Upvalue.prototype._read = function () {
36
- return this._closed ? this._value : this._frame.locals[this._slot];
37
+ return this._closed ? this._value : this._frame.regs[this._slot];
37
38
  };
38
39
  Upvalue.prototype._write = function (v) {
39
- if (this._closed) this._value = v;else this._frame.locals[this._slot] = v;
40
+ if (this._closed) this._value = v;else this._frame.regs[this._slot] = v;
40
41
  };
41
42
  Upvalue.prototype._close = function () {
42
- this._value = this._frame.locals[this._slot];
43
+ this._value = this._frame.regs[this._slot];
43
44
  this._closed = true;
44
45
  };
45
46
 
@@ -47,44 +48,36 @@ Upvalue.prototype._close = function () {
47
48
  function Closure(fn) {
48
49
  this.fn = fn;
49
50
  this.upvalues = [];
50
- this.prototype = {}; // <- default prototype object for \`new\`
51
+ this.prototype = {}; // <- default prototype object for `new`
51
52
  }
52
- function Frame(closure, returnPc, parent, thisVal) {
53
+ function Frame(closure, returnPc, parent, thisVal, retDstReg) {
53
54
  this.closure = closure;
54
- this.locals = new Array(closure.fn.localCount).fill(undefined);
55
+ this.regs = new Array(closure.fn.regCount).fill(undefined);
55
56
  this._pc = closure.fn.startPc; // <- initialize from fn descriptor
56
57
  this._returnPc = returnPc; // pc to resume in parent frame after RETURN
57
58
  this._parent = parent;
58
59
  this.thisVal = thisVal !== undefined ? thisVal : undefined;
60
+ this._retDstReg = retDstReg !== undefined ? retDstReg : 0; // register in parent to write return value
59
61
  this._newObj = null; // <- set by NEW so RETURN can see it
60
62
  this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP
61
63
  }
62
64
 
63
65
  // VM
64
- function VM(bytecode, mainStartPc, constants, globals) {
66
+ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
65
67
  this.bytecode = bytecode;
66
68
  this.constants = constants;
67
69
  this.globals = globals;
68
- this._stack = [];
69
70
  this._frameStack = [];
70
71
  this._openUpvalues = []; // all currently open Upvalue objects across all frames
71
72
 
72
73
  var mainFn = {
73
74
  paramCount: 0,
74
- localCount: 0,
75
+ regCount: mainRegCount,
75
76
  startPc: mainStartPc // <- where main begins
76
77
  };
77
- this._currentFrame = new Frame(new Closure(mainFn), null, null);
78
+ this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0);
79
+ this._internals = {};
78
80
  }
79
- VM.prototype._push = function (v) {
80
- this._stack.push(v);
81
- };
82
- VM.prototype._pop = function () {
83
- return this._stack.pop();
84
- };
85
- VM.prototype.peek = function () {
86
- return this._stack[this._stack.length - 1];
87
- };
88
81
 
89
82
  // Consume the next slot from the flat bytecode stream and advance the PC.
90
83
  // Called by opcode handlers to read each of their operands in order.
@@ -102,6 +95,33 @@ VM.prototype.captureUpvalue = function (frame, slot) {
102
95
  this._openUpvalues.push(uv);
103
96
  return uv;
104
97
  };
98
+
99
+ // Reads and decodes a constant from the pool.
100
+ // idx — pool index (first operand of the constant pair emitted by resolveConstants).
101
+ // key — conceal key (second operand). 0 means no concealment.
102
+ //
103
+ // For integers: stored value is (original ^ key); XOR again to recover.
104
+ // For strings: stored value is a base64 string containing u16 LE byte pairs.
105
+ // Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with
106
+ // (key + i) & 0xFFFF to recover the original char codes.
107
+ // idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.
108
+ VM.prototype._constant = function (idxIn, keyIn) {
109
+ var idx = idxIn ?? this._operand();
110
+ var key = keyIn ?? this._operand();
111
+ var v = this.constants[idx];
112
+ if (!key) return v;
113
+ if (typeof v === "number") return v ^ key;
114
+ // 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
+ });
118
+ var out = "";
119
+ for (var i = 0; i < b.length / 2; i++) {
120
+ var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE
121
+ out += String.fromCharCode(code ^ key + i & 0xffff);
122
+ }
123
+ return out;
124
+ };
105
125
  VM.prototype._closeUpvaluesFor = function (frame) {
106
126
  // Called on RETURN - close every upvalue that was pointing into this frame.
107
127
  // After this, closures that captured from the frame read from upvalue.value.
@@ -122,9 +142,14 @@ VM.prototype.run = function () {
122
142
  var frame = this._currentFrame;
123
143
  var bc = this.bytecode;
124
144
  if (frame._pc >= bc.length) break;
125
- var op = this.bytecode[frame._pc++];
126
-
127
- // console.log(frame._pc - 1, op);
145
+ var pc = frame._pc++;
146
+ var op = this.bytecode[pc];
147
+ var opcode = this.bytecode[pc];
148
+ // console.log(
149
+ // "pc=" + pc,
150
+ // "opcode=" + opcode,
151
+ // Object.keys(OP).find((key) => OP[key] === opcode),
152
+ // );
128
153
 
129
154
  // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks
130
155
  if (TIMING_CHECKS) {
@@ -135,171 +160,249 @@ VM.prototype.run = function () {
135
160
  // Poison the bytecode
136
161
  for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;
137
162
  // Break the current state
138
- op = OP.POP;
139
- this._stack = [];
163
+ frame.regs.fill(undefined);
164
+ op = OP.JUMP;
165
+ frame._pc = this.bytecode.length; // jump past end to halt
140
166
  }
141
167
  }
142
168
  try {
143
169
  /* @SWITCH */
144
170
  switch (op) {
145
171
  case OP.LOAD_CONST:
146
- this._push(this.constants[this._operand()]);
147
- break;
172
+ {
173
+ var dst = this._operand();
174
+ frame.regs[dst] = this._constant();
175
+ break;
176
+ }
148
177
  case OP.LOAD_INT:
149
- this._push(this._operand());
150
- break;
151
- case OP.LOAD_LOCAL:
152
- this._push(frame.locals[this._operand()]);
153
- break;
154
- case OP.STORE_LOCAL:
155
- frame.locals[this._operand()] = this._pop();
156
- break;
178
+ {
179
+ var dst = this._operand();
180
+ frame.regs[dst] = this._operand();
181
+ break;
182
+ }
157
183
  case OP.LOAD_GLOBAL:
158
- this._push(this.globals[this.constants[this._operand()]]);
159
- break;
184
+ {
185
+ var dst = this._operand();
186
+ var globalName = this._constant();
187
+ if (!(globalName in this.globals)) {
188
+ throw new ReferenceError(`${globalName} is not defined`);
189
+ }
190
+ frame.regs[dst] = this.globals[globalName];
191
+ break;
192
+ }
193
+ case OP.LOAD_UPVALUE:
194
+ {
195
+ var dst = this._operand();
196
+ frame.regs[dst] = frame.closure.upvalues[this._operand()]._read();
197
+ break;
198
+ }
199
+ case OP.LOAD_THIS:
200
+ {
201
+ var dst = this._operand();
202
+ frame.regs[dst] = frame.thisVal;
203
+ break;
204
+ }
205
+ case OP.MOVE:
206
+ {
207
+ var dst = this._operand();
208
+ frame.regs[dst] = frame.regs[this._operand()];
209
+ break;
210
+ }
160
211
  case OP.STORE_GLOBAL:
161
- this.globals[this.constants[this._operand()]] = this._pop();
162
- break;
212
+ {
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()];
216
+ break;
217
+ }
218
+ case OP.STORE_UPVALUE:
219
+ {
220
+ var uvIdx = this._operand();
221
+ frame.closure.upvalues[uvIdx]._write(frame.regs[this._operand()]);
222
+ break;
223
+ }
163
224
  case OP.GET_PROP:
164
225
  {
165
- // Stack: [..., obj, key] -> [..., obj, obj[key]]
166
- // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver
167
- var key = this._pop();
168
- var obj = this.peek();
169
- this._push(obj[key]);
226
+ // dst = regs[obj][regs[key]]
227
+ var dst = this._operand();
228
+ var obj = frame.regs[this._operand()];
229
+ var key = frame.regs[this._operand()];
230
+ frame.regs[dst] = obj[key];
231
+ break;
232
+ }
233
+ case OP.SET_PROP:
234
+ {
235
+ // 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.
241
+ Reflect.set(obj, key, val);
242
+ break;
243
+ }
244
+ case OP.DELETE_PROP:
245
+ {
246
+ 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];
170
250
  break;
171
251
  }
252
+
253
+ // ── Arithmetic (dst, src1, src2) ────────────────────────────────────
172
254
  case OP.ADD:
173
255
  {
174
- var b = this._pop();
175
- this._push(this._pop() + b);
256
+ var dst = this._operand();
257
+ var a = frame.regs[this._operand()];
258
+ frame.regs[dst] = a + frame.regs[this._operand()];
176
259
  break;
177
260
  }
178
261
  case OP.SUB:
179
262
  {
180
- var b = this._pop();
181
- this._push(this._pop() - b);
263
+ var dst = this._operand();
264
+ var a = frame.regs[this._operand()];
265
+ frame.regs[dst] = a - frame.regs[this._operand()];
182
266
  break;
183
267
  }
184
268
  case OP.MUL:
185
269
  {
186
- var b = this._pop();
187
- this._push(this._pop() * b);
270
+ var dst = this._operand();
271
+ var a = frame.regs[this._operand()];
272
+ frame.regs[dst] = a * frame.regs[this._operand()];
188
273
  break;
189
274
  }
190
275
  case OP.DIV:
191
276
  {
192
- var b = this._pop();
193
- this._push(this._pop() / b);
277
+ var dst = this._operand();
278
+ var a = frame.regs[this._operand()];
279
+ frame.regs[dst] = a / frame.regs[this._operand()];
194
280
  break;
195
281
  }
196
282
  case OP.MOD:
197
283
  {
198
- var b = this._pop();
199
- this._push(this._pop() % b);
284
+ var dst = this._operand();
285
+ var a = frame.regs[this._operand()];
286
+ frame.regs[dst] = a % frame.regs[this._operand()];
200
287
  break;
201
288
  }
202
289
  case OP.BAND:
203
290
  {
204
- var b = this._pop();
205
- this._push(this._pop() & b);
291
+ var dst = this._operand();
292
+ var a = frame.regs[this._operand()];
293
+ frame.regs[dst] = a & frame.regs[this._operand()];
206
294
  break;
207
295
  }
208
296
  case OP.BOR:
209
297
  {
210
- var b = this._pop();
211
- this._push(this._pop() | b);
298
+ var dst = this._operand();
299
+ var a = frame.regs[this._operand()];
300
+ frame.regs[dst] = a | frame.regs[this._operand()];
212
301
  break;
213
302
  }
214
303
  case OP.BXOR:
215
304
  {
216
- var b = this._pop();
217
- this._push(this._pop() ^ b);
305
+ var dst = this._operand();
306
+ var a = frame.regs[this._operand()];
307
+ frame.regs[dst] = a ^ frame.regs[this._operand()];
218
308
  break;
219
309
  }
220
310
  case OP.SHL:
221
311
  {
222
- var b = this._pop();
223
- this._push(this._pop() << b);
312
+ var dst = this._operand();
313
+ var a = frame.regs[this._operand()];
314
+ frame.regs[dst] = a << frame.regs[this._operand()];
224
315
  break;
225
316
  }
226
317
  case OP.SHR:
227
318
  {
228
- var b = this._pop();
229
- this._push(this._pop() >> b);
319
+ var dst = this._operand();
320
+ var a = frame.regs[this._operand()];
321
+ frame.regs[dst] = a >> frame.regs[this._operand()];
230
322
  break;
231
323
  }
232
324
  case OP.USHR:
233
325
  {
234
- var b = this._pop();
235
- this._push(this._pop() >>> b);
326
+ var dst = this._operand();
327
+ var a = frame.regs[this._operand()];
328
+ frame.regs[dst] = a >>> frame.regs[this._operand()];
236
329
  break;
237
330
  }
331
+
332
+ // ── Comparison (dst, src1, src2) ─────────────────────────────────────
238
333
  case OP.LT:
239
334
  {
240
- var b = this._pop();
241
- this._push(this._pop() < b);
335
+ var dst = this._operand();
336
+ var a = frame.regs[this._operand()];
337
+ frame.regs[dst] = a < frame.regs[this._operand()];
242
338
  break;
243
339
  }
244
340
  case OP.GT:
245
341
  {
246
- var b = this._pop();
247
- this._push(this._pop() > b);
342
+ var dst = this._operand();
343
+ var a = frame.regs[this._operand()];
344
+ frame.regs[dst] = a > frame.regs[this._operand()];
248
345
  break;
249
346
  }
250
- case OP.EQ:
347
+ case OP.LTE:
251
348
  {
252
- var b = this._pop();
253
- this._push(this._pop() === b);
349
+ var dst = this._operand();
350
+ var a = frame.regs[this._operand()];
351
+ frame.regs[dst] = a <= frame.regs[this._operand()];
254
352
  break;
255
353
  }
256
- case OP.LTE:
354
+ case OP.GTE:
257
355
  {
258
- var b = this._pop();
259
- this._push(this._pop() <= b);
356
+ var dst = this._operand();
357
+ var a = frame.regs[this._operand()];
358
+ frame.regs[dst] = a >= frame.regs[this._operand()];
260
359
  break;
261
360
  }
262
- case OP.GTE:
361
+ case OP.EQ:
263
362
  {
264
- var b = this._pop();
265
- this._push(this._pop() >= b);
363
+ var dst = this._operand();
364
+ var a = frame.regs[this._operand()];
365
+ frame.regs[dst] = a === frame.regs[this._operand()];
266
366
  break;
267
367
  }
268
368
  case OP.NEQ:
269
369
  {
270
- var b = this._pop();
271
- this._push(this._pop() !== b);
370
+ var dst = this._operand();
371
+ var a = frame.regs[this._operand()];
372
+ frame.regs[dst] = a !== frame.regs[this._operand()];
272
373
  break;
273
374
  }
274
375
  case OP.LOOSE_EQ:
275
376
  {
276
- var b = this._pop();
277
- this._push(this._pop() == b);
377
+ var dst = this._operand();
378
+ var a = frame.regs[this._operand()];
379
+ frame.regs[dst] = a == frame.regs[this._operand()];
278
380
  break;
279
381
  }
280
382
  case OP.LOOSE_NEQ:
281
383
  {
282
- var b = this._pop();
283
- this._push(this._pop() != b);
384
+ var dst = this._operand();
385
+ var a = frame.regs[this._operand()];
386
+ frame.regs[dst] = a != frame.regs[this._operand()];
284
387
  break;
285
388
  }
286
389
  case OP.IN:
287
390
  {
288
- var b = this._pop();
289
- this._push(this._pop() in b);
391
+ var dst = this._operand();
392
+ var a = frame.regs[this._operand()];
393
+ frame.regs[dst] = a in frame.regs[this._operand()];
290
394
  break;
291
395
  }
292
396
  case OP.INSTANCEOF:
293
397
  {
294
- var ctor = this._pop();
295
- var obj = this._pop();
398
+ var dst = this._operand();
399
+ var obj = frame.regs[this._operand()];
400
+ var ctor = frame.regs[this._operand()];
296
401
  if (typeof ctor === "function") {
297
- // Native constructor (e.g. Array, Date) - native instanceof is fine
298
- this._push(obj instanceof ctor);
402
+ frame.regs[dst] = obj instanceof ctor;
299
403
  } else {
300
- // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.
301
- // Walk obj's prototype chain looking for identity with ctor.prototype.
302
- var proto = ctor.prototype; // the .prototype property on the Closure
404
+ // VM Closure - walk prototype chain for identity with ctor.prototype.
405
+ var proto = ctor.prototype;
303
406
  var target = Object.getPrototypeOf(obj);
304
407
  var result = false;
305
408
  while (target !== null) {
@@ -309,78 +412,172 @@ VM.prototype.run = function () {
309
412
  }
310
413
  target = Object.getPrototypeOf(target);
311
414
  }
312
- this._push(result);
415
+ frame.regs[dst] = result;
313
416
  }
314
417
  break;
315
418
  }
419
+
420
+ // ── Unary (dst, src) ─────────────────────────────────────────────────
316
421
  case OP.UNARY_NEG:
317
- this._push(-this._pop());
318
- break;
422
+ {
423
+ var dst = this._operand();
424
+ frame.regs[dst] = -frame.regs[this._operand()];
425
+ break;
426
+ }
319
427
  case OP.UNARY_POS:
320
- this._push(this._pop());
321
- break;
428
+ {
429
+ var dst = this._operand();
430
+ frame.regs[dst] = +frame.regs[this._operand()];
431
+ break;
432
+ }
322
433
  case OP.UNARY_NOT:
323
- this._push(!this._pop());
324
- break;
434
+ {
435
+ var dst = this._operand();
436
+ frame.regs[dst] = !frame.regs[this._operand()];
437
+ break;
438
+ }
325
439
  case OP.UNARY_BITNOT:
326
- this._push(~this._pop());
327
- break;
440
+ {
441
+ var dst = this._operand();
442
+ frame.regs[dst] = ~frame.regs[this._operand()];
443
+ break;
444
+ }
328
445
  case OP.TYPEOF:
329
- this._push(typeof this._pop());
330
- break;
446
+ {
447
+ var dst = this._operand();
448
+ frame.regs[dst] = typeof frame.regs[this._operand()];
449
+ break;
450
+ }
331
451
  case OP.VOID:
332
- this._pop();
333
- this._push(undefined);
334
- break;
452
+ {
453
+ var dst = this._operand();
454
+ this._operand(); // consume src — evaluated for side-effects by compiler
455
+ frame.regs[dst] = undefined;
456
+ break;
457
+ }
335
458
  case OP.TYPEOF_SAFE:
336
459
  {
337
- // operand is a const index holding the variable name string.
338
- // Mimics JS semantics: typeof undeclaredVar === "undefined" (no throw).
339
- var name = this._pop(); // LOAD_CONST pushed the name - consume it
460
+ // dst, nameConstIdx safe typeof for potentially-undeclared globals.
461
+ var dst = this._operand();
462
+ var name = this._constant();
340
463
  var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined;
341
- this._push(typeof val);
464
+ frame.regs[dst] = typeof val;
342
465
  break;
343
466
  }
467
+
468
+ // ── Control flow ──────────────────────────────────────────────────────
344
469
  case OP.JUMP:
345
470
  frame._pc = this._operand();
346
471
  break;
347
472
  case OP.JUMP_IF_FALSE:
348
473
  {
474
+ var src = this._operand();
349
475
  var target = this._operand();
350
- if (!this._pop()) frame._pc = target;
476
+ if (!frame.regs[src]) frame._pc = target;
351
477
  break;
352
478
  }
353
- case OP.JUMP_IF_TRUE_OR_POP:
479
+ case OP.JUMP_IF_TRUE:
354
480
  {
355
- // || semantics: if truthy, we're done - leave value, jump over RHS.
356
- // If falsy, discard it and fall through to evaluate RHS.
481
+ // || short-circuit: if truthy, jump over RHS.
482
+ var src = this._operand();
357
483
  var target = this._operand();
358
- if (this.peek()) {
359
- frame._pc = target;
484
+ if (frame.regs[src]) frame._pc = target;
485
+ break;
486
+ }
487
+
488
+ // ── Calls ─────────────────────────────────────────────────────────────
489
+ case OP.CALL:
490
+ {
491
+ // dst, calleeReg, argc, [argReg...]
492
+ var dst = this._operand();
493
+ var callee = frame.regs[this._operand()];
494
+ var argc = this._operand();
495
+ var args = new Array(argc);
496
+ for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];
497
+ 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;
502
+ this._frameStack.push(this._currentFrame);
503
+ this._currentFrame = f;
360
504
  } else {
361
- this._pop();
505
+ frame.regs[dst] = callee.apply(null, args);
362
506
  }
363
507
  break;
364
508
  }
365
- case OP.JUMP_IF_FALSE_OR_POP:
509
+ case OP.CALL_METHOD:
366
510
  {
367
- // && semantics: if falsy, we're done - leave value, jump over RHS.
368
- // If truthy, discard it and fall through to evaluate RHS.
369
- var target = this._operand();
370
- if (!this.peek()) {
371
- frame._pc = target;
511
+ // dst, receiverReg, calleeReg, argc, [argReg...]
512
+ var dst = this._operand();
513
+ var receiver = frame.regs[this._operand()];
514
+ var callee = frame.regs[this._operand()];
515
+ var argc = this._operand();
516
+ var args = new Array(argc);
517
+ for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];
518
+ 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;
523
+ this._frameStack.push(this._currentFrame);
524
+ this._currentFrame = f;
525
+ } else {
526
+ frame.regs[dst] = callee.apply(receiver, args);
527
+ }
528
+ break;
529
+ }
530
+ case OP.NEW:
531
+ {
532
+ // dst, calleeReg, argc, [argReg...]
533
+ var dst = this._operand();
534
+ var callee = frame.regs[this._operand()];
535
+ var argc = this._operand();
536
+ var args = new Array(argc);
537
+ for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];
538
+ 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);
542
+ 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
+ this._frameStack.push(this._currentFrame);
546
+ this._currentFrame = f;
372
547
  } else {
373
- this._pop();
548
+ // Reflect.construct is required - Object.create+apply does NOT set
549
+ // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.
550
+ frame.regs[dst] = Reflect.construct(callee, args);
551
+ }
552
+ break;
553
+ }
554
+ case OP.RETURN:
555
+ {
556
+ var retVal = frame.regs[this._operand()];
557
+ this._closeUpvaluesFor(frame); // must happen before frame is abandoned
558
+
559
+ if (this._frameStack.length === 0) return retVal; // main script returning
560
+
561
+ // new-call rule: primitive return -> discard, use the constructed object instead
562
+ if (frame._newObj !== null) {
563
+ if (typeof retVal !== "object" || retVal === null) retVal = frame._newObj;
374
564
  }
565
+ var parentFrame = this._frameStack.pop();
566
+ parentFrame.regs[frame._retDstReg] = retVal;
567
+ this._currentFrame = parentFrame;
375
568
  break;
376
569
  }
570
+ case OP.THROW:
571
+ throw frame.regs[this._operand()];
572
+
573
+ // ── Closures ──────────────────────────────────────────────────────────
377
574
  case OP.MAKE_CLOSURE:
378
575
  {
379
- // Inline operands: startPc, paramCount, localCount, uvCount,
380
- // [isLocal_0, idx_0, isLocal_1, idx_1, ...]
576
+ // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]
577
+ var dst = this._operand();
381
578
  var startPc = this._operand();
382
579
  var paramCount = this._operand();
383
- var localCount = this._operand();
580
+ var regCount = this._operand();
384
581
  var uvCount = this._operand();
385
582
  var uvDescs = new Array(uvCount);
386
583
  for (var i = 0; i < uvCount; i++) {
@@ -393,7 +590,7 @@ VM.prototype.run = function () {
393
590
  }
394
591
  var fn = {
395
592
  paramCount: paramCount,
396
- localCount: localCount,
593
+ regCount: regCount,
397
594
  startPc: startPc,
398
595
  upvalueDescriptors: uvDescs
399
596
  };
@@ -401,13 +598,12 @@ VM.prototype.run = function () {
401
598
  for (var i = 0; i < uvDescs.length; i++) {
402
599
  var uvd = uvDescs[i];
403
600
  if (uvd.isLocal) {
404
- // Capture directly from current frame's local slot
405
601
  closure.upvalues.push(this.captureUpvalue(frame, uvd._index));
406
602
  } else {
407
- // Relay - take upvalue from the enclosing closure's list
408
603
  closure.upvalues.push(frame.closure.upvalues[uvd._index]);
409
604
  }
410
605
  }
606
+
411
607
  // Wrap in a native callable shell so host code (array methods,
412
608
  // test assertions, setTimeout, etc.) can invoke VM closures.
413
609
  // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.
@@ -415,167 +611,90 @@ VM.prototype.run = function () {
415
611
  var shell = function (c) {
416
612
  return function () {
417
613
  var args = Array.prototype.slice.call(arguments);
418
- var sub = new VM(self.bytecode, 0, self.constants, self.globals);
419
- // Sloppy-mode: null/undefined thisArg global object
420
- var f = new Frame(c, null, null, this == null ? self.globals : this);
421
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
422
- f.locals[c.fn.paramCount] = args;
614
+ 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;
423
618
  sub._currentFrame = f;
424
619
  return sub.run();
425
620
  };
426
621
  }(closure);
427
622
  shell[CLOSURE_SYM] = closure;
428
623
  shell.prototype = closure.prototype; // unified prototype for new/instanceof
429
- this._push(shell);
624
+ frame.regs[dst] = shell;
430
625
  break;
431
626
  }
432
- case OP.LOAD_UPVALUE:
433
- this._push(frame.closure.upvalues[this._operand()]._read());
434
- break;
435
- case OP.STORE_UPVALUE:
436
- frame.closure.upvalues[this._operand()]._write(this._pop());
437
- break;
627
+
628
+ // ── Collections ───────────────────────────────────────────────────────
438
629
  case OP.BUILD_ARRAY:
439
630
  {
440
- var elems = this._stack.splice(this._stack.length - this._operand());
441
- this._push(elems);
631
+ // dst, count, [elemReg...]
632
+ var dst = this._operand();
633
+ var count = this._operand();
634
+ var elems = new Array(count);
635
+ for (var i = 0; i < count; i++) elems[i] = frame.regs[this._operand()];
636
+ frame.regs[dst] = elems;
442
637
  break;
443
638
  }
444
639
  case OP.BUILD_OBJECT:
445
640
  {
446
- // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)
447
- // Pop all pairs and build the object.
448
- var pairs = this._stack.splice(this._stack.length - this._operand() * 2);
641
+ // dst, pairCount, [keyReg, valReg, ...]
642
+ var dst = this._operand();
643
+ var pairCount = this._operand();
449
644
  var o = {};
450
- for (var i = 0; i < pairs.length; i += 2) {
451
- o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd
452
- }
453
- this._push(o);
454
- break;
455
- }
456
- case OP.SET_PROP:
457
- {
458
- // Stack: [..., obj, key, val]
459
- // Leaves val on stack - assignment is an expression in JS.
460
- var val = this._pop();
461
- var key = this._pop();
462
- var obj = this._pop();
463
- // Reflect.set performs [[Set]] without throwing on failure,
464
- // correctly simulating sloppy-mode assignment from a strict-mode host
465
- // (output.js is an ES module). This also properly invokes inherited
466
- // or prototype-chain setter functions.
467
- Reflect.set(obj, key, val);
468
- this._push(val); // assignment expression evaluates to the assigned value
469
- break;
470
- }
471
- case OP.GET_PROP_COMPUTED:
472
- {
473
- // Stack: [..., obj, key] - key is a runtime value (nums[i])
474
- // Mirrors GET_PROP but pops the key that was pushed dynamically.
475
- var key = this._pop();
476
- var obj = this._pop();
477
- this._push(obj[key]);
478
- break;
479
- }
480
- case OP.DELETE_PROP:
481
- {
482
- var key = this._pop();
483
- var obj = this._pop();
484
- this._push(delete obj[key]);
485
- break;
486
- }
487
- case OP.CALL:
488
- {
489
- var args = this._stack.splice(this._stack.length - this._operand());
490
- var callee = this._pop();
491
- if (callee && callee[CLOSURE_SYM]) {
492
- // VM closure - run directly in this VM, no sub-VM overhead
493
- var c = callee[CLOSURE_SYM];
494
- // Sloppy-mode: plain function call → global object as this
495
- var f = new Frame(c, frame._pc, frame, this.globals);
496
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
497
- f.locals[c.fn.paramCount] = args;
498
- this._frameStack.push(this._currentFrame);
499
- this._currentFrame = f;
500
- } else {
501
- // Native function
502
- this._push(callee.apply(null, args));
645
+ for (var i = 0; i < pairCount; i++) {
646
+ var key = frame.regs[this._operand()];
647
+ var val = frame.regs[this._operand()];
648
+ o[key] = val;
503
649
  }
650
+ frame.regs[dst] = o;
504
651
  break;
505
652
  }
506
- case OP.CALL_METHOD:
653
+
654
+ // ── Property definitions (getters / setters) ──────────────────────────
655
+ case OP.DEFINE_GETTER:
507
656
  {
508
- var args = this._stack.splice(this._stack.length - this._operand());
509
- var callee = this._pop();
510
- var receiver = this._pop(); // left on stack by GET_PROP
511
- if (callee && callee[CLOSURE_SYM]) {
512
- // VM closure - run directly in this VM with receiver as this
513
- var c = callee[CLOSURE_SYM];
514
- var f = new Frame(c, frame._pc, frame, receiver);
515
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
516
- f.locals[c.fn.paramCount] = args;
517
- this._frameStack.push(this._currentFrame);
518
- this._currentFrame = f;
519
- } else {
520
- // Native method
521
- this._push(callee.apply(receiver, args));
657
+ // obj, key, fn
658
+ var obj = frame.regs[this._operand()];
659
+ var key = frame.regs[this._operand()];
660
+ var getterFn = frame.regs[this._operand()];
661
+ var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
662
+ var getDesc = {
663
+ get: getterFn,
664
+ configurable: true,
665
+ enumerable: true
666
+ };
667
+ if (existingDesc && typeof existingDesc.set === "function") {
668
+ getDesc.set = existingDesc.set;
522
669
  }
670
+ Object.defineProperty(obj, key, getDesc);
523
671
  break;
524
672
  }
525
- case OP.LOAD_THIS:
526
- this._push(frame.thisVal);
527
- break;
528
- case OP.NEW:
673
+ case OP.DEFINE_SETTER:
529
674
  {
530
- var args = this._stack.splice(this._stack.length - this._operand());
531
- var callee = this._pop();
532
- if (callee && callee[CLOSURE_SYM]) {
533
- // VM closure constructor - prototype is unified via shell.prototype = closure.prototype
534
- var c = callee[CLOSURE_SYM];
535
- var newObj = Object.create(c.prototype || null);
536
- var f = new Frame(c, frame._pc, frame, newObj);
537
- f._newObj = newObj;
538
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
539
- f.locals[c.fn.paramCount] = args;
540
- this._frameStack.push(this._currentFrame);
541
- this._currentFrame = f;
542
- } else {
543
- // Native constructor (e.g. new Error(), new Date()).
544
- // Reflect.construct is required - Object.create+apply does NOT set
545
- // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.
546
- this._push(Reflect.construct(callee, args));
675
+ // obj, key, fn
676
+ var obj = frame.regs[this._operand()];
677
+ var key = frame.regs[this._operand()];
678
+ var setterFn = frame.regs[this._operand()];
679
+ var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
680
+ var setDesc = {
681
+ set: setterFn,
682
+ configurable: true,
683
+ enumerable: true
684
+ };
685
+ if (existingDesc && typeof existingDesc.get === "function") {
686
+ setDesc.get = existingDesc.get;
547
687
  }
688
+ Object.defineProperty(obj, key, setDesc);
548
689
  break;
549
690
  }
550
- case OP.RETURN:
551
- {
552
- var retVal = this._pop();
553
- this._closeUpvaluesFor(frame); // must happen before frame is abandoned
554
- if (this._frameStack.length === 0) return retVal;
555
691
 
556
- // new-call rule: primitive return -> discard, use the constructed object instead
557
- if (frame._newObj !== null) {
558
- if (typeof retVal !== "object" || retVal === null) retVal = frame._newObj;
559
- }
560
- this._currentFrame = this._frameStack.pop();
561
- this._push(retVal);
562
- break;
563
- }
564
- case OP.POP:
565
- this._pop();
566
- break;
567
- case OP.DUP:
568
- this._push(this.peek());
569
- break;
570
- case OP.THROW:
571
- throw this._pop();
692
+ // ── For-in iteration ──────────────────────────────────────────────────
572
693
  case OP.FOR_IN_SETUP:
573
694
  {
574
- // Pop the object; build an ordered list of all enumerable own+inherited
575
- // string keys by walking the prototype chain manually.
576
- // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,
577
- // so we never rely on Object.keys() and we handle inheritance correctly.
578
- var obj = this._pop();
695
+ // dst, src build iterator object from enumerable keys of regs[src]
696
+ var dst = this._operand();
697
+ var obj = frame.regs[this._operand()];
579
698
  var keys = [];
580
699
  if (obj !== null && obj !== undefined) {
581
700
  var seen = Object.create(null);
@@ -595,45 +714,34 @@ VM.prototype.run = function () {
595
714
  cur = Object.getPrototypeOf(cur);
596
715
  }
597
716
  }
598
- this._push({
717
+ frame.regs[dst] = {
599
718
  _keys: keys,
600
719
  i: 0
601
- });
720
+ };
602
721
  break;
603
722
  }
604
723
  case OP.FOR_IN_NEXT:
605
724
  {
606
- // Operand = jump target for the done case. Must be read before the
607
- // conditional so the PC stays correctly aligned either way.
608
- var target = this._operand();
609
- var iter = this._pop();
725
+ // dst, iterReg, exitTarget
726
+ // Advances iterator; writes next key to dst, or jumps to exitTarget when done.
727
+ var dst = this._operand();
728
+ var iter = frame.regs[this._operand()];
729
+ var exitTarget = this._operand();
610
730
  if (iter.i >= iter._keys.length) {
611
- frame._pc = target;
731
+ frame._pc = exitTarget;
612
732
  } else {
613
- this._push(iter._keys[iter.i++]);
614
- }
615
- break;
616
- }
617
- case OP.PATCH:
618
- {
619
- // Inline operands: destPc, sliceStart, sliceEnd
620
- // Copies bytecode[sliceStart..sliceEnd) flat u16 slots to destPc.
621
- var destPc = this._operand();
622
- var sliceStart = this._operand();
623
- var sliceEnd = this._operand();
624
- for (var pi = sliceStart; pi < sliceEnd; pi++) {
625
- this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];
733
+ frame.regs[dst] = iter._keys[iter.i++];
626
734
  }
627
735
  break;
628
736
  }
737
+
738
+ // ── Exception handling ────────────────────────────────────────────────
629
739
  case OP.TRY_SETUP:
630
740
  {
631
- // Push an exception handler record onto the current frame.
632
- // Saves: catch PC (operand), current stack depth, current frame-stack depth.
633
- // If an exception is thrown before TRY_END fires, the VM jumps here.
741
+ // handlerPc, exceptionReg — push exception handler record onto current frame.
634
742
  frame._handlerStack.push({
635
743
  handlerPc: this._operand(),
636
- stackDepth: this._stack.length,
744
+ exceptionReg: this._operand(),
637
745
  frameStackDepth: this._frameStack.length
638
746
  });
639
747
  break;
@@ -644,44 +752,17 @@ VM.prototype.run = function () {
644
752
  frame._handlerStack.pop();
645
753
  break;
646
754
  }
647
- case OP.DEFINE_GETTER:
648
- {
649
- // Stack: [..., obj, key, getterFn]
650
- // Pops all three; defines an enumerable, configurable getter on obj.
651
- // If a setter was already defined for this key, it is preserved.
652
- var getterFn = this._pop();
653
- var key = this._pop();
654
- var obj = this._pop();
655
- var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
656
- var getDesc = {
657
- get: getterFn,
658
- configurable: true,
659
- enumerable: true
660
- };
661
- if (existingDesc && typeof existingDesc.set === "function") {
662
- getDesc.set = existingDesc.set;
663
- }
664
- Object.defineProperty(obj, key, getDesc);
665
- break;
666
- }
667
- case OP.DEFINE_SETTER:
755
+
756
+ // ── Self-modifying bytecode ───────────────────────────────────────────
757
+ case OP.PATCH:
668
758
  {
669
- // Stack: [..., obj, key, setterFn]
670
- // Pops all three; defines an enumerable, configurable setter on obj.
671
- // If a getter was already defined for this key, it is preserved.
672
- var setterFn = this._pop();
673
- var key = this._pop();
674
- var obj = this._pop();
675
- var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
676
- var setDesc = {
677
- set: setterFn,
678
- configurable: true,
679
- enumerable: true
680
- };
681
- if (existingDesc && typeof existingDesc.get === "function") {
682
- setDesc.get = existingDesc.get;
759
+ // destPc, sliceStart, sliceEnd
760
+ var destPc = this._operand();
761
+ var sliceStart = this._operand();
762
+ var sliceEnd = this._operand();
763
+ for (var pi = sliceStart; pi < sliceEnd; pi++) {
764
+ this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];
683
765
  }
684
- Object.defineProperty(obj, key, setDesc);
685
766
  break;
686
767
  }
687
768
  case OP.DEBUGGER:
@@ -713,13 +794,10 @@ VM.prototype.run = function () {
713
794
  if (!handledFrame) throw err; // no handler anywhere — propagate to host
714
795
 
715
796
  var h = handledFrame._handlerStack.pop();
716
- // Restore the VM value stack to the depth recorded at TRY_SETUP time,
717
- // then push the caught exception so the catch binding can store it.
718
- this._stack.length = h.stackDepth;
719
- this._push(err);
720
- // Discard any call-frames that were pushed inside the try body
721
- // (functions called from within the try block that are still live).
797
+ // Discard any call-frames that were pushed inside the try body.
722
798
  this._frameStack.length = h.frameStackDepth;
799
+ // Write the caught exception directly into the designated register.
800
+ handledFrame.regs[h.exceptionReg] = err;
723
801
  // Jump to the catch block.
724
802
  handledFrame._pc = h.handlerPc;
725
803
  this._currentFrame = handledFrame;
@@ -738,12 +816,15 @@ for (var k of Object.getOwnPropertyNames(globalThis)) {
738
816
  // If a window object is in scope (browser or test harness), capture it
739
817
  // explicitly so VM code can read/write window.TEST_OUTPUT etc.
740
818
  if (typeof window !== "undefined") {
741
- globals["window"] = window;
819
+ globals.window = window;
820
+ for (var k of Object.getOwnPropertyNames(window)) {
821
+ globals[k] = window[k];
822
+ }
742
823
  }
743
824
 
744
825
  // Transfer common primitives
745
826
  globals.undefined = undefined;
746
827
  globals.Infinity = Infinity;
747
828
  globals.NaN = NaN;
748
- var vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, CONSTANTS, globals);
829
+ var vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, MAIN_REG_COUNT, CONSTANTS, globals);
749
830
  vm.run();