js-confuser-vm 0.0.3 → 0.0.5

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