js-confuser-vm 0.0.1 → 0.0.3

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.
package/src/runtime.ts CHANGED
@@ -1,13 +1,14 @@
1
- import { OP } from "./compiler.ts";
1
+ import { OP_ORIGINAL as OP } from "./compiler.ts";
2
2
  const BYTECODE = [];
3
3
  const MAIN_START_PC = 0;
4
4
  const CONSTANTS = [];
5
- const PACK = false;
5
+ const ENCODE_BYTECODE = false;
6
+ const TIMING_CHECKS = false;
6
7
  // The text above is not included in the compiled output - for type intellisense only
7
8
  // @START
8
9
 
9
10
  function decodeBytecode(s) {
10
- if (!PACK) return s;
11
+ if (!ENCODE_BYTECODE) return s;
11
12
 
12
13
  var b =
13
14
  typeof Buffer !== "undefined"
@@ -25,64 +26,65 @@ function decodeBytecode(s) {
25
26
  return r;
26
27
  }
27
28
 
28
- // ── Closure symbol ────────────────────────────────────────────────
29
+ // Closure symbol
29
30
  // Used to tag shell functions so the VM can fast-path back to the
30
31
  // inner Closure instead of going through a sub-VM on internal calls.
31
32
  var CLOSURE_SYM = Symbol(); // Nameless for obfuscation
32
33
 
33
- // ── Upvalue ───────────────────────────────────────────────────────
34
+ // Upvalue
34
35
  // While the outer frame is alive: reads/writes go to frame.locals[slot].
35
36
  // After the outer frame returns (closed): reads/writes hit this.value.
36
37
  function Upvalue(frame, slot) {
37
- this.frame = frame;
38
- this.slot = slot;
38
+ this._frame = frame;
39
+ this._slot = slot;
39
40
  this._closed = false;
40
41
  this._value = undefined;
41
42
  }
42
- Upvalue.prototype.read = function () {
43
- return this._closed ? this._value : this.frame.locals[this.slot];
43
+ Upvalue.prototype._read = function () {
44
+ return this._closed ? this._value : this._frame.locals[this._slot];
44
45
  };
45
- Upvalue.prototype.write = function (v) {
46
+ Upvalue.prototype._write = function (v) {
46
47
  if (this._closed) this._value = v;
47
- else this.frame.locals[this.slot] = v;
48
+ else this._frame.locals[this._slot] = v;
48
49
  };
49
- Upvalue.prototype.close = function () {
50
- this._value = this.frame.locals[this.slot];
50
+ Upvalue.prototype._close = function () {
51
+ this._value = this._frame.locals[this._slot];
51
52
  this._closed = true;
52
53
  };
53
54
 
54
- // ── Closure & Frame ───────────────────────────────────────────────
55
+ // Closure & Frame
55
56
  function Closure(fn) {
56
57
  this.fn = fn;
57
58
  this.upvalues = [];
58
- this.prototype = {}; // default prototype object for \`new\`
59
+ this.prototype = {}; // <- default prototype object for \`new\`
59
60
  }
60
61
 
61
62
  function Frame(closure, returnPc, parent, thisVal?) {
62
63
  this.closure = closure;
63
64
  this.locals = new Array(closure.fn.localCount).fill(undefined);
64
- this.pc = closure.fn.startPc; // initialize from fn descriptor
65
- this.returnPc = returnPc; // pc to resume in parent frame after RETURN
66
- this.parent = parent;
65
+ this._pc = closure.fn.startPc; // <- initialize from fn descriptor
66
+ this._returnPc = returnPc; // pc to resume in parent frame after RETURN
67
+ this._parent = parent;
67
68
  this.thisVal = thisVal !== undefined ? thisVal : undefined;
68
- this._newObj = null; // set by NEW so RETURN can see it
69
+ this._newObj = null; // <- set by NEW so RETURN can see it
70
+ this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP
69
71
  }
70
72
 
71
- // ── VM ────────────────────────────────────────────────────────────
73
+ // VM
72
74
  function VM(bytecode, mainStartPc, constants, globals) {
73
75
  this.bytecode = bytecode;
74
76
  this.constants = constants;
75
77
  this.globals = globals;
76
78
  this._stack = [];
77
- this.frameStack = [];
78
- this.openUpvalues = []; // all currently open Upvalue objects across all frames
79
+ this._frameStack = [];
80
+ this._openUpvalues = []; // all currently open Upvalue objects across all frames
79
81
 
80
82
  var mainFn = {
81
83
  paramCount: 0,
82
84
  localCount: 0,
83
- startPc: mainStartPc, // where main begins
85
+ startPc: mainStartPc, // <- where main begins
84
86
  };
85
- this.currentFrame = new Frame(new Closure(mainFn), null, null);
87
+ this._currentFrame = new Frame(new Closure(mainFn), null, null);
86
88
  }
87
89
 
88
90
  VM.prototype._push = function (v) {
@@ -95,24 +97,36 @@ VM.prototype.peek = function () {
95
97
  return this._stack[this._stack.length - 1];
96
98
  };
97
99
 
100
+ // Read one instruction word from this.bytecode at `pc`, unwrapping the
101
+ // encoding so callers always get a plain { op, operand } pair regardless
102
+ // of whether ENCODE_BYTECODE is active.
103
+ VM.prototype.readWord = function (pc) {
104
+ var word = this.bytecode[pc];
105
+ if (ENCODE_BYTECODE) {
106
+ return { op: word & 0xff, operand: word >>> 8 };
107
+ } else {
108
+ return { op: word[0], operand: word[1] };
109
+ }
110
+ };
111
+
98
112
  VM.prototype.captureUpvalue = function (frame, slot) {
99
113
  // Reuse existing open upvalue for this frame+slot if one exists.
100
114
  // This is what makes two closures share the same mutable cell.
101
- for (var i = 0; i < this.openUpvalues.length; i++) {
102
- var uv = this.openUpvalues[i];
103
- if (uv.frame === frame && uv.slot === slot) return uv;
115
+ for (var i = 0; i < this._openUpvalues.length; i++) {
116
+ var uv = this._openUpvalues[i];
117
+ if (uv._frame === frame && uv._slot === slot) return uv;
104
118
  }
105
119
  var uv = new Upvalue(frame, slot);
106
- this.openUpvalues.push(uv);
120
+ this._openUpvalues.push(uv);
107
121
  return uv;
108
122
  };
109
123
 
110
- VM.prototype.closeUpvaluesFor = function (frame) {
111
- // Called on RETURN close every upvalue that was pointing into this frame.
124
+ VM.prototype._closeUpvaluesFor = function (frame) {
125
+ // Called on RETURN - close every upvalue that was pointing into this frame.
112
126
  // After this, closures that captured from the frame read from upvalue.value.
113
- this.openUpvalues = this.openUpvalues.filter(function (uv) {
114
- if (uv.frame === frame) {
115
- uv.close();
127
+ this._openUpvalues = this._openUpvalues.filter(function (uv) {
128
+ if (uv._frame === frame) {
129
+ uv._close();
116
130
  return false;
117
131
  }
118
132
  return true;
@@ -127,488 +141,622 @@ VM.prototype.run = function () {
127
141
  var t = now();
128
142
 
129
143
  while (true) {
130
- var frame = this.currentFrame;
144
+ var frame = this._currentFrame;
131
145
  var bc = this.bytecode;
132
- if (frame.pc >= bc.length) break;
146
+ if (frame._pc >= bc.length) break;
133
147
 
134
148
  var op, operand;
135
- var word = bc[frame.pc++];
136
-
137
- if (PACK) {
138
- op = word & 0xff;
139
- operand = word >>> 8;
140
- } else {
141
- op = word[0];
142
- operand = word[1];
143
- }
149
+ var word = this.readWord(frame._pc++);
144
150
 
145
- // console.log(frame.pc - 1, op, operand);
151
+ op = word.op;
152
+ operand = word.operand;
153
+
154
+ // console.log(frame._pc - 1, op, operand);
146
155
 
147
156
  // Debugging protection
148
- var t2 = now();
149
- var isTamper = t2 - t > 1000;
150
- t = t2;
151
- if (isTamper) {
152
- op = OP.RETURN;
157
+ if (TIMING_CHECKS) {
158
+ var t2 = now();
159
+ var isTamper = t2 - t > 1000;
160
+ t = t2;
161
+ if (isTamper) {
162
+ op = OP.POP;
163
+ }
153
164
  }
154
165
 
155
- /* @SWITCH */
156
- switch (op) {
157
- case OP.LOAD_CONST:
158
- this._push(this.constants[operand]);
159
- break;
160
-
161
- case OP.LOAD_LOCAL:
162
- this._push(frame.locals[operand]);
163
- break;
164
-
165
- case OP.STORE_LOCAL:
166
- frame.locals[operand] = this._pop();
167
- break;
168
-
169
- case OP.LOAD_GLOBAL:
170
- this._push(this.globals[this.constants[operand]]);
171
- break;
172
-
173
- case OP.STORE_GLOBAL:
174
- this.globals[this.constants[operand]] = this._pop();
175
- break;
176
-
177
- case OP.GET_PROP: {
178
- // Stack: [..., obj, key] → [..., obj, obj[key]]
179
- // obj is PEEKED (not popped) — CALL_METHOD needs it as receiver
180
- var key = this._pop();
181
- var obj = this.peek();
182
- this._push(obj[key]);
183
- break;
184
- }
166
+ try {
167
+ /* @SWITCH */
168
+ switch (op) {
169
+ case OP.LOAD_CONST:
170
+ this._push(this.constants[operand]);
171
+ break;
172
+
173
+ case OP.LOAD_INT:
174
+ this._push(operand);
175
+ break;
176
+
177
+ case OP.LOAD_LOCAL:
178
+ this._push(frame.locals[operand]);
179
+ break;
180
+
181
+ case OP.STORE_LOCAL:
182
+ frame.locals[operand] = this._pop();
183
+ break;
184
+
185
+ case OP.LOAD_GLOBAL:
186
+ this._push(this.globals[this.constants[operand]]);
187
+ break;
188
+
189
+ case OP.STORE_GLOBAL:
190
+ this.globals[this.constants[operand]] = this._pop();
191
+ break;
192
+
193
+ case OP.GET_PROP: {
194
+ // Stack: [..., obj, key] -> [..., obj, obj[key]]
195
+ // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver
196
+ var key = this._pop();
197
+ var obj = this.peek();
198
+ this._push(obj[key]);
199
+ break;
200
+ }
185
201
 
186
- case OP.ADD: {
187
- var b = this._pop();
188
- this._push(this._pop() + b);
189
- break;
190
- }
191
- case OP.SUB: {
192
- var b = this._pop();
193
- this._push(this._pop() - b);
194
- break;
195
- }
196
- case OP.MUL: {
197
- var b = this._pop();
198
- this._push(this._pop() * b);
199
- break;
200
- }
201
- case OP.DIV: {
202
- var b = this._pop();
203
- this._push(this._pop() / b);
204
- break;
205
- }
206
- case OP.MOD: {
207
- var b = this._pop();
208
- this._push(this._pop() % b);
209
- break;
210
- }
211
- case OP.BAND: {
212
- var b = this._pop();
213
- this._push(this._pop() & b);
214
- break;
215
- }
216
- case OP.BOR: {
217
- var b = this._pop();
218
- this._push(this._pop() | b);
219
- break;
220
- }
221
- case OP.BXOR: {
222
- var b = this._pop();
223
- this._push(this._pop() ^ b);
224
- break;
225
- }
226
- case OP.SHL: {
227
- var b = this._pop();
228
- this._push(this._pop() << b);
229
- break;
230
- }
231
- case OP.SHR: {
232
- var b = this._pop();
233
- this._push(this._pop() >> b);
234
- break;
235
- }
236
- case OP.USHR: {
237
- var b = this._pop();
238
- this._push(this._pop() >>> b);
239
- break;
240
- }
202
+ case OP.ADD: {
203
+ var b = this._pop();
204
+ this._push(this._pop() + b);
205
+ break;
206
+ }
207
+ case OP.SUB: {
208
+ var b = this._pop();
209
+ this._push(this._pop() - b);
210
+ break;
211
+ }
212
+ case OP.MUL: {
213
+ var b = this._pop();
214
+ this._push(this._pop() * b);
215
+ break;
216
+ }
217
+ case OP.DIV: {
218
+ var b = this._pop();
219
+ this._push(this._pop() / b);
220
+ break;
221
+ }
222
+ case OP.MOD: {
223
+ var b = this._pop();
224
+ this._push(this._pop() % b);
225
+ break;
226
+ }
227
+ case OP.BAND: {
228
+ var b = this._pop();
229
+ this._push(this._pop() & b);
230
+ break;
231
+ }
232
+ case OP.BOR: {
233
+ var b = this._pop();
234
+ this._push(this._pop() | b);
235
+ break;
236
+ }
237
+ case OP.BXOR: {
238
+ var b = this._pop();
239
+ this._push(this._pop() ^ b);
240
+ break;
241
+ }
242
+ case OP.SHL: {
243
+ var b = this._pop();
244
+ this._push(this._pop() << b);
245
+ break;
246
+ }
247
+ case OP.SHR: {
248
+ var b = this._pop();
249
+ this._push(this._pop() >> b);
250
+ break;
251
+ }
252
+ case OP.USHR: {
253
+ var b = this._pop();
254
+ this._push(this._pop() >>> b);
255
+ break;
256
+ }
241
257
 
242
- case OP.LT: {
243
- var b = this._pop();
244
- this._push(this._pop() < b);
245
- break;
246
- }
247
- case OP.GT: {
248
- var b = this._pop();
249
- this._push(this._pop() > b);
250
- break;
251
- }
252
- case OP.EQ: {
253
- var b = this._pop();
254
- this._push(this._pop() === b);
255
- break;
256
- }
258
+ case OP.LT: {
259
+ var b = this._pop();
260
+ this._push(this._pop() < b);
261
+ break;
262
+ }
263
+ case OP.GT: {
264
+ var b = this._pop();
265
+ this._push(this._pop() > b);
266
+ break;
267
+ }
268
+ case OP.EQ: {
269
+ var b = this._pop();
270
+ this._push(this._pop() === b);
271
+ break;
272
+ }
257
273
 
258
- case OP.LTE: {
259
- var b = this._pop();
260
- this._push(this._pop() <= b);
261
- break;
262
- }
263
- case OP.GTE: {
264
- var b = this._pop();
265
- this._push(this._pop() >= b);
266
- break;
267
- }
268
- case OP.NEQ: {
269
- var b = this._pop();
270
- this._push(this._pop() !== b);
271
- break;
272
- }
273
- case OP.LOOSE_EQ: {
274
- var b = this._pop();
275
- this._push(this._pop() == b);
276
- break;
277
- }
278
- case OP.LOOSE_NEQ: {
279
- var b = this._pop();
280
- this._push(this._pop() != b);
281
- break;
282
- }
274
+ case OP.LTE: {
275
+ var b = this._pop();
276
+ this._push(this._pop() <= b);
277
+ break;
278
+ }
279
+ case OP.GTE: {
280
+ var b = this._pop();
281
+ this._push(this._pop() >= b);
282
+ break;
283
+ }
284
+ case OP.NEQ: {
285
+ var b = this._pop();
286
+ this._push(this._pop() !== b);
287
+ break;
288
+ }
289
+ case OP.LOOSE_EQ: {
290
+ var b = this._pop();
291
+ this._push(this._pop() == b);
292
+ break;
293
+ }
294
+ case OP.LOOSE_NEQ: {
295
+ var b = this._pop();
296
+ this._push(this._pop() != b);
297
+ break;
298
+ }
283
299
 
284
- case OP.IN: {
285
- var b = this._pop();
286
- this._push(this._pop() in b);
287
- break;
288
- }
300
+ case OP.IN: {
301
+ var b = this._pop();
302
+ this._push(this._pop() in b);
303
+ break;
304
+ }
289
305
 
290
- case OP.INSTANCEOF: {
291
- var ctor = this._pop();
292
- var obj = this._pop();
293
- if (typeof ctor === "function") {
294
- // Native constructor (e.g. Array, Date) native instanceof is fine
295
- this._push(obj instanceof ctor);
296
- } else {
297
- // VM Closure ctor.prototype was set by MAKE_CLOSURE / user assignment.
298
- // Walk obj's prototype chain looking for identity with ctor.prototype.
299
- var proto = ctor.prototype; // the .prototype property on the Closure
300
- var target = Object.getPrototypeOf(obj);
301
- var result = false;
302
- while (target !== null) {
303
- if (target === proto) {
304
- result = true;
305
- break;
306
+ case OP.INSTANCEOF: {
307
+ var ctor = this._pop();
308
+ var obj = this._pop();
309
+ if (typeof ctor === "function") {
310
+ // Native constructor (e.g. Array, Date) - native instanceof is fine
311
+ this._push(obj instanceof ctor);
312
+ } else {
313
+ // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.
314
+ // Walk obj's prototype chain looking for identity with ctor.prototype.
315
+ var proto = ctor.prototype; // the .prototype property on the Closure
316
+ var target = Object.getPrototypeOf(obj);
317
+ var result = false;
318
+ while (target !== null) {
319
+ if (target === proto) {
320
+ result = true;
321
+ break;
322
+ }
323
+ target = Object.getPrototypeOf(target);
306
324
  }
307
- target = Object.getPrototypeOf(target);
325
+ this._push(result);
308
326
  }
309
- this._push(result);
327
+ break;
310
328
  }
311
- break;
312
- }
313
329
 
314
- case OP.UNARY_NEG:
315
- this._push(-this._pop());
316
- break;
317
- case OP.UNARY_POS:
318
- this._push(this._pop());
319
- break;
320
- case OP.UNARY_NOT:
321
- this._push(!this._pop());
322
- break;
323
- case OP.UNARY_BITNOT:
324
- this._push(~this._pop());
325
- break;
326
- case OP.TYPEOF:
327
- this._push(typeof this._pop());
328
- break;
329
- case OP.VOID:
330
- this._pop();
331
- this._push(undefined);
332
- break;
333
-
334
- case OP.TYPEOF_SAFE: {
335
- // operand is a const index holding the variable name string.
336
- // Mimics JS semantics: typeof undeclaredVar === "undefined" (no throw).
337
- var name = this._pop(); // LOAD_CONST pushed the name consume it
338
- var val = Object.prototype.hasOwnProperty.call(this.globals, name)
339
- ? this.globals[name]
340
- : undefined;
341
- this._push(typeof val);
342
- break;
343
- }
330
+ case OP.UNARY_NEG:
331
+ this._push(-this._pop());
332
+ break;
333
+ case OP.UNARY_POS:
334
+ this._push(this._pop());
335
+ break;
336
+ case OP.UNARY_NOT:
337
+ this._push(!this._pop());
338
+ break;
339
+ case OP.UNARY_BITNOT:
340
+ this._push(~this._pop());
341
+ break;
342
+ case OP.TYPEOF:
343
+ this._push(typeof this._pop());
344
+ break;
345
+ case OP.VOID:
346
+ this._pop();
347
+ this._push(undefined);
348
+ break;
349
+
350
+ case OP.TYPEOF_SAFE: {
351
+ // operand is a const index holding the variable name string.
352
+ // Mimics JS semantics: typeof undeclaredVar === "undefined" (no throw).
353
+ var name = this._pop(); // LOAD_CONST pushed the name - consume it
354
+ var val = Object.prototype.hasOwnProperty.call(this.globals, name)
355
+ ? this.globals[name]
356
+ : undefined;
357
+ this._push(typeof val);
358
+ break;
359
+ }
344
360
 
345
- case OP.JUMP:
346
- frame.pc = operand;
347
- break;
361
+ case OP.JUMP:
362
+ frame._pc = operand;
363
+ break;
348
364
 
349
- case OP.JUMP_IF_FALSE:
350
- if (!this._pop()) frame.pc = operand;
351
- break;
365
+ case OP.JUMP_IF_FALSE:
366
+ if (!this._pop()) frame._pc = operand;
367
+ break;
352
368
 
353
- case OP.JUMP_IF_TRUE_OR_POP:
354
- // || semantics: if truthy, we're done leave value, jump over RHS.
355
- // If falsy, discard it and fall through to evaluate RHS.
356
- if (this.peek()) {
357
- frame.pc = operand;
358
- } else {
359
- this._pop();
369
+ case OP.JUMP_IF_TRUE_OR_POP:
370
+ // || semantics: if truthy, we're done - leave value, jump over RHS.
371
+ // If falsy, discard it and fall through to evaluate RHS.
372
+ if (this.peek()) {
373
+ frame._pc = operand;
374
+ } else {
375
+ this._pop();
376
+ }
377
+ break;
378
+
379
+ case OP.JUMP_IF_FALSE_OR_POP:
380
+ // && semantics: if falsy, we're done - leave value, jump over RHS.
381
+ // If truthy, discard it and fall through to evaluate RHS.
382
+ if (!this.peek()) {
383
+ frame._pc = operand;
384
+ } else {
385
+ this._pop();
386
+ }
387
+ break;
388
+
389
+ case OP.MAKE_CLOSURE: {
390
+ // operand = startPc: absolute index of the function body's first instruction.
391
+ // Metadata is read from the value stack (pushed by _emitClosureMetadata).
392
+ // Stack layout when we arrive here (top is rightmost):
393
+ // [isLocal_0, idx_0, ..., isLocal_N-1, idx_N-1, uvCount, localCount, paramCount]
394
+ var startPc = operand;
395
+ var paramCount = this._pop();
396
+ var localCount = this._pop();
397
+ var uvCount = this._pop();
398
+
399
+ // Upvalues were pushed in order 0..N-1 so we pop them in reverse.
400
+ var uvDescs = new Array(uvCount);
401
+ for (var i = uvCount - 1; i >= 0; i--) {
402
+ var uvIndex = this._pop();
403
+ var isLocalRaw = this._pop();
404
+ uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };
405
+ }
406
+
407
+ var fn = {
408
+ paramCount: paramCount,
409
+ localCount: localCount,
410
+ startPc: startPc,
411
+ upvalueDescriptors: uvDescs,
412
+ };
413
+
414
+ var closure = new Closure(fn);
415
+ for (var i = 0; i < uvDescs.length; i++) {
416
+ var uvd = uvDescs[i];
417
+ if (uvd.isLocal) {
418
+ // Capture directly from current frame's local slot
419
+ closure.upvalues.push(this.captureUpvalue(frame, uvd._index));
420
+ } else {
421
+ // Relay - take upvalue from the enclosing closure's list
422
+ closure.upvalues.push(frame.closure.upvalues[uvd._index]);
423
+ }
424
+ }
425
+ // Wrap in a native callable shell so host code (array methods,
426
+ // test assertions, setTimeout, etc.) can invoke VM closures.
427
+ // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.
428
+ var self = this;
429
+ var shell = (function (c) {
430
+ return function () {
431
+ var args = Array.prototype.slice.call(arguments);
432
+ var sub = new VM(self.bytecode, 0, self.constants, self.globals);
433
+ // Sloppy-mode: null/undefined thisArg → global object
434
+ var f = new Frame(
435
+ c,
436
+ null,
437
+ null,
438
+ this == null ? self.globals : this,
439
+ );
440
+ for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
441
+ f.locals[c.fn.paramCount] = args;
442
+ sub._currentFrame = f;
443
+ return sub.run();
444
+ };
445
+ })(closure);
446
+ shell[CLOSURE_SYM] = closure;
447
+ shell.prototype = closure.prototype; // unified prototype for new/instanceof
448
+ this._push(shell);
449
+ break;
360
450
  }
361
- break;
362
451
 
363
- case OP.JUMP_IF_FALSE_OR_POP:
364
- // && semantics: if falsy, we're done leave value, jump over RHS.
365
- // If truthy, discard it and fall through to evaluate RHS.
366
- if (!this.peek()) {
367
- frame.pc = operand;
368
- } else {
369
- this._pop();
452
+ case OP.DATA:
453
+ // Should never appear in compiled output (reserved opcode slot).
454
+ throw new Error("DATA opcode executed at pc " + (frame._pc - 1));
455
+
456
+ case OP.LOAD_UPVALUE:
457
+ this._push(frame.closure.upvalues[operand]._read());
458
+ break;
459
+
460
+ case OP.STORE_UPVALUE:
461
+ frame.closure.upvalues[operand]._write(this._pop());
462
+ break;
463
+
464
+ case OP.BUILD_ARRAY: {
465
+ // Pop \`operand\` values off the stack in reverse, assemble array.
466
+ var elems = this._stack.splice(this._stack.length - operand);
467
+ this._push(elems);
468
+ break;
370
469
  }
371
- break;
372
470
 
373
- case OP.MAKE_CLOSURE: {
374
- var fn = this.constants[operand];
375
- var closure = new Closure(fn);
376
- for (var i = 0; i < fn.upvalueDescriptors.length; i++) {
377
- var desc = fn.upvalueDescriptors[i];
378
- if (desc.isLocal) {
379
- // Capture directly from current frame's local slot
380
- closure.upvalues.push(this.captureUpvalue(frame, desc._index));
471
+ case OP.BUILD_OBJECT: {
472
+ // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)
473
+ // Pop all pairs and build the object.
474
+ var pairs = this._stack.splice(this._stack.length - operand * 2);
475
+ var o = {};
476
+ for (var i = 0; i < pairs.length; i += 2) {
477
+ o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd
478
+ }
479
+ this._push(o);
480
+ break;
481
+ }
482
+ case OP.SET_PROP: {
483
+ // Stack: [..., obj, key, val]
484
+ // Leaves val on stack - assignment is an expression in JS.
485
+ var val = this._pop();
486
+ var key = this._pop();
487
+ var obj = this._pop();
488
+ // Reflect.set performs [[Set]] without throwing on failure,
489
+ // correctly simulating sloppy-mode assignment from a strict-mode host
490
+ // (output.js is an ES module). This also properly invokes inherited
491
+ // or prototype-chain setter functions.
492
+ Reflect.set(obj, key, val);
493
+ this._push(val); // assignment expression evaluates to the assigned value
494
+ break;
495
+ }
496
+ case OP.GET_PROP_COMPUTED: {
497
+ // Stack: [..., obj, key] - key is a runtime value (nums[i])
498
+ // Mirrors GET_PROP but pops the key that was pushed dynamically.
499
+ var key = this._pop();
500
+ var obj = this._pop();
501
+ this._push(obj[key]);
502
+ break;
503
+ }
504
+ case OP.DELETE_PROP: {
505
+ var key = this._pop();
506
+ var obj = this._pop();
507
+ this._push(delete obj[key]);
508
+ break;
509
+ }
510
+
511
+ case OP.CALL: {
512
+ var args = this._stack.splice(this._stack.length - operand);
513
+ var callee = this._pop();
514
+ if (callee && callee[CLOSURE_SYM]) {
515
+ // VM closure - run directly in this VM, no sub-VM overhead
516
+ var c = callee[CLOSURE_SYM];
517
+ // Sloppy-mode: plain function call → global object as this
518
+ var f = new Frame(c, frame._pc, frame, this.globals);
519
+ for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
520
+ f.locals[c.fn.paramCount] = args;
521
+ this._frameStack.push(this._currentFrame);
522
+ this._currentFrame = f;
381
523
  } else {
382
- // Relay — take upvalue from the enclosing closure's list
383
- closure.upvalues.push(frame.closure.upvalues[desc._index]);
524
+ // Native function
525
+ this._push(callee.apply(null, args));
384
526
  }
527
+ break;
385
528
  }
386
- // Wrap in a native callable shell so host code (array methods,
387
- // test assertions, setTimeout, etc.) can invoke VM closures.
388
- // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.
389
- var self = this;
390
- var shell = (function (c) {
391
- return function () {
392
- var args = Array.prototype.slice.call(arguments);
393
- var sub = new VM(self.bytecode, 0, self.constants, self.globals);
394
- var f = new Frame(c, null, null, this);
529
+
530
+ case OP.CALL_METHOD: {
531
+ var args = this._stack.splice(this._stack.length - operand);
532
+ var callee = this._pop();
533
+ var receiver = this._pop(); // left on stack by GET_PROP
534
+ if (callee && callee[CLOSURE_SYM]) {
535
+ // VM closure - run directly in this VM with receiver as this
536
+ var c = callee[CLOSURE_SYM];
537
+ var f = new Frame(c, frame._pc, frame, receiver);
395
538
  for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
396
539
  f.locals[c.fn.paramCount] = args;
397
- sub.currentFrame = f;
398
- return sub.run();
399
- };
400
- })(closure);
401
- shell[CLOSURE_SYM] = closure;
402
- shell.prototype = closure.prototype; // unified prototype for new/instanceof
403
- this._push(shell);
404
- break;
405
- }
540
+ this._frameStack.push(this._currentFrame);
541
+ this._currentFrame = f;
542
+ } else {
543
+ // Native method
544
+ this._push(callee.apply(receiver, args));
545
+ }
546
+ break;
547
+ }
406
548
 
407
- case OP.LOAD_UPVALUE:
408
- this._push(frame.closure.upvalues[operand].read());
409
- break;
549
+ case OP.LOAD_THIS:
550
+ this._push(frame.thisVal);
551
+ break;
552
+
553
+ case OP.NEW: {
554
+ var args = this._stack.splice(this._stack.length - operand);
555
+ var callee = this._pop();
556
+ if (callee && callee[CLOSURE_SYM]) {
557
+ // VM closure constructor - prototype is unified via shell.prototype = closure.prototype
558
+ var c = callee[CLOSURE_SYM];
559
+ var newObj = Object.create(c.prototype || null);
560
+ var f = new Frame(c, frame._pc, frame, newObj);
561
+ f._newObj = newObj;
562
+ for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
563
+ f.locals[c.fn.paramCount] = args;
564
+ this._frameStack.push(this._currentFrame);
565
+ this._currentFrame = f;
566
+ } else {
567
+ // Native constructor (e.g. new Error(), new Date()).
568
+ // Reflect.construct is required - Object.create+apply does NOT set
569
+ // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.
570
+ this._push(Reflect.construct(callee, args));
571
+ }
572
+ break;
573
+ }
410
574
 
411
- case OP.STORE_UPVALUE:
412
- frame.closure.upvalues[operand].write(this._pop());
413
- break;
575
+ case OP.RETURN: {
576
+ var retVal = this._pop();
577
+ this._closeUpvaluesFor(frame); // must happen before frame is abandoned
578
+ if (this._frameStack.length === 0) return retVal;
414
579
 
415
- case OP.BUILD_ARRAY: {
416
- // Pop \`operand\` values off the stack in reverse, assemble array.
417
- var elems = this._stack.splice(this._stack.length - operand);
418
- this._push(elems);
419
- break;
420
- }
580
+ // new-call rule: primitive return -> discard, use the constructed object instead
581
+ if (frame._newObj !== null) {
582
+ if (typeof retVal !== "object" || retVal === null)
583
+ retVal = frame._newObj;
584
+ }
421
585
 
422
- case OP.BUILD_OBJECT: {
423
- // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left→right)
424
- // Pop all pairs and build the object.
425
- var pairs = this._stack.splice(this._stack.length - operand * 2);
426
- var o = {};
427
- for (var i = 0; i < pairs.length; i += 2) {
428
- o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd
586
+ this._currentFrame = this._frameStack.pop();
587
+ this._push(retVal);
588
+ break;
429
589
  }
430
- this._push(o);
431
- break;
432
- }
433
- case OP.SET_PROP: {
434
- // Stack: [..., obj, key, val]
435
- // Leaves val on stack — assignment is an expression in JS.
436
- var val = this._pop();
437
- var key = this._pop();
438
- var obj = this._pop();
439
- obj[key] = val;
440
- this._push(val); // assignment expression evaluates to the assigned value
441
- break;
442
- }
443
- case OP.GET_PROP_COMPUTED: {
444
- // Stack: [..., obj, key] — key is a runtime value (nums[i])
445
- // Mirrors GET_PROP but pops the key that was pushed dynamically.
446
- var key = this._pop();
447
- var obj = this._pop();
448
- this._push(obj[key]);
449
- break;
450
- }
451
- case OP.DELETE_PROP: {
452
- var key = this._pop();
453
- var obj = this._pop();
454
- this._push(delete obj[key]);
455
- break;
456
- }
457
590
 
458
- case OP.CALL: {
459
- var args = this._stack.splice(this._stack.length - operand);
460
- var callee = this._pop();
461
- if (callee && callee[CLOSURE_SYM]) {
462
- // VM closure — run directly in this VM, no sub-VM overhead
463
- var c = callee[CLOSURE_SYM];
464
- var f = new Frame(c, frame.pc, frame, undefined);
465
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
466
- f.locals[c.fn.paramCount] = args;
467
- this.frameStack.push(this.currentFrame);
468
- this.currentFrame = f;
469
- } else {
470
- // Native function
471
- this._push(callee.apply(null, args));
472
- }
473
- break;
474
- }
591
+ case OP.POP:
592
+ this._pop();
593
+ break;
594
+
595
+ case OP.DUP:
596
+ this._push(this.peek());
597
+ break;
598
+
599
+ case OP.THROW:
600
+ throw this._pop();
601
+
602
+ case OP.FOR_IN_SETUP: {
603
+ // Pop the object; build an ordered list of all enumerable own+inherited
604
+ // string keys by walking the prototype chain manually.
605
+ // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,
606
+ // so we never rely on Object.keys() and we handle inheritance correctly.
607
+ var obj = this._pop();
608
+ var keys = [];
609
+ if (obj !== null && obj !== undefined) {
610
+ var seen = Object.create(null);
611
+ var cur = Object(obj); // box primitives
612
+ while (cur !== null) {
613
+ var ownNames = Object.getOwnPropertyNames(cur);
614
+ for (var i = 0; i < ownNames.length; i++) {
615
+ var k = ownNames[i];
616
+ if (!(k in seen)) {
617
+ seen[k] = true;
618
+ var propDesc = Object.getOwnPropertyDescriptor(cur, k);
619
+ if (propDesc && propDesc.enumerable) {
620
+ keys.push(k);
621
+ }
622
+ }
623
+ }
624
+ cur = Object.getPrototypeOf(cur);
625
+ }
626
+ }
627
+ this._push({ _keys: keys, i: 0 });
628
+ break;
629
+ }
475
630
 
476
- case OP.CALL_METHOD: {
477
- var args = this._stack.splice(this._stack.length - operand);
478
- var callee = this._pop();
479
- var receiver = this._pop(); // left on stack by GET_PROP
480
- if (callee && callee[CLOSURE_SYM]) {
481
- // VM closure — run directly in this VM with receiver as this
482
- var c = callee[CLOSURE_SYM];
483
- var f = new Frame(c, frame.pc, frame, receiver);
484
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
485
- f.locals[c.fn.paramCount] = args;
486
- this.frameStack.push(this.currentFrame);
487
- this.currentFrame = f;
488
- } else {
489
- // Native method
490
- this._push(callee.apply(receiver, args));
491
- }
492
- break;
493
- }
631
+ case OP.FOR_IN_NEXT: {
632
+ // operand = jump target for the done case.
633
+ // Pop the iterator; if exhausted jump to exit, otherwise push next key.
634
+ var iter = this._pop();
635
+ if (iter.i >= iter._keys.length) {
636
+ frame._pc = operand;
637
+ } else {
638
+ this._push(iter._keys[iter.i++]);
639
+ }
640
+ break;
641
+ }
494
642
 
495
- case OP.LOAD_THIS:
496
- this._push(frame.thisVal);
497
- break;
498
-
499
- case OP.NEW: {
500
- var args = this._stack.splice(this._stack.length - operand);
501
- var callee = this._pop();
502
- if (callee && callee[CLOSURE_SYM]) {
503
- // VM closure constructor — prototype is unified via shell.prototype = closure.prototype
504
- var c = callee[CLOSURE_SYM];
505
- var newObj = Object.create(c.prototype || null);
506
- var f = new Frame(c, frame.pc, frame, newObj);
507
- f._newObj = newObj;
508
- for (var i = 0; i < args.length; i++) f.locals[i] = args[i];
509
- f.locals[c.fn.paramCount] = args;
510
- this.frameStack.push(this.currentFrame);
511
- this.currentFrame = f;
512
- } else {
513
- // Native constructor (e.g. new Error(), new Date()).
514
- // Reflect.construct is required — Object.create+apply does NOT set
515
- // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.
516
- this._push(Reflect.construct(callee, args));
517
- }
518
- break;
519
- }
643
+ case OP.PATCH: {
644
+ // Writes at operand the bytecode[arg1:arg2]
645
+ var destPc = operand;
646
+ var instructions = this.bytecode.slice(this._pop(), this._pop());
647
+
648
+ for (var i = 0; i < instructions.length; i++) {
649
+ this.bytecode[destPc + i] = instructions[i];
650
+ }
520
651
 
521
- case OP.RETURN: {
522
- var retVal = this._pop();
523
- this.closeUpvaluesFor(frame); // must happen before frame is abandoned
524
- if (this.frameStack.length === 0) return retVal;
652
+ break;
653
+ }
525
654
 
526
- // new-call rule: primitive return → discard, use the constructed object instead
527
- if (frame._newObj !== null) {
528
- if (typeof retVal !== "object" || retVal === null)
529
- retVal = frame._newObj;
655
+ case OP.TRY_SETUP: {
656
+ // Push an exception handler record onto the current frame.
657
+ // Saves: catch PC (operand), current stack depth, current frame-stack depth.
658
+ // If an exception is thrown before TRY_END fires, the VM jumps here.
659
+ frame._handlerStack.push({
660
+ handlerPc: operand,
661
+ stackDepth: this._stack.length,
662
+ frameStackDepth: this._frameStack.length,
663
+ });
664
+ break;
530
665
  }
531
666
 
532
- this.currentFrame = this.frameStack.pop();
533
- this._push(retVal);
534
- break;
535
- }
667
+ case OP.TRY_END: {
668
+ // Normal exit from a try block — disarm the exception handler.
669
+ frame._handlerStack.pop();
670
+ break;
671
+ }
536
672
 
537
- case OP.POP:
538
- this._pop();
539
- break;
540
-
541
- case OP.DUP:
542
- this._push(this.peek());
543
- break;
544
-
545
- case OP.THROW:
546
- throw this._pop();
547
-
548
- case OP.FOR_IN_SETUP: {
549
- // Pop the object; build an ordered list of all enumerable own+inherited
550
- // string keys by walking the prototype chain manually.
551
- // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,
552
- // so we never rely on Object.keys() and we handle inheritance correctly.
553
- var obj = this._pop();
554
- var keys = [];
555
- if (obj !== null && obj !== undefined) {
556
- var seen = Object.create(null);
557
- var cur = Object(obj); // box primitives
558
- while (cur !== null) {
559
- var ownNames = Object.getOwnPropertyNames(cur);
560
- for (var i = 0; i < ownNames.length; i++) {
561
- var k = ownNames[i];
562
- if (!(k in seen)) {
563
- seen[k] = true;
564
- var propDesc = Object.getOwnPropertyDescriptor(cur, k);
565
- if (propDesc && propDesc.enumerable) {
566
- keys.push(k);
567
- }
568
- }
569
- }
570
- cur = Object.getPrototypeOf(cur);
673
+ case OP.DEFINE_GETTER: {
674
+ // Stack: [..., obj, key, getterFn]
675
+ // Pops all three; defines an enumerable, configurable getter on obj.
676
+ // If a setter was already defined for this key, it is preserved.
677
+ var getterFn = this._pop();
678
+ var key = this._pop();
679
+ var obj = this._pop();
680
+ var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
681
+ var getDesc: PropertyDescriptor = {
682
+ get: getterFn,
683
+ configurable: true,
684
+ enumerable: true,
685
+ };
686
+ if (existingDesc && typeof existingDesc.set === "function") {
687
+ getDesc.set = existingDesc.set;
571
688
  }
689
+ Object.defineProperty(obj, key, getDesc);
690
+ break;
572
691
  }
573
- this._push({ keys: keys, i: 0 });
574
- break;
575
- }
576
692
 
577
- case OP.FOR_IN_NEXT: {
578
- // operand = jump target for the done case.
579
- // Pop the iterator; if exhausted jump to exit, otherwise push next key.
580
- var iter = this._pop();
581
- if (iter.i >= iter.keys.length) {
582
- frame.pc = operand;
583
- } else {
584
- this._push(iter.keys[iter.i++]);
693
+ case OP.DEFINE_SETTER: {
694
+ // Stack: [..., obj, key, setterFn]
695
+ // Pops all three; defines an enumerable, configurable setter on obj.
696
+ // If a getter was already defined for this key, it is preserved.
697
+ var setterFn = this._pop();
698
+ var key = this._pop();
699
+ var obj = this._pop();
700
+ var existingDesc = Object.getOwnPropertyDescriptor(obj, key);
701
+ var setDesc: PropertyDescriptor = {
702
+ set: setterFn,
703
+ configurable: true,
704
+ enumerable: true,
705
+ };
706
+ if (existingDesc && typeof existingDesc.get === "function") {
707
+ setDesc.get = existingDesc.get;
708
+ }
709
+ Object.defineProperty(obj, key, setDesc);
710
+ break;
585
711
  }
586
- break;
587
- }
588
712
 
589
- case OP.PATCH: {
590
- // Pop destination PC, then write constants[operand] (packed word array)
591
- // directly into this.bytecode starting at that PC.
592
- var destPc = this._pop();
593
- var words = this.constants[operand];
594
-
595
- if (PACK) {
596
- words = decodeBytecode(words);
713
+ case OP.DEBUGGER: {
714
+ debugger;
715
+ break;
597
716
  }
598
717
 
599
- for (var i = 0; i < words.length; i++) {
600
- this.bytecode[destPc + i] = words[i];
718
+ default:
719
+ throw new Error(
720
+ "Unknown opcode: " + op + " at pc " + (frame._pc - 1),
721
+ );
722
+ }
723
+ } catch (err) {
724
+ // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).
725
+ // Walk from the current frame upward until we find a frame that has an open
726
+ // exception handler (TRY_SETUP without a matching TRY_END).
727
+ // For every frame we abandon along the way, close its captured upvalues.
728
+ var handledFrame = null;
729
+ var searchFrame = this._currentFrame;
730
+ while (true) {
731
+ if (searchFrame._handlerStack.length > 0) {
732
+ handledFrame = searchFrame;
733
+ break;
601
734
  }
602
- break;
603
- }
604
-
605
- default:
606
- throw new Error("Unknown opcode: " + op + " at pc " + (frame.pc - 1));
735
+ // No handler in this frame — abandon it and walk up.
736
+ this._closeUpvaluesFor(searchFrame);
737
+ if (this._frameStack.length === 0) break;
738
+ searchFrame = this._frameStack.pop();
739
+ this._currentFrame = searchFrame;
740
+ }
741
+
742
+ if (!handledFrame) throw err; // no handler anywhere — propagate to host
743
+
744
+ var h = handledFrame._handlerStack.pop();
745
+ // Restore the VM value stack to the depth recorded at TRY_SETUP time,
746
+ // then push the caught exception so the catch binding can store it.
747
+ this._stack.length = h.stackDepth;
748
+ this._push(err);
749
+ // Discard any call-frames that were pushed inside the try body
750
+ // (functions called from within the try block that are still live).
751
+ this._frameStack.length = h.frameStackDepth;
752
+ // Jump to the catch block.
753
+ handledFrame._pc = h.handlerPc;
754
+ this._currentFrame = handledFrame;
607
755
  }
608
756
  }
609
757
  };
610
758
 
611
- // ── Boot ─────────────────────────────────────────────────────────
759
+ // Boot
612
760
  var globals: any = {}; // global object for globals
613
761
 
614
762
  // Always pull built-ins from globalThis so eval() scoping can't shadow them