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