js-confuser-vm 0.0.5 → 0.0.6

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