js-confuser-vm 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.MD +186 -107
  3. package/dist/build-runtime.js +7 -1
  4. package/dist/compiler.js +801 -785
  5. package/dist/runtime.js +409 -332
  6. package/dist/transforms/bytecode/aliasedOpcodes.js +140 -0
  7. package/dist/transforms/bytecode/concealConstants.js +31 -0
  8. package/dist/transforms/bytecode/macroOpcodes.js +22 -10
  9. package/dist/transforms/bytecode/resolveContants.js +73 -10
  10. package/dist/transforms/bytecode/selfModifying.js +3 -2
  11. package/dist/transforms/bytecode/specializedOpcodes.js +38 -28
  12. package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
  13. package/dist/transforms/runtime/shuffleOpcodes.js +1 -1
  14. package/dist/transforms/runtime/specializedOpcodes.js +21 -16
  15. package/dist/utils/op-utils.js +29 -0
  16. package/dist/utils/random-utils.js +27 -0
  17. package/index.ts +10 -8
  18. package/jest.config.js +10 -0
  19. package/package.json +1 -1
  20. package/src/build-runtime.ts +7 -1
  21. package/src/compiler.ts +2395 -2069
  22. package/src/options.ts +2 -0
  23. package/src/runtime.ts +838 -771
  24. package/src/transforms/bytecode/aliasedOpcodes.ts +158 -0
  25. package/src/transforms/bytecode/concealConstants.ts +52 -0
  26. package/src/transforms/bytecode/macroOpcodes.ts +32 -15
  27. package/src/transforms/bytecode/resolveContants.ts +87 -16
  28. package/src/transforms/bytecode/selfModifying.ts +3 -3
  29. package/src/transforms/bytecode/specializedOpcodes.ts +58 -29
  30. package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
  31. package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
  32. package/src/transforms/runtime/specializedOpcodes.ts +39 -24
  33. package/src/{transforms/utils → utils}/op-utils.ts +7 -0
  34. /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
package/dist/compiler.js CHANGED
@@ -12,158 +12,152 @@ import { selfModifying } from "./transforms/bytecode/selfModifying.js";
12
12
  import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
13
13
  import * as b from "./types.js";
14
14
  import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
15
- import { getRandomInt } from "./transforms/utils/random-utils.js";
16
- import { U16_MAX } from "./transforms/utils/op-utils.js";
15
+ import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
16
+ import { getRandomInt } from "./utils/random-utils.js";
17
+ import { U16_MAX } from "./utils/op-utils.js";
18
+ import { concealConstants } from "./transforms/bytecode/concealConstants.js";
17
19
  const traverse = traverseImport.default || traverseImport;
18
- const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\r\nconst BYTECODE = [];\r\nconst MAIN_START_PC = 0;\r\nconst CONSTANTS = [];\r\nconst ENCODE_BYTECODE = false;\r\nconst TIMING_CHECKS = false;\r\n// The text above is not included in the compiled output - for type intellisense only\r\n// @START\r\n\r\nfunction decodeBytecode(s) {\r\n if (!ENCODE_BYTECODE) return s;\r\n\r\n var b =\r\n typeof Buffer !== \"undefined\"\r\n ? Buffer.from(s, \"base64\")\r\n : Uint8Array.from(atob(s), function (c) {\r\n return c.charCodeAt(0);\r\n });\r\n // Each slot is a u16 stored as 2 little-endian bytes.\r\n var r = new Uint16Array(b.length / 2);\r\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\r\n return r;\r\n}\r\n\r\n// Closure symbol\r\n// Used to tag shell functions so the VM can fast-path back to the\r\n// inner Closure instead of going through a sub-VM on internal calls.\r\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\r\n\r\n// Upvalue\r\n// While the outer frame is alive: reads/writes go to frame.locals[slot].\r\n// After the outer frame returns (closed): reads/writes hit this.value.\r\nfunction Upvalue(frame, slot) {\r\n this._frame = frame;\r\n this._slot = slot;\r\n this._closed = false;\r\n this._value = undefined;\r\n}\r\nUpvalue.prototype._read = function () {\r\n return this._closed ? this._value : this._frame.locals[this._slot];\r\n};\r\nUpvalue.prototype._write = function (v) {\r\n if (this._closed) this._value = v;\r\n else this._frame.locals[this._slot] = v;\r\n};\r\nUpvalue.prototype._close = function () {\r\n this._value = this._frame.locals[this._slot];\r\n this._closed = true;\r\n};\r\n\r\n// Closure & Frame\r\nfunction Closure(fn) {\r\n this.fn = fn;\r\n this.upvalues = [];\r\n this.prototype = {}; // <- default prototype object for \\`new\\`\r\n}\r\n\r\nfunction Frame(closure, returnPc, parent, thisVal ) {\r\n this.closure = closure;\r\n this.locals = new Array(closure.fn.localCount).fill(undefined);\r\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\r\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\r\n this._parent = parent;\r\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\r\n this._newObj = null; // <- set by NEW so RETURN can see it\r\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\r\n}\r\n\r\n// VM\r\nfunction VM(bytecode, mainStartPc, constants, globals) {\r\n this.bytecode = bytecode;\r\n this.constants = constants;\r\n this.globals = globals;\r\n this._stack = [];\r\n this._frameStack = [];\r\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\r\n\r\n var mainFn = {\r\n paramCount: 0,\r\n localCount: 0,\r\n startPc: mainStartPc, // <- where main begins\r\n };\r\n this._currentFrame = new Frame(new Closure(mainFn), null, null);\r\n}\r\n\r\nVM.prototype._push = function (v) {\r\n this._stack.push(v);\r\n};\r\nVM.prototype._pop = function () {\r\n return this._stack.pop();\r\n};\r\nVM.prototype.peek = function () {\r\n return this._stack[this._stack.length - 1];\r\n};\r\n\r\n// Consume the next slot from the flat bytecode stream and advance the PC.\r\n// Called by opcode handlers to read each of their operands in order.\r\nVM.prototype._operand = function () {\r\n return this.bytecode[this._currentFrame._pc++];\r\n};\r\n\r\nVM.prototype.captureUpvalue = function (frame, slot) {\r\n // Reuse existing open upvalue for this frame+slot if one exists.\r\n // This is what makes two closures share the same mutable cell.\r\n for (var i = 0; i < this._openUpvalues.length; i++) {\r\n var uv = this._openUpvalues[i];\r\n if (uv._frame === frame && uv._slot === slot) return uv;\r\n }\r\n var uv = new Upvalue(frame, slot);\r\n this._openUpvalues.push(uv);\r\n return uv;\r\n};\r\n\r\nVM.prototype._closeUpvaluesFor = function (frame) {\r\n // Called on RETURN - close every upvalue that was pointing into this frame.\r\n // After this, closures that captured from the frame read from upvalue.value.\r\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\r\n if (uv._frame === frame) {\r\n uv._close();\r\n return false;\r\n }\r\n return true;\r\n });\r\n};\r\n\r\nVM.prototype.run = function () {\r\n var now = () => {\r\n return performance.now();\r\n };\r\n\r\n var lastTime = now();\r\n\r\n while (true) {\r\n var frame = this._currentFrame;\r\n var bc = this.bytecode;\r\n if (frame._pc >= bc.length) break;\r\n\r\n var op = this.bytecode[frame._pc++];\r\n\r\n // console.log(frame._pc - 1, op);\r\n\r\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\r\n if (TIMING_CHECKS) {\r\n var currentTime = now();\r\n var isTamper = currentTime - lastTime > 1000;\r\n lastTime = currentTime;\r\n if (isTamper) {\r\n // Poison the bytecode\r\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\r\n // Break the current state\r\n op = OP.POP;\r\n this._stack = [];\r\n }\r\n }\r\n\r\n try {\r\n /* @SWITCH */\r\n switch (op) {\r\n case OP.LOAD_CONST:\r\n this._push(this.constants[this._operand()]);\r\n break;\r\n\r\n case OP.LOAD_INT:\r\n this._push(this._operand());\r\n break;\r\n\r\n case OP.LOAD_LOCAL:\r\n this._push(frame.locals[this._operand()]);\r\n break;\r\n\r\n case OP.STORE_LOCAL:\r\n frame.locals[this._operand()] = this._pop();\r\n break;\r\n\r\n case OP.LOAD_GLOBAL:\r\n this._push(this.globals[this.constants[this._operand()]]);\r\n break;\r\n\r\n case OP.STORE_GLOBAL:\r\n this.globals[this.constants[this._operand()]] = this._pop();\r\n break;\r\n\r\n case OP.GET_PROP: {\r\n // Stack: [..., obj, key] -> [..., obj, obj[key]]\r\n // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver\r\n var key = this._pop();\r\n var obj = this.peek();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n\r\n case OP.ADD: {\r\n var b = this._pop();\r\n this._push(this._pop() + b);\r\n break;\r\n }\r\n case OP.SUB: {\r\n var b = this._pop();\r\n this._push(this._pop() - b);\r\n break;\r\n }\r\n case OP.MUL: {\r\n var b = this._pop();\r\n this._push(this._pop() * b);\r\n break;\r\n }\r\n case OP.DIV: {\r\n var b = this._pop();\r\n this._push(this._pop() / b);\r\n break;\r\n }\r\n case OP.MOD: {\r\n var b = this._pop();\r\n this._push(this._pop() % b);\r\n break;\r\n }\r\n case OP.BAND: {\r\n var b = this._pop();\r\n this._push(this._pop() & b);\r\n break;\r\n }\r\n case OP.BOR: {\r\n var b = this._pop();\r\n this._push(this._pop() | b);\r\n break;\r\n }\r\n case OP.BXOR: {\r\n var b = this._pop();\r\n this._push(this._pop() ^ b);\r\n break;\r\n }\r\n case OP.SHL: {\r\n var b = this._pop();\r\n this._push(this._pop() << b);\r\n break;\r\n }\r\n case OP.SHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >> b);\r\n break;\r\n }\r\n case OP.USHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >>> b);\r\n break;\r\n }\r\n\r\n case OP.LT: {\r\n var b = this._pop();\r\n this._push(this._pop() < b);\r\n break;\r\n }\r\n case OP.GT: {\r\n var b = this._pop();\r\n this._push(this._pop() > b);\r\n break;\r\n }\r\n case OP.EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() === b);\r\n break;\r\n }\r\n\r\n case OP.LTE: {\r\n var b = this._pop();\r\n this._push(this._pop() <= b);\r\n break;\r\n }\r\n case OP.GTE: {\r\n var b = this._pop();\r\n this._push(this._pop() >= b);\r\n break;\r\n }\r\n case OP.NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() !== b);\r\n break;\r\n }\r\n case OP.LOOSE_EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() == b);\r\n break;\r\n }\r\n case OP.LOOSE_NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() != b);\r\n break;\r\n }\r\n\r\n case OP.IN: {\r\n var b = this._pop();\r\n this._push(this._pop() in b);\r\n break;\r\n }\r\n\r\n case OP.INSTANCEOF: {\r\n var ctor = this._pop();\r\n var obj = this._pop();\r\n if (typeof ctor === \"function\") {\r\n // Native constructor (e.g. Array, Date) - native instanceof is fine\r\n this._push(obj instanceof ctor);\r\n } else {\r\n // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.\r\n // Walk obj's prototype chain looking for identity with ctor.prototype.\r\n var proto = ctor.prototype; // the .prototype property on the Closure\r\n var target = Object.getPrototypeOf(obj);\r\n var result = false;\r\n while (target !== null) {\r\n if (target === proto) {\r\n result = true;\r\n break;\r\n }\r\n target = Object.getPrototypeOf(target);\r\n }\r\n this._push(result);\r\n }\r\n break;\r\n }\r\n\r\n case OP.UNARY_NEG:\r\n this._push(-this._pop());\r\n break;\r\n case OP.UNARY_POS:\r\n this._push(this._pop());\r\n break;\r\n case OP.UNARY_NOT:\r\n this._push(!this._pop());\r\n break;\r\n case OP.UNARY_BITNOT:\r\n this._push(~this._pop());\r\n break;\r\n case OP.TYPEOF:\r\n this._push(typeof this._pop());\r\n break;\r\n case OP.VOID:\r\n this._pop();\r\n this._push(undefined);\r\n break;\r\n\r\n case OP.TYPEOF_SAFE: {\r\n // operand is a const index holding the variable name string.\r\n // Mimics JS semantics: typeof undeclaredVar === \"undefined\" (no throw).\r\n var name = this._pop(); // LOAD_CONST pushed the name - consume it\r\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\r\n ? this.globals[name]\r\n : undefined;\r\n this._push(typeof val);\r\n break;\r\n }\r\n\r\n case OP.JUMP:\r\n frame._pc = this._operand();\r\n break;\r\n\r\n case OP.JUMP_IF_FALSE: {\r\n var target = this._operand();\r\n if (!this._pop()) frame._pc = target;\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_TRUE_OR_POP: {\r\n // || semantics: if truthy, we're done - leave value, jump over RHS.\r\n // If falsy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_FALSE_OR_POP: {\r\n // && semantics: if falsy, we're done - leave value, jump over RHS.\r\n // If truthy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (!this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.MAKE_CLOSURE: {\r\n // Inline operands: startPc, paramCount, localCount, uvCount,\r\n // [isLocal_0, idx_0, isLocal_1, idx_1, ...]\r\n var startPc = this._operand();\r\n var paramCount = this._operand();\r\n var localCount = this._operand();\r\n var uvCount = this._operand();\r\n\r\n var uvDescs = new Array(uvCount);\r\n for (var i = 0; i < uvCount; i++) {\r\n var isLocalRaw = this._operand();\r\n var uvIndex = this._operand();\r\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\r\n }\r\n\r\n var fn = {\r\n paramCount: paramCount,\r\n localCount: localCount,\r\n startPc: startPc,\r\n upvalueDescriptors: uvDescs,\r\n };\r\n\r\n var closure = new Closure(fn);\r\n for (var i = 0; i < uvDescs.length; i++) {\r\n var uvd = uvDescs[i];\r\n if (uvd.isLocal) {\r\n // Capture directly from current frame's local slot\r\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\r\n } else {\r\n // Relay - take upvalue from the enclosing closure's list\r\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\r\n }\r\n }\r\n // Wrap in a native callable shell so host code (array methods,\r\n // test assertions, setTimeout, etc.) can invoke VM closures.\r\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\r\n var self = this;\r\n var shell = (function (c) {\r\n return function () {\r\n var args = Array.prototype.slice.call(arguments);\r\n var sub = new VM(self.bytecode, 0, self.constants, self.globals);\r\n // Sloppy-mode: null/undefined thisArg \u2192 global object\r\n var f = new Frame(\r\n c,\r\n null,\r\n null,\r\n this == null ? self.globals : this,\r\n );\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n sub._currentFrame = f;\r\n return sub.run();\r\n };\r\n })(closure);\r\n shell[CLOSURE_SYM] = closure;\r\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\r\n this._push(shell);\r\n break;\r\n }\r\n\r\n case OP.LOAD_UPVALUE:\r\n this._push(frame.closure.upvalues[this._operand()]._read());\r\n break;\r\n\r\n case OP.STORE_UPVALUE:\r\n frame.closure.upvalues[this._operand()]._write(this._pop());\r\n break;\r\n\r\n case OP.BUILD_ARRAY: {\r\n var elems = this._stack.splice(this._stack.length - this._operand());\r\n this._push(elems);\r\n break;\r\n }\r\n\r\n case OP.BUILD_OBJECT: {\r\n // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)\r\n // Pop all pairs and build the object.\r\n var pairs = this._stack.splice(\r\n this._stack.length - this._operand() * 2,\r\n );\r\n var o = {};\r\n for (var i = 0; i < pairs.length; i += 2) {\r\n o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd\r\n }\r\n this._push(o);\r\n break;\r\n }\r\n case OP.SET_PROP: {\r\n // Stack: [..., obj, key, val]\r\n // Leaves val on stack - assignment is an expression in JS.\r\n var val = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n // Reflect.set performs [[Set]] without throwing on failure,\r\n // correctly simulating sloppy-mode assignment from a strict-mode host\r\n // (output.js is an ES module). This also properly invokes inherited\r\n // or prototype-chain setter functions.\r\n Reflect.set(obj, key, val);\r\n this._push(val); // assignment expression evaluates to the assigned value\r\n break;\r\n }\r\n case OP.GET_PROP_COMPUTED: {\r\n // Stack: [..., obj, key] - key is a runtime value (nums[i])\r\n // Mirrors GET_PROP but pops the key that was pushed dynamically.\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n case OP.DELETE_PROP: {\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(delete obj[key]);\r\n break;\r\n }\r\n\r\n case OP.CALL: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM, no sub-VM overhead\r\n var c = callee[CLOSURE_SYM];\r\n // Sloppy-mode: plain function call \u2192 global object as this\r\n var f = new Frame(c, frame._pc, frame, this.globals);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native function\r\n this._push(callee.apply(null, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.CALL_METHOD: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n var receiver = this._pop(); // left on stack by GET_PROP\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM with receiver as this\r\n var c = callee[CLOSURE_SYM];\r\n var f = new Frame(c, frame._pc, frame, receiver);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native method\r\n this._push(callee.apply(receiver, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.LOAD_THIS:\r\n this._push(frame.thisVal);\r\n break;\r\n\r\n case OP.NEW: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure constructor - prototype is unified via shell.prototype = closure.prototype\r\n var c = callee[CLOSURE_SYM];\r\n var newObj = Object.create(c.prototype || null);\r\n var f = new Frame(c, frame._pc, frame, newObj);\r\n f._newObj = newObj;\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native constructor (e.g. new Error(), new Date()).\r\n // Reflect.construct is required - Object.create+apply does NOT set\r\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\r\n this._push(Reflect.construct(callee, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.RETURN: {\r\n var retVal = this._pop();\r\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\r\n if (this._frameStack.length === 0) return retVal;\r\n\r\n // new-call rule: primitive return -> discard, use the constructed object instead\r\n if (frame._newObj !== null) {\r\n if (typeof retVal !== \"object\" || retVal === null)\r\n retVal = frame._newObj;\r\n }\r\n\r\n this._currentFrame = this._frameStack.pop();\r\n this._push(retVal);\r\n break;\r\n }\r\n\r\n case OP.POP:\r\n this._pop();\r\n break;\r\n\r\n case OP.DUP:\r\n this._push(this.peek());\r\n break;\r\n\r\n case OP.THROW:\r\n throw this._pop();\r\n\r\n case OP.FOR_IN_SETUP: {\r\n // Pop the object; build an ordered list of all enumerable own+inherited\r\n // string keys by walking the prototype chain manually.\r\n // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,\r\n // so we never rely on Object.keys() and we handle inheritance correctly.\r\n var obj = this._pop();\r\n var keys = [];\r\n if (obj !== null && obj !== undefined) {\r\n var seen = Object.create(null);\r\n var cur = Object(obj); // box primitives\r\n while (cur !== null) {\r\n var ownNames = Object.getOwnPropertyNames(cur);\r\n for (var i = 0; i < ownNames.length; i++) {\r\n var k = ownNames[i];\r\n if (!(k in seen)) {\r\n seen[k] = true;\r\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\r\n if (propDesc && propDesc.enumerable) {\r\n keys.push(k);\r\n }\r\n }\r\n }\r\n cur = Object.getPrototypeOf(cur);\r\n }\r\n }\r\n this._push({ _keys: keys, i: 0 });\r\n break;\r\n }\r\n\r\n case OP.FOR_IN_NEXT: {\r\n // Operand = jump target for the done case. Must be read before the\r\n // conditional so the PC stays correctly aligned either way.\r\n var target = this._operand();\r\n var iter = this._pop();\r\n if (iter.i >= iter._keys.length) {\r\n frame._pc = target;\r\n } else {\r\n this._push(iter._keys[iter.i++]);\r\n }\r\n break;\r\n }\r\n\r\n case OP.PATCH: {\r\n // Inline operands: destPc, sliceStart, sliceEnd\r\n // Copies bytecode[sliceStart..sliceEnd) flat u16 slots to destPc.\r\n var destPc = this._operand();\r\n var sliceStart = this._operand();\r\n var sliceEnd = this._operand();\r\n\r\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\r\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\r\n }\r\n break;\r\n }\r\n\r\n case OP.TRY_SETUP: {\r\n // Push an exception handler record onto the current frame.\r\n // Saves: catch PC (operand), current stack depth, current frame-stack depth.\r\n // If an exception is thrown before TRY_END fires, the VM jumps here.\r\n frame._handlerStack.push({\r\n handlerPc: this._operand(),\r\n stackDepth: this._stack.length,\r\n frameStackDepth: this._frameStack.length,\r\n });\r\n break;\r\n }\r\n\r\n case OP.TRY_END: {\r\n // Normal exit from a try block \u2014 disarm the exception handler.\r\n frame._handlerStack.pop();\r\n break;\r\n }\r\n\r\n case OP.DEFINE_GETTER: {\r\n // Stack: [..., obj, key, getterFn]\r\n // Pops all three; defines an enumerable, configurable getter on obj.\r\n // If a setter was already defined for this key, it is preserved.\r\n var getterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var getDesc = {\r\n get: getterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.set === \"function\") {\r\n getDesc.set = existingDesc.set;\r\n }\r\n Object.defineProperty(obj, key, getDesc);\r\n break;\r\n }\r\n\r\n case OP.DEFINE_SETTER: {\r\n // Stack: [..., obj, key, setterFn]\r\n // Pops all three; defines an enumerable, configurable setter on obj.\r\n // If a getter was already defined for this key, it is preserved.\r\n var setterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var setDesc = {\r\n set: setterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.get === \"function\") {\r\n setDesc.get = existingDesc.get;\r\n }\r\n Object.defineProperty(obj, key, setDesc);\r\n break;\r\n }\r\n\r\n case OP.DEBUGGER: {\r\n debugger;\r\n break;\r\n }\r\n\r\n default:\r\n throw new Error(\r\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\r\n );\r\n }\r\n } catch (err) {\r\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\r\n // Walk from the current frame upward until we find a frame that has an open\r\n // exception handler (TRY_SETUP without a matching TRY_END).\r\n // For every frame we abandon along the way, close its captured upvalues.\r\n var handledFrame = null;\r\n var searchFrame = this._currentFrame;\r\n while (true) {\r\n if (searchFrame._handlerStack.length > 0) {\r\n handledFrame = searchFrame;\r\n break;\r\n }\r\n // No handler in this frame \u2014 abandon it and walk up.\r\n this._closeUpvaluesFor(searchFrame);\r\n if (this._frameStack.length === 0) break;\r\n searchFrame = this._frameStack.pop();\r\n this._currentFrame = searchFrame;\r\n }\r\n\r\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\r\n\r\n var h = handledFrame._handlerStack.pop();\r\n // Restore the VM value stack to the depth recorded at TRY_SETUP time,\r\n // then push the caught exception so the catch binding can store it.\r\n this._stack.length = h.stackDepth;\r\n this._push(err);\r\n // Discard any call-frames that were pushed inside the try body\r\n // (functions called from within the try block that are still live).\r\n this._frameStack.length = h.frameStackDepth;\r\n // Jump to the catch block.\r\n handledFrame._pc = h.handlerPc;\r\n this._currentFrame = handledFrame;\r\n }\r\n }\r\n};\r\n\r\n// Boot\r\nvar globals = {}; // global object for globals\r\n\r\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\r\n// with a local `window` variable (e.g. the test harness fake window).\r\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\r\n globals[k] = globalThis[k];\r\n}\r\n// If a window object is in scope (browser or test harness), capture it\r\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\r\nif (typeof window !== \"undefined\") {\r\n globals[\"window\"] = window;\r\n}\r\n\r\n// Transfer common primitives\r\nglobals.undefined = undefined;\r\nglobals.Infinity = Infinity;\r\nglobals.NaN = NaN;\r\n\r\nvar vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, CONSTANTS, globals);\r\nvm.run();\r\n";
20
+ const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue\n// While the outer frame is alive: reads/writes go to frame.regs[slot].\n// After the outer frame returns (closed): reads/writes hit this._value.\nfunction Upvalue(frame, slot) {\n this._frame = frame;\n this._slot = slot;\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._frame.regs[this._slot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._frame.regs[this._slot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._frame.regs[this._slot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg) {\n this.closure = closure;\n this.regs = new Array(closure.fn.regCount).fill(undefined);\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0; // register in parent to write return value\n this._newObj = null; // <- set by NEW so RETURN can see it\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc, // <- where main begins\n };\n this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0);\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Reuse existing open upvalue for this frame+slot if one exists.\n // This is what makes two closures share the same mutable cell.\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (uv._frame === frame && uv._slot === slot) return uv;\n }\n var uv = new Upvalue(frame, slot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN - close every upvalue that was pointing into this frame.\n // After this, closures that captured from the frame read from upvalue.value.\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (uv._frame === frame) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n frame.regs.fill(undefined);\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n frame.regs[dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n frame.regs[dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n frame.regs[dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n frame.regs[dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n frame.regs[dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n frame.regs[dst] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(frame.regs[this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a + frame.regs[this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a - frame.regs[this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a * frame.regs[this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a / frame.regs[this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a % frame.regs[this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a & frame.regs[this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a | frame.regs[this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a ^ frame.regs[this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a << frame.regs[this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >> frame.regs[this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >>> frame.regs[this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a < frame.regs[this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a > frame.regs[this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a <= frame.regs[this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >= frame.regs[this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a === frame.regs[this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a !== frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a == frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a != frame.regs[this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a in frame.regs[this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var ctor = frame.regs[this._operand()];\n if (typeof ctor === \"function\") {\n frame.regs[dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n frame.regs[dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n frame.regs[dst] = -frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n frame.regs[dst] = +frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n frame.regs[dst] = !frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n frame.regs[dst] = ~frame.regs[this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n frame.regs[dst] = typeof frame.regs[this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n frame.regs[dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n frame.regs[dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!frame.regs[src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (frame.regs[src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, this.globals, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = frame.regs[this._operand()];\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, receiver, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var newObj = Object.create(c.prototype || null);\n var f = new Frame(c, frame._pc, frame, newObj, dst);\n f._newObj = newObj;\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n frame.regs[dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = frame.regs[this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n parentFrame.regs[frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw frame.regs[this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n );\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n sub._currentFrame = f;\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n frame.regs[dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = frame.regs[this._operand()];\n frame.regs[dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n o[key] = val;\n }\n frame.regs[dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var getterFn = frame.regs[this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var setterFn = frame.regs[this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n frame.regs[dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = frame.regs[this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n frame.regs[dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n handledFrame.regs[h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals[\"window\"] = window;\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
19
21
  export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
20
- export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE"); // Attach source node location to pseudo bytecode instructions
21
-
22
- // Opcodes
22
+ export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
23
+
24
+ // ── Opcodes ──────────────────────────────────────────────────────────────────
25
+ // Register-based encoding. Operand convention (x86 / CPython style):
26
+ // destination register first, then source registers, then immediates.
27
+ //
28
+ // dst – register index that receives the result
29
+ // src – register index holding an input value
30
+ // imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
31
+ //
32
+ // Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
33
+ // Every load: [op, dst, ...]
34
+ // Every store: [op, target, src]
35
+ // Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
36
+ // CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
23
37
  export const OP_ORIGINAL = {
38
+ // ── Loads ─────────────────────────────────────────────────────────────────
24
39
  LOAD_CONST: 0,
25
- LOAD_LOCAL: 1,
26
- STORE_LOCAL: 2,
27
- LOAD_GLOBAL: 3,
28
- STORE_GLOBAL: 4,
29
- GET_PROP: 5,
30
- ADD: 6,
31
- // a + b (both are popped)
32
- SUB: 7,
33
- // a - b
34
- MUL: 8,
35
- // a * b
36
- DIV: 9,
37
- // a / b
38
- MAKE_CLOSURE: 10,
39
- CALL: 11,
40
- CALL_METHOD: 12,
41
- RETURN: 13,
42
- POP: 14,
43
- // discard top of stack
44
- LT: 15,
45
- // pop b, pop a -> push (a < b)
46
- GT: 16,
47
- // pop b, pop a -> push (a > b)
48
- EQ: 17,
49
- // pop b, pop a -> push (a === b)
50
- JUMP: 18,
51
- // unconditional - operand = absolute bytecode index
52
- JUMP_IF_FALSE: 19,
53
- // pop value; jump if falsy
54
- LTE: 20,
55
- // a <= b
56
- GTE: 21,
57
- // a >= b
58
- NEQ: 22,
59
- // a !== b
60
- LOAD_UPVALUE: 23,
61
- // push frame.closure.upvalues[operand].read()
62
- STORE_UPVALUE: 24,
63
- // frame.closure.upvalues[operand].write(pop())
64
-
65
- // Unary
66
- UNARY_NEG: 25,
67
- // -x
68
- UNARY_POS: 26,
69
- // +x
70
- UNARY_NOT: 27,
71
- // !x
72
- UNARY_BITNOT: 28,
73
- // ~x
74
- TYPEOF: 29,
75
- // typeof x
76
- VOID: 30,
77
- // void x -> always undefined
78
-
79
- TYPEOF_SAFE: 31,
80
- // operand = name constIdx - typeof guard for undeclared globals
81
- BUILD_ARRAY: 32,
82
- // operand = element count - pops N values -> pushes array
83
- BUILD_OBJECT: 33,
84
- // operand = pair count - pops N*2 (key,val) -> pushes object
85
- SET_PROP: 34,
86
- // pop val, pop key, peek obj -> obj[key] = val (obj stays on stack)
87
- GET_PROP_COMPUTED: 35,
88
- // pop key, peek obj -> push obj[key] (computed: nums[i])
89
-
90
- MOD: 36,
91
- // a % b
92
- BAND: 37,
93
- // a & b
94
- BOR: 38,
95
- // a | b
96
- BXOR: 39,
97
- // a ^ b
98
- SHL: 40,
99
- // a << b
100
- SHR: 41,
101
- // a >> b
102
- USHR: 42,
103
- // a >>> b
104
-
105
- JUMP_IF_FALSE_OR_POP: 43,
106
- // && - if top falsy: jump (keep), else: pop, eval RHS
107
- JUMP_IF_TRUE_OR_POP: 44,
108
- // || - if top truthy: jump (keep), else: pop, eval RHS
109
-
110
- DELETE_PROP: 45,
111
- IN: 46,
112
- // a in b
113
- INSTANCEOF: 47,
114
- // a instanceof b
115
-
116
- // NEW
117
- LOAD_THIS: 48,
118
- // push frame.thisVal
119
- NEW: 49,
120
- // operand = argCount - construct a new object
121
- DUP: 50,
122
- // duplicate top of stack
123
- THROW: 51,
124
- // pop value, throw it
125
- LOOSE_EQ: 52,
126
- // a == b (abstract equality)
127
- LOOSE_NEQ: 53,
128
- // a != b (abstract inequality)
129
-
130
- FOR_IN_SETUP: 54,
131
- // pop obj -> build enumerable-key iterator -> push {keys,i}
132
- FOR_IN_NEXT: 55,
133
- // operand=exit_pc; pop iter; if done->jump; else push next key
134
-
135
- // Self-modifying bytecode
40
+ // dst, constIdx regs[dst] = constants[constIdx]
41
+ LOAD_INT: 1,
42
+ // dst, imm regs[dst] = imm (raw u16 literal)
43
+ LOAD_GLOBAL: 2,
44
+ // dst, nameIdx regs[dst] = globals[constants[nameIdx]]
45
+ LOAD_UPVALUE: 3,
46
+ // dst, uvIdx regs[dst] = upvalues[uvIdx].read()
47
+ LOAD_THIS: 4,
48
+ // dst regs[dst] = frame.thisVal
49
+ MOVE: 5,
50
+ // dst, src regs[dst] = regs[src]
51
+
52
+ // ── Stores ────────────────────────────────────────────────────────────────
53
+ STORE_GLOBAL: 6,
54
+ // nameIdx, src globals[constants[nameIdx]] = regs[src]
55
+ STORE_UPVALUE: 7,
56
+ // uvIdx, src upvalues[uvIdx].write(regs[src])
57
+
58
+ // ── Property access ───────────────────────────────────────────────────────
59
+ GET_PROP: 8,
60
+ // dst, obj, key regs[dst] = regs[obj][regs[key]]
61
+ SET_PROP: 9,
62
+ // obj, key, val regs[obj][regs[key]] = regs[val] (result stays in val reg)
63
+ DELETE_PROP: 10,
64
+ // dst, obj, key regs[dst] = delete regs[obj][regs[key]]
65
+
66
+ // ── Arithmetic / bitwise (dst, src1, src2) ───────────────────────────────
67
+ ADD: 11,
68
+ SUB: 12,
69
+ MUL: 13,
70
+ DIV: 14,
71
+ MOD: 15,
72
+ BAND: 16,
73
+ BOR: 17,
74
+ BXOR: 18,
75
+ SHL: 19,
76
+ SHR: 20,
77
+ USHR: 21,
78
+ // ── Comparison (dst, src1, src2) ─────────────────────────────────────────
79
+ LT: 22,
80
+ GT: 23,
81
+ LTE: 24,
82
+ GTE: 25,
83
+ EQ: 26,
84
+ NEQ: 27,
85
+ LOOSE_EQ: 28,
86
+ LOOSE_NEQ: 29,
87
+ IN: 30,
88
+ INSTANCEOF: 31,
89
+ // ── Unary (dst, src) ─────────────────────────────────────────────────────
90
+ UNARY_NEG: 32,
91
+ UNARY_POS: 33,
92
+ UNARY_NOT: 34,
93
+ UNARY_BITNOT: 35,
94
+ TYPEOF: 36,
95
+ // dst, src
96
+ VOID: 37,
97
+ // dst, src – regs[dst] = undefined (src evaluated for side-effects)
98
+ TYPEOF_SAFE: 38,
99
+ // dst, nameConstIdx safe typeof for potentially-undeclared globals
100
+
101
+ // ── Control flow ──────────────────────────────────────────────────────────
102
+ JUMP: 39,
103
+ // target
104
+ JUMP_IF_FALSE: 40,
105
+ // src, target if !regs[src] then pc = target
106
+ JUMP_IF_TRUE: 41,
107
+ // src, target if regs[src] then pc = target (|| short-circuit)
108
+
109
+ // ── Calls & constructors ──────────────────────────────────────────────────
110
+ CALL: 42,
111
+ // dst, callee, argc, [argRegs…]
112
+ CALL_METHOD: 43,
113
+ // dst, receiver, callee, argc, [argRegs…]
114
+ NEW: 44,
115
+ // dst, callee, argc, [argRegs…]
116
+ RETURN: 45,
117
+ // src
118
+ THROW: 46,
119
+ // src
120
+
121
+ // ── Closures ──────────────────────────────────────────────────────────────
122
+ // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
123
+ MAKE_CLOSURE: 47,
124
+ // ── Collections ───────────────────────────────────────────────────────────
125
+ BUILD_ARRAY: 48,
126
+ // dst, count, [elemRegs…]
127
+ BUILD_OBJECT: 49,
128
+ // dst, pairCount, [keyReg, valReg, …]
129
+
130
+ // ── Property definitions (getters / setters) ──────────────────────────────
131
+ DEFINE_GETTER: 50,
132
+ // obj, key, fn
133
+ DEFINE_SETTER: 51,
134
+ // obj, key, fn
135
+
136
+ // ── For-in iteration ──────────────────────────────────────────────────────
137
+ FOR_IN_SETUP: 52,
138
+ // dst, src dst = { _keys: enumKeys(src), i: 0 }
139
+ FOR_IN_NEXT: 53,
140
+ // dst, iter, exitTarget
141
+
142
+ // ── Exception handling ────────────────────────────────────────────────────
143
+ TRY_SETUP: 54,
144
+ // handlerPc, exceptionReg
145
+ TRY_END: 55,
146
+ // ── Self-modifying bytecode ───────────────────────────────────────────────
136
147
  PATCH: 56,
137
- // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
138
-
139
- // Try-Catch
140
- TRY_SETUP: 57,
141
- // operand = catch_pc; push exception handler onto frame._handlerStack
142
- TRY_END: 58,
143
- // pop exception handler (normal exit from try body)
144
-
145
- // Getter / Setter (ES5 object literal accessor syntax)
146
- DEFINE_GETTER: 59,
147
- // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {get: fn})
148
- DEFINE_SETTER: 60,
149
- // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
150
-
151
- DEBUGGER: 61,
152
- // emits a "debugger" statement
153
-
154
- // Push the raw integer operand directly onto the stack (no constant pool lookup).
155
- // Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
156
- // raw PC number that becomes the operand, which is pushed as-is at runtime.
157
- LOAD_INT: 62
148
+ // destPc, sliceStart, sliceEnd
149
+
150
+ // ── Debug ─────────────────────────────────────────────────────────────────
151
+ DEBUGGER: 57
158
152
  };
159
153
 
160
- // Scope
161
- // Each function call gets its own Scope. Locals are resolved to
162
- // numeric slots at compile time -- zero name lookups at runtime.
154
+ // ── Scope ─────────────────────────────────────────────────────────────────────
155
+ // Maps variable names to register indices (slot numbers).
156
+ // Locals are allocated at compile time; zero name lookups at runtime.
163
157
  class Scope {
164
158
  constructor(parent = null) {
165
159
  this.parent = parent;
166
- this._locals = new Map(); // name -> slot index
160
+ this._locals = new Map();
167
161
  this._next = 0;
168
162
  }
169
163
  define(name) {
@@ -172,8 +166,6 @@ class Scope {
172
166
  }
173
167
  return this._locals.get(name);
174
168
  }
175
-
176
- // Walk up scope chain. If we fall off the top -> global.
177
169
  resolve(name) {
178
170
  if (this._locals.has(name)) {
179
171
  return {
@@ -191,21 +183,33 @@ class Scope {
191
183
  }
192
184
  }
193
185
 
194
- // FnContext
186
+ // ── FnContext ─────────────────────────────────────────────────────────────────
195
187
  // Compiler-side state for the function currently being compiled.
196
- // Distinct from runtime Frame -- this is compile-time only.
188
+ // Distinct from the runtime Frame this is compile-time only.
197
189
  class FnContext {
190
+ // Register allocator. Locals occupy regs[0..scope.localCount-1];
191
+ // temps are allocated above that and freed at statement boundaries.
192
+ regTop = 0;
193
+ maxRegTop = 0;
198
194
  constructor(compiler, parentCtx = null) {
199
195
  this.compiler = compiler;
200
196
  this.parentCtx = parentCtx;
201
197
  this.scope = new Scope();
202
198
  this.bc = [];
203
- this.upvalues = []; // { name, isLocal, index }
199
+ this.upvalues = [];
200
+ }
201
+
202
+ /** Allocate the next free temp register and return its index. */
203
+ allocReg() {
204
+ const r = this.regTop++;
205
+ if (this.regTop > this.maxRegTop) this.maxRegTop = this.regTop;
206
+ return r;
204
207
  }
205
208
 
206
- // Find or register a captured variable as an upvalue.
207
- // isLocal=true -> captured directly from parent's locals[index]
208
- // isLocal=false -> relayed from parent's own upvalue list[index]
209
+ /** Release all temps above the local region. Called at each statement boundary. */
210
+ resetTemps() {
211
+ this.regTop = this.scope.localCount;
212
+ }
209
213
  addUpvalue(name, isLocal, index) {
210
214
  const existing = this.upvalues.findIndex(u => u.name === name);
211
215
  if (existing !== -1) return existing;
@@ -213,41 +217,35 @@ class FnContext {
213
217
  this.upvalues.push({
214
218
  name,
215
219
  isLocal,
216
- index: index
220
+ index
217
221
  });
218
222
  return idx;
219
223
  }
220
224
  }
221
-
222
- // Compiler
225
+ // ── Compiler ──────────────────────────────────────────────────────────────────
223
226
  export class Compiler {
224
227
  emit(bc, instr, node) {
225
228
  bc.push(instr);
226
229
  instr[SOURCE_NODE_SYM] = node;
227
230
  }
228
-
229
- // DO NOT USE THIS KEY UNLESS YOU ARE "RESOLVE CONSTANTS"
230
- // CONSTANTS DURING COMPILATION MUST BE USED BY REFERENCE WITH b.constantOperand("myConstantHere")
231
-
232
231
  constructor(options = DEFAULT_OPTIONS) {
233
232
  this.options = options;
234
- this.fnDescriptors = []; // populated in pass 1
233
+ this.fnDescriptors = [];
235
234
  this.bytecode = [];
236
235
  this.mainStartPc = 0;
237
- this._currentCtx = null; // FnContext of the function being compiled, null at top-level
238
- this._loopStack = []; // per active loop/switch/block/try
236
+ this.mainRegCount = 0;
237
+ this._currentCtx = null;
238
+ this._loopStack = [];
239
239
  this._pendingLabel = null;
240
- this._forInCount = 0; // counter for synthetic for-in iterator global names
241
- this._labelCount = 0; // monotonically increasing counter for unique label names
242
-
240
+ this._forInCount = 0;
241
+ this._labelCount = 0;
243
242
  this.serializer = new Serializer(this);
244
243
  this.MACRO_OPS = {};
245
244
  this.SPECIALIZED_OPS = {};
245
+ this.ALIASED_OPS = {};
246
246
  this.OP = {
247
247
  ...OP_ORIGINAL
248
248
  };
249
-
250
- // Construct randomized opcode mapping
251
249
  if (this.options.randomizeOpcodes) {
252
250
  let usedNumbers = new Set();
253
251
  for (const key in this.OP) {
@@ -259,59 +257,122 @@ export class Compiler {
259
257
  this.OP[key] = val;
260
258
  }
261
259
  }
262
-
263
- // Reverse map for comment generation
264
260
  this.OP_NAME = Object.fromEntries(Object.entries(this.OP).map(([k, v]) => [v, k]));
265
- this.JUMP_OPS = new Set([this.OP.JUMP, this.OP.JUMP_IF_FALSE, this.OP.JUMP_IF_TRUE_OR_POP, this.OP.JUMP_IF_FALSE_OR_POP, this.OP.FOR_IN_NEXT, this.OP.TRY_SETUP // catch_pc operand needs offset adjustment like jump targets
266
- ]);
261
+ this.JUMP_OPS = new Set([this.OP.JUMP, this.OP.JUMP_IF_FALSE, this.OP.JUMP_IF_TRUE, this.OP.FOR_IN_NEXT, this.OP.TRY_SETUP]);
267
262
  }
268
-
269
- // Generate a globally unique label string with an optional hint for readability.
270
263
  _makeLabel(hint = "") {
271
- var id = this._labelCount++;
272
- return `${hint || "L"}_${id}`;
264
+ return `${hint || "L"}_${this._labelCount++}`;
273
265
  }
274
-
275
- // Variable resolution
276
- // Walks up the FnContext chain. Crossing a context boundary means
277
- // we're capturing from an outer function - register an upvalue.
278
266
  _resolve(name, ctx) {
279
267
  if (!ctx) return {
280
268
  kind: "global"
281
269
  };
282
-
283
- // 1. Own locals
284
270
  if (ctx.scope._locals.has(name)) {
285
271
  return {
286
272
  kind: "local",
287
273
  slot: ctx.scope._locals.get(name)
288
274
  };
289
275
  }
290
-
291
- // 2. No parent context -> must be global
292
276
  if (!ctx.parentCtx) return {
293
277
  kind: "global"
294
278
  };
295
-
296
- // 3. Ask parent -- recurse up the chain
297
279
  const parentResult = this._resolve(name, ctx.parentCtx);
298
280
  if (parentResult.kind === "global") return {
299
281
  kind: "global"
300
282
  };
301
-
302
- // 4. Parent has it (as local or upvalue) -- register an upvalue here.
303
- // isLocal=true means "take it straight from parent's locals[index]"
304
- // isLocal=false means "relay parent's upvalue[index]" (multi-level capture)
305
283
  const isLocal = parentResult.kind === "local";
306
284
  const index = isLocal ? parentResult.slot : parentResult.index;
307
- const uvIdx = ctx.addUpvalue(name, isLocal, index);
285
+ const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
308
286
  return {
309
287
  kind: "upvalue",
310
288
  index: uvIdx
311
289
  };
312
290
  }
313
291
 
314
- // Entry point
292
+ // ── Variable hoisting ──────────────────────────────────────────────────────
293
+ // Pre-scan a statement list and reserve scope slots for every var declaration,
294
+ // function declaration, for-in iterator, and try-catch binding.
295
+ // Must be called before compilation begins so that ctx.regTop can be set
296
+ // safely above ALL locals (including those declared late in the body).
297
+ _hoistVars(stmts, scope) {
298
+ for (const stmt of stmts) {
299
+ switch (stmt.type) {
300
+ case "VariableDeclaration":
301
+ for (const decl of stmt.declarations) {
302
+ if (decl.id.type === "Identifier") scope.define(decl.id.name);
303
+ }
304
+ break;
305
+ case "FunctionDeclaration":
306
+ if (stmt.id) scope.define(stmt.id.name);
307
+ break;
308
+ case "BlockStatement":
309
+ this._hoistVars(stmt.body, scope);
310
+ break;
311
+ case "IfStatement":
312
+ {
313
+ const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
314
+ this._hoistVars(cons, scope);
315
+ if (stmt.alternate) {
316
+ const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
317
+ this._hoistVars(alt, scope);
318
+ }
319
+ break;
320
+ }
321
+ case "WhileStatement":
322
+ case "DoWhileStatement":
323
+ {
324
+ const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
325
+ this._hoistVars(body, scope);
326
+ break;
327
+ }
328
+ case "ForStatement":
329
+ {
330
+ if (stmt.init?.type === "VariableDeclaration") {
331
+ for (const decl of stmt.init.declarations) {
332
+ if (decl.id.type === "Identifier") scope.define(decl.id.name);
333
+ }
334
+ }
335
+ const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
336
+ this._hoistVars(body, scope);
337
+ break;
338
+ }
339
+ case "ForInStatement":
340
+ {
341
+ // Reserve a hidden register for the iterator object.
342
+ stmt._iterSlot = scope._next++;
343
+ if (stmt.left.type === "VariableDeclaration") {
344
+ for (const decl of stmt.left.declarations) {
345
+ if (decl.id.type === "Identifier") scope.define(decl.id.name);
346
+ }
347
+ }
348
+ const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
349
+ this._hoistVars(body, scope);
350
+ break;
351
+ }
352
+ case "SwitchStatement":
353
+ for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
354
+ break;
355
+ case "TryStatement":
356
+ this._hoistVars(stmt.block.body, scope);
357
+ if (stmt.handler) {
358
+ if (stmt.handler.param?.type === "Identifier") {
359
+ // Catch parameter IS the exception register.
360
+ scope.define(stmt.handler.param.name);
361
+ } else {
362
+ // No catch binding – reserve a dummy slot for the exception value.
363
+ stmt._exceptionSlot = scope._next++;
364
+ }
365
+ this._hoistVars(stmt.handler.body.body, scope);
366
+ }
367
+ break;
368
+ case "LabeledStatement":
369
+ this._hoistVars([stmt.body], scope);
370
+ break;
371
+ }
372
+ }
373
+ }
374
+
375
+ // ── Entry point ───────────────────────────────────────────────────────────
315
376
  compile(source) {
316
377
  const ast = parse(source, {
317
378
  sourceType: "script"
@@ -319,146 +380,127 @@ export class Compiler {
319
380
  return this.compileAST(ast);
320
381
  }
321
382
  compileAST(ast) {
322
- // Pass 1 - compile every FunctionDeclaration into a descriptor.
323
- // Traverse finds them regardless of nesting depth.
324
383
  traverse(ast, {
325
384
  FunctionDeclaration: path => {
326
- // Only handle top-level functions for this MVP.
327
- // (Parent is Program node)
328
385
  if (path.parent.type !== "Program") return;
329
386
  this._compileFunctionDecl(path.node);
330
- path.skip(); // don't recurse into nested functions
387
+ path.skip();
331
388
  }
332
389
  });
333
-
334
- // Pass 2 -- compile top-level statements into BYTECODE.
335
390
  this._compileMain(ast.program.body);
336
391
  return this.bytecode;
337
392
  }
338
393
 
339
- // Function Declaration
340
-
394
+ // ── Function compilation ───────────────────────────────────────────────────
341
395
  _compileFunctionDecl(node) {
342
- // Reserve a slot in fnDescriptors NOW, before compiling the body, so that
343
- // any nested _compileFunctionDecl calls see the correct .length and get a
344
- // distinct _fnIdx. The placeholder object is mutated in-place below once
345
- // the body and header are ready.
396
+ ok(!node.generator, "Generator functions are not supported");
397
+ ok(!node.async, "Async functions are not supported");
346
398
  var fnIdx = this.fnDescriptors.length;
347
399
  const entryLabel = this._makeLabel(`fn_${fnIdx}`);
348
- var desc = {}; // placeholder — filled in after compilation
400
+ var desc = {};
349
401
  this.fnDescriptors.push(desc);
350
-
351
- // Create a context whose parent is whatever we're currently compiling.
352
- // This is what lets _resolve cross function boundaries correctly.
353
402
  const ctx = new FnContext(this, this._currentCtx);
354
403
  const savedCtx = this._currentCtx;
355
404
  this._currentCtx = ctx;
356
-
357
- // Isolate the loop stack so that try/loop entries from the outer scope
358
- // don't cause spurious TRY_END / extra jumps inside this function body.
359
405
  const savedLoopStack = this._loopStack;
360
406
  this._loopStack = [];
361
407
 
362
- // Params occupy the first N local slots (args are copied in on CALL)
408
+ // 1. Define parameters (occupy regs 0..paramCount-1).
363
409
  for (const param of node.params) {
364
410
  let identifier = param.type === "AssignmentPattern" ? param.left : param;
365
411
  ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
366
412
  ctx.scope.define(identifier.name);
367
413
  }
368
414
 
369
- // Reserve the next slot for the implicit `arguments` object.
370
- // Slot index will always equal paramCount (params are 0..paramCount-1).
415
+ // 2. Reserve the `arguments` slot (reg index = paramCount).
371
416
  ctx.scope.define("arguments");
372
417
 
373
- // Pass 2: emit default-value guards at top of fn body
374
- // Mirrors what JS engines do: if the caller passed undefined (or
375
- // nothing), evaluate the default expression and overwrite the slot.
418
+ // 3. Hoist all var declarations so temps start above every local.
419
+ this._hoistVars(node.body.body, ctx.scope);
420
+
421
+ // 4. Temps now start above all locals.
422
+ ctx.regTop = ctx.scope.localCount;
423
+ ctx.maxRegTop = ctx.regTop;
424
+
425
+ // 5. Emit default-value guards.
376
426
  for (const param of node.params) {
377
427
  if (param.type !== "AssignmentPattern") continue;
378
428
  const slot = ctx.scope._locals.get(param.left.name);
379
429
  const skipLabel = this._makeLabel("param_skip");
380
430
 
381
- // if (param === undefined) param = <default expr>
382
- this.emit(ctx.bc, [this.OP.LOAD_LOCAL, slot], param);
383
- this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], param);
384
- this.emit(ctx.bc, [this.OP.EQ], param);
385
- this.emit(ctx.bc, [this.OP.JUMP_IF_FALSE, {
431
+ // if (param === undefined) param = <default>
432
+ const reg_undef = ctx.allocReg();
433
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], param);
434
+ const reg_cmp = ctx.allocReg();
435
+ this.emit(ctx.bc, [this.OP.EQ, reg_cmp, slot, reg_undef], param);
436
+ this.emit(ctx.bc, [this.OP.JUMP_IF_FALSE, reg_cmp, {
386
437
  type: "label",
387
438
  label: skipLabel
388
439
  }], param);
389
- this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
390
- this.emit(ctx.bc, [this.OP.STORE_LOCAL, slot], param);
440
+ ctx.resetTemps();
441
+ const srcReg = this._compileExpr(param.right, ctx.scope, ctx.bc);
442
+ if (srcReg !== slot) {
443
+ this.emit(ctx.bc, [this.OP.MOVE, slot, srcReg], param);
444
+ }
445
+ ctx.resetTemps();
391
446
  this.emit(ctx.bc, [null, {
392
447
  type: "defineLabel",
393
448
  label: skipLabel
394
449
  }], param);
395
450
  }
451
+
452
+ // 6. Compile body.
396
453
  for (const stmt of node.body.body) {
397
454
  this._compileStatement(stmt, ctx.scope, ctx.bc);
398
455
  }
399
456
 
400
- // If we fall off the end of the function, implicitly return undefined.
401
- this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
402
- this.emit(ctx.bc, [this.OP.RETURN], node);
403
- this._currentCtx = savedCtx; // restore before touching fnDescriptors
457
+ // Implicit return undefined at end of function.
458
+ const reg_undef = ctx.allocReg();
459
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], node);
460
+ this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
461
+ this._currentCtx = savedCtx;
404
462
  this._loopStack = savedLoopStack;
405
463
  node._fnIdx = fnIdx;
406
-
407
- // Fill the placeholder that was reserved at the top of this function.
408
- // Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
409
- // as inline operands on the MAKE_CLOSURE instruction via _emitMakeClosure.
410
464
  desc.name = node.id?.name || "<anonymous>";
411
465
  desc.entryLabel = entryLabel;
412
466
  desc.bytecode = ctx.bc;
413
467
  desc._fnIdx = fnIdx;
414
468
  desc.paramCount = node.params.length;
415
- desc.localCount = ctx.scope.localCount;
469
+ desc.regCount = ctx.maxRegTop; // total registers needed at runtime
416
470
  desc.upvalues = ctx.upvalues.slice();
417
471
  return desc;
418
472
  }
419
473
 
420
- // Emit a single MAKE_CLOSURE instruction with all closure metadata packed
421
- // as inline operands. The runtime reads them via _operand() — no stack
422
- // shuffling needed.
423
- //
424
- // Flat operand layout: startPc, paramCount, localCount, uvCount,
425
- // [isLocal_0, idx_0, isLocal_1, idx_1, ...]
474
+ // Emit MAKE_CLOSURE with all metadata as inline operands.
475
+ // Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
426
476
  _emitMakeClosure(desc, node, bc) {
477
+ const ctx = this._currentCtx;
478
+ const dst = ctx.allocReg();
427
479
  const uvOperands = [];
428
480
  for (const uv of desc.upvalues) {
429
481
  uvOperands.push(uv.isLocal ? 1 : 0);
430
482
  uvOperands.push(uv.index);
431
483
  }
432
- this.emit(bc, [this.OP.MAKE_CLOSURE, {
484
+ this.emit(bc, [this.OP.MAKE_CLOSURE, dst, {
433
485
  type: "label",
434
486
  label: desc.entryLabel
435
- }, desc.paramCount, desc.localCount, desc.upvalues.length, ...uvOperands], node);
487
+ }, desc.paramCount, desc.regCount, desc.upvalues.length, ...uvOperands], node);
488
+ return dst;
436
489
  }
437
490
 
438
- // Main (top-level)
491
+ // ── Main (top-level) ───────────────────────────────────────────────────────
439
492
  _compileMain(body) {
440
- const bc = this.bytecode;
441
-
442
- // Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
443
- // (mirrors JS hoisting -- functions are available before other code)
444
- for (const node of body) {
445
- if (node.type !== "FunctionDeclaration") continue;
446
- const desc = this.fnDescriptors.find(d => d._fnIdx === node._fnIdx);
447
- const nameRef = b.constantOperand(node.id.name);
448
- this._emitMakeClosure(desc, node, bc);
449
- this.emit(bc, [this.OP.STORE_GLOBAL, nameRef], node);
450
- }
451
-
452
- // Compile everything else in order
453
- for (const node of body) {
454
- if (node.type === "FunctionDeclaration") continue;
455
- this._compileStatement(node, null, bc); // null scope -> global context
456
- }
457
- this.emit(bc, [this.OP.RETURN], null); // end program
458
-
459
- // Append all function bodies. Each function's entryLabel (already generated
460
- // in _compileFunctionDecl) points directly to the first body instruction;
461
- // metadata is pushed onto the stack at each call site, not stored inline.
493
+ const mainCtx = new FnContext(this, null);
494
+ const savedCtx = this._currentCtx;
495
+ this._currentCtx = mainCtx;
496
+ var desc = this._compileFunctionDecl({
497
+ type: "FunctionDeclaration",
498
+ async: false,
499
+ generator: false,
500
+ params: [],
501
+ id: null,
502
+ body: t.blockStatement([...body])
503
+ });
462
504
  for (const descriptor of this.fnDescriptors) {
463
505
  this.bytecode.push([null, {
464
506
  type: "defineLabel",
@@ -468,88 +510,103 @@ export class Compiler {
468
510
  this.bytecode.push(instr);
469
511
  }
470
512
  }
513
+ this.mainRegCount = mainCtx.maxRegTop;
514
+ this.mainFn = desc;
515
+ this._currentCtx = savedCtx;
471
516
  }
472
517
 
473
- // Statements
518
+ // ── Statements ────────────────────────────────────────────────────────────
519
+ // Wrapper that resets temps after every statement so that short-lived
520
+ // expression temps don't accumulate across statements.
474
521
  _compileStatement(node, scope, bc) {
522
+ this._compileStatementImpl(node, scope, bc);
523
+ this._currentCtx?.resetTemps();
524
+ }
525
+ _compileStatementImpl(node, scope, bc) {
526
+ const ctx = this._currentCtx;
475
527
  switch (node.type) {
476
528
  case "EmptyStatement":
477
- {
478
- // nothing to emit -- bare semicolon is a no-op
479
- break;
480
- }
529
+ break;
481
530
  case "DebuggerStatement":
482
531
  this.emit(bc, [this.OP.DEBUGGER], node);
483
532
  break;
484
533
  case "BlockStatement":
485
- {
486
- for (const stmt of node.body) {
487
- this._compileStatement(stmt, scope, bc);
488
- }
489
- break;
534
+ for (const stmt of node.body) {
535
+ this._compileStatement(stmt, scope, bc);
490
536
  }
537
+ break;
491
538
  case "FunctionDeclaration":
492
539
  {
493
- // Nested function -- compile it into a descriptor, then emit
494
- // MAKE_CLOSURE so it's captured as a live closure at runtime.
495
- // (_compileFunctionDecl pushes/pops _currentCtx internally)
496
540
  const desc = this._compileFunctionDecl(node);
497
- this._emitMakeClosure(desc, node, bc);
541
+ const closureReg = this._emitMakeClosure(desc, node, bc);
498
542
  if (scope) {
499
- const slot = scope.define(node.id.name);
500
- this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
543
+ const slot = scope._locals.get(node.id.name);
544
+ if (closureReg !== slot) {
545
+ this.emit(bc, [this.OP.MOVE, slot, closureReg], node);
546
+ }
501
547
  } else {
502
- this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)], node);
548
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name), closureReg], node);
503
549
  }
504
550
  break;
505
551
  }
506
552
  case "ThrowStatement":
507
553
  {
508
- this._compileExpr(node.argument, scope, bc);
509
- this.emit(bc, [this.OP.THROW], node);
554
+ const reg = this._compileExpr(node.argument, scope, bc);
555
+ this.emit(bc, [this.OP.THROW, reg], node);
510
556
  break;
511
557
  }
512
558
  case "ReturnStatement":
513
559
  {
560
+ let reg;
514
561
  if (node.argument) {
515
- this._compileExpr(node.argument, scope, bc);
562
+ reg = this._compileExpr(node.argument, scope, bc);
516
563
  } else {
517
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
564
+ reg = ctx.allocReg();
565
+ this.emit(bc, [this.OP.LOAD_CONST, reg, b.constantOperand(undefined)], node);
518
566
  }
519
- // Disarm any open try handlers before leaving the function.
520
- // TRY_END only touches frame._handlerStack, not the value stack,
521
- // so the return value sitting on top is safe.
522
567
  for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
523
568
  if (this._loopStack[_ri].type === "try") {
524
569
  this.emit(bc, [this.OP.TRY_END], node);
525
570
  }
526
571
  }
527
- this.emit(bc, [this.OP.RETURN], node);
572
+ this.emit(bc, [this.OP.RETURN, reg], node);
528
573
  break;
529
574
  }
530
575
  case "ExpressionStatement":
531
- {
532
- this._compileExpr(node.expression, scope, bc);
533
- this.emit(bc, [this.OP.POP], node); // discard return value of statement-level expressions
534
- break;
535
- }
576
+ this._compileExpr(node.expression, scope, bc);
577
+ // Result is discarded; resetTemps in the wrapper handles cleanup.
578
+ break;
536
579
  case "VariableDeclaration":
537
580
  {
538
581
  for (const decl of node.declarations) {
539
- // Push the initialiser (or undefined if absent)
540
- if (decl.init) {
541
- this._compileExpr(decl.init, scope, bc);
542
- } else {
543
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
544
- }
545
582
  ok(decl.id.type === "Identifier", "Only simple identifiers can be declared");
546
-
547
- // Store: local slot if inside a function, global name otherwise
583
+ const name = decl.id.name;
548
584
  if (scope) {
549
- const slot = scope.define(decl.id.name);
550
- this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
585
+ const slot = scope._locals.get(name); // already defined by _hoistVars
586
+ if (decl.init) {
587
+ const srcReg = this._compileExpr(decl.init, scope, bc);
588
+ if (srcReg !== slot) {
589
+ this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
590
+ }
591
+ } else {
592
+ this.emit(bc, [this.OP.MOVE, slot, ctx.allocReg()], node);
593
+ // Load undefined into the just-allocated temp, then move.
594
+ // Actually: just emit LOAD_CONST directly into slot.
595
+ // Undo the allocReg – instead emit directly:
596
+ ctx.regTop--; // undo the allocReg above
597
+ const tmp = ctx.allocReg();
598
+ this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
599
+ if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
600
+ }
551
601
  } else {
552
- this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(decl.id.name)], node);
602
+ if (decl.init) {
603
+ const srcReg = this._compileExpr(decl.init, scope, bc);
604
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), srcReg], node);
605
+ } else {
606
+ const tmp = ctx.allocReg();
607
+ this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
608
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), tmp], node);
609
+ }
553
610
  }
554
611
  }
555
612
  break;
@@ -557,42 +614,37 @@ export class Compiler {
557
614
  case "IfStatement":
558
615
  {
559
616
  const elseOrEndLabel = this._makeLabel("if_else");
560
- // 1. Compile the test expression -> leaves a value on the stack
561
- this._compileExpr(node.test, scope, bc);
562
- // 2. Emit JUMP_IF_FALSE to the else branch (or end if no else)
563
- this.emit(bc, [this.OP.JUMP_IF_FALSE, {
617
+ const savedTop = ctx.regTop;
618
+ const testReg = this._compileExpr(node.test, scope, bc);
619
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
564
620
  type: "label",
565
621
  label: elseOrEndLabel
566
622
  }], node);
567
- // 3. Compile the consequent block (the "then" branch)
623
+ ctx.regTop = savedTop; // free test temps
624
+
568
625
  const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
569
626
  for (const stmt of consequentBody) {
570
627
  this._compileStatement(stmt, scope, bc);
571
628
  }
572
629
  if (node.alternate) {
573
- // 4a. Consequent needs to jump OVER the else block when done
574
630
  const endLabel = this._makeLabel("if_end");
575
631
  this.emit(bc, [this.OP.JUMP, {
576
632
  type: "label",
577
633
  label: endLabel
578
634
  }], node);
579
- // Mark start of else
580
635
  this.emit(bc, [null, {
581
636
  type: "defineLabel",
582
637
  label: elseOrEndLabel
583
638
  }], node);
584
- // 5. Compile the alternate (else) block
585
- const altBody = node.alternate.type === "BlockStatement" ? node.alternate.body : [node.alternate]; // handles `else if` -- it's just a nested IfStatement
639
+ const altBody = node.alternate.type === "BlockStatement" ? node.alternate.body : [node.alternate];
586
640
  for (const stmt of altBody) {
587
641
  this._compileStatement(stmt, scope, bc);
588
642
  }
589
- // Mark end (consequent's jump lands here)
590
643
  this.emit(bc, [null, {
591
644
  type: "defineLabel",
592
645
  label: endLabel
593
646
  }], node);
594
647
  } else {
595
- // 4b. No else -- label lands right after the then block
596
648
  this.emit(bc, [null, {
597
649
  type: "defineLabel",
598
650
  label: elseOrEndLabel
@@ -610,17 +662,19 @@ export class Compiler {
610
662
  type: "loop",
611
663
  label: _wLabel,
612
664
  breakLabel: exitLabel,
613
- continueLabel: loopTopLabel // continue re-evaluates the test
665
+ continueLabel: loopTopLabel
614
666
  });
615
667
  this.emit(bc, [null, {
616
668
  type: "defineLabel",
617
669
  label: loopTopLabel
618
670
  }], node);
619
- this._compileExpr(node.test, scope, bc);
620
- this.emit(bc, [this.OP.JUMP_IF_FALSE, {
671
+ const savedTop = ctx.regTop;
672
+ const testReg = this._compileExpr(node.test, scope, bc);
673
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
621
674
  type: "label",
622
675
  label: exitLabel
623
676
  }], node);
677
+ ctx.regTop = savedTop;
624
678
  const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
625
679
  for (const stmt of whileBody) {
626
680
  this._compileStatement(stmt, scope, bc);
@@ -647,7 +701,7 @@ export class Compiler {
647
701
  type: "loop",
648
702
  label: _dwLabel,
649
703
  breakLabel: exitLabel,
650
- continueLabel: continueLabel // continue falls to the test
704
+ continueLabel: continueLabel
651
705
  });
652
706
  this.emit(bc, [null, {
653
707
  type: "defineLabel",
@@ -657,17 +711,17 @@ export class Compiler {
657
711
  for (const stmt of doWhileBody) {
658
712
  this._compileStatement(stmt, scope, bc);
659
713
  }
660
-
661
- // continue -> skip rest of body, fall through to test
662
714
  this.emit(bc, [null, {
663
715
  type: "defineLabel",
664
716
  label: continueLabel
665
717
  }], node);
666
- this._compileExpr(node.test, scope, bc);
667
- this.emit(bc, [this.OP.JUMP_IF_FALSE, {
718
+ const savedTop = ctx.regTop;
719
+ const testReg = this._compileExpr(node.test, scope, bc);
720
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
668
721
  type: "label",
669
722
  label: exitLabel
670
723
  }], node);
724
+ ctx.regTop = savedTop;
671
725
  this.emit(bc, [this.OP.JUMP, {
672
726
  type: "label",
673
727
  label: loopTopLabel
@@ -685,7 +739,6 @@ export class Compiler {
685
739
  this._pendingLabel = null;
686
740
  const loopTopLabel = this._makeLabel("for_top");
687
741
  const exitLabel = this._makeLabel("for_exit");
688
- // continue jumps to the update clause if present, else straight to test
689
742
  const updateLabel = node.update ? this._makeLabel("for_update") : loopTopLabel;
690
743
  this._loopStack.push({
691
744
  type: "loop",
@@ -698,7 +751,7 @@ export class Compiler {
698
751
  this._compileStatement(node.init, scope, bc);
699
752
  } else {
700
753
  this._compileExpr(node.init, scope, bc);
701
- this.emit(bc, [this.OP.POP], node);
754
+ // result discarded; resetTemps in next iteration
702
755
  }
703
756
  }
704
757
  this.emit(bc, [null, {
@@ -706,25 +759,25 @@ export class Compiler {
706
759
  label: loopTopLabel
707
760
  }], node);
708
761
  if (node.test) {
709
- this._compileExpr(node.test, scope, bc);
710
- this.emit(bc, [this.OP.JUMP_IF_FALSE, {
762
+ const savedTop = ctx.regTop;
763
+ const testReg = this._compileExpr(node.test, scope, bc);
764
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
711
765
  type: "label",
712
766
  label: exitLabel
713
767
  }], node);
768
+ ctx.regTop = savedTop;
714
769
  }
715
770
  const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
716
771
  for (const stmt of forBody) {
717
772
  this._compileStatement(stmt, scope, bc);
718
773
  }
719
-
720
- // continue -> run update (if any) then back to test
721
774
  if (node.update) {
722
775
  this.emit(bc, [null, {
723
776
  type: "defineLabel",
724
777
  label: updateLabel
725
778
  }], node);
726
779
  this._compileExpr(node.update, scope, bc);
727
- this.emit(bc, [this.OP.POP], node);
780
+ ctx.resetTemps(); // discard update expression result
728
781
  }
729
782
  this.emit(bc, [this.OP.JUMP, {
730
783
  type: "label",
@@ -739,7 +792,6 @@ export class Compiler {
739
792
  }
740
793
  case "BreakStatement":
741
794
  {
742
- // Find the jump target in the loop stack.
743
795
  let _bTargetIdx = -1;
744
796
  if (node.label) {
745
797
  const _bLabelName = node.label.name;
@@ -751,7 +803,6 @@ export class Compiler {
751
803
  }
752
804
  if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
753
805
  } else {
754
- // Find innermost loop/switch/block (skip "try" entries)
755
806
  for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
756
807
  if (this._loopStack[_bi].type !== "try") {
757
808
  _bTargetIdx = _bi;
@@ -760,7 +811,6 @@ export class Compiler {
760
811
  }
761
812
  if (_bTargetIdx === -1) throw new Error("break outside loop");
762
813
  }
763
- // Emit TRY_END for every open try block between here and the target.
764
814
  for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
765
815
  if (this._loopStack[_bi].type === "try") {
766
816
  this.emit(bc, [this.OP.TRY_END], node);
@@ -774,7 +824,6 @@ export class Compiler {
774
824
  }
775
825
  case "ContinueStatement":
776
826
  {
777
- // Find the target loop in the loop stack.
778
827
  let _cTargetIdx = -1;
779
828
  if (node.label) {
780
829
  const _cLabelName = node.label.name;
@@ -786,7 +835,6 @@ export class Compiler {
786
835
  }
787
836
  if (_cTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found for continue`);
788
837
  } else {
789
- // Find the innermost loop (skip switch, block, and try contexts)
790
838
  for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
791
839
  if (this._loopStack[_ci].type === "loop") {
792
840
  _cTargetIdx = _ci;
@@ -795,7 +843,6 @@ export class Compiler {
795
843
  }
796
844
  if (_cTargetIdx === -1) throw new Error("continue outside loop");
797
845
  }
798
- // Emit TRY_END for every open try block between here and the target loop.
799
846
  for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
800
847
  if (this._loopStack[_ci].type === "try") {
801
848
  this.emit(bc, [this.OP.TRY_END], node);
@@ -816,32 +863,29 @@ export class Compiler {
816
863
  type: "switch",
817
864
  label: _swLabel,
818
865
  breakLabel: switchBreakLabel,
819
- continueLabel: switchBreakLabel // not used for switch
866
+ continueLabel: switchBreakLabel
820
867
  });
821
868
 
822
- // Compile the discriminant and leave it on the stack
823
- this._compileExpr(node.discriminant, scope, bc);
869
+ // Compile discriminant into a register that lives for the whole switch.
870
+ const discReg = this._compileExpr(node.discriminant, scope, bc);
824
871
  const cases = node.cases;
825
872
  const defaultIdx = cases.findIndex(c => c.test === null);
826
-
827
- // Pre-allocate a label for each case body so dispatch can reference them
828
873
  const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
829
874
 
830
- // Dispatch section: for each non-default case, check and jump to its body
875
+ // Dispatch: for each non-default case, test and jump.
831
876
  for (let i = 0; i < cases.length; i++) {
832
877
  const cas = cases[i];
833
- if (cas.test === null) continue; // skip default in dispatch
834
-
878
+ if (cas.test === null) continue;
835
879
  const nextCheckLabel = this._makeLabel("sw_next");
836
- this.emit(bc, [this.OP.DUP], node);
837
- this._compileExpr(cas.test, scope, bc);
838
- this.emit(bc, [this.OP.EQ], node);
839
- // If not matched, fall through to the next check
840
- this.emit(bc, [this.OP.JUMP_IF_FALSE, {
880
+ const savedTop = ctx.regTop;
881
+ const caseValReg = this._compileExpr(cas.test, scope, bc);
882
+ const cmpReg = ctx.allocReg();
883
+ this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
884
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, cmpReg, {
841
885
  type: "label",
842
886
  label: nextCheckLabel
843
887
  }], node);
844
- // If matched, jump directly to this case's body
888
+ ctx.regTop = savedTop;
845
889
  this.emit(bc, [this.OP.JUMP, {
846
890
  type: "label",
847
891
  label: caseLabels[i]
@@ -851,14 +895,10 @@ export class Compiler {
851
895
  label: nextCheckLabel
852
896
  }], node);
853
897
  }
854
-
855
- // No case matched: jump to default body or exit (which pops discriminant)
856
898
  this.emit(bc, [this.OP.JUMP, {
857
899
  type: "label",
858
900
  label: defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel
859
901
  }], node);
860
-
861
- // Body section: compile all case bodies in source order (fallthrough intact)
862
902
  for (let i = 0; i < cases.length; i++) {
863
903
  this.emit(bc, [null, {
864
904
  type: "defineLabel",
@@ -869,12 +909,11 @@ export class Compiler {
869
909
  }
870
910
  }
871
911
 
872
- // break label lands here; pop the discriminant and continue after switch
912
+ // Break lands here discriminant register is simply abandoned.
873
913
  this.emit(bc, [null, {
874
914
  type: "defineLabel",
875
915
  label: switchBreakLabel
876
916
  }], node);
877
- this.emit(bc, [this.OP.POP], node);
878
917
  this._loopStack.pop();
879
918
  break;
880
919
  }
@@ -885,18 +924,16 @@ export class Compiler {
885
924
  const _lIsLoop = _lBody.type === "ForStatement" || _lBody.type === "WhileStatement" || _lBody.type === "DoWhileStatement" || _lBody.type === "ForInStatement";
886
925
  const _lIsSwitch = _lBody.type === "SwitchStatement";
887
926
  if (_lIsLoop || _lIsSwitch) {
888
- // Pass label down to the loop/switch handler via _pendingLabel
889
927
  this._pendingLabel = _lName;
890
928
  this._compileStatement(_lBody, scope, bc);
891
- this._pendingLabel = null; // safety clear if handler didn't consume it
929
+ this._pendingLabel = null;
892
930
  } else {
893
- // Non-loop labeled statement (e.g. labeled block) -- only break is valid
894
931
  const blockBreakLabel = this._makeLabel("block_break");
895
932
  this._loopStack.push({
896
933
  type: "block",
897
934
  label: _lName,
898
935
  breakLabel: blockBreakLabel,
899
- continueLabel: blockBreakLabel // unused
936
+ continueLabel: blockBreakLabel
900
937
  });
901
938
  this._compileStatement(_lBody, scope, bc);
902
939
  this._loopStack.pop();
@@ -912,72 +949,56 @@ export class Compiler {
912
949
  const _fiLabel = this._pendingLabel;
913
950
  this._pendingLabel = null;
914
951
 
915
- // Evaluate the object expression -> on stack
916
- this._compileExpr(node.right, scope, bc);
917
- // FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
918
- this.emit(bc, [this.OP.FOR_IN_SETUP], node);
952
+ // Iterator register was reserved by _hoistVars.
953
+ const iterSlot = node._iterSlot;
919
954
 
920
- // Store iterator in a hidden slot so break/continue need no cleanup
921
- let emitLoadIter;
922
- let emitStoreIter;
923
- if (scope) {
924
- // Reserve a hidden local slot (no name mapping needed)
925
- const iterSlot = scope._next++;
926
- emitLoadIter = () => this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
927
- emitStoreIter = () => this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
928
- } else {
929
- // Top level -- use a synthetic global that won't collide with user code
930
- const iterNameIdx = b.constantOperand("__fi" + this._forInCount++);
931
- emitLoadIter = () => this.emit(bc, [this.OP.LOAD_GLOBAL, iterNameIdx], node);
932
- emitStoreIter = () => this.emit(bc, [this.OP.STORE_GLOBAL, iterNameIdx], node);
933
- }
934
- emitStoreIter();
955
+ // FOR_IN_SETUP dst, src
956
+ const objReg = this._compileExpr(node.right, scope, bc);
957
+ this.emit(bc, [this.OP.FOR_IN_SETUP, iterSlot, objReg], node);
935
958
  const loopTopLabel = this._makeLabel("forin_top");
936
959
  const exitLabel = this._makeLabel("forin_exit");
937
960
  this._loopStack.push({
938
961
  type: "loop",
939
962
  label: _fiLabel,
940
963
  breakLabel: exitLabel,
941
- continueLabel: loopTopLabel // continue re-checks the iterator
964
+ continueLabel: loopTopLabel
942
965
  });
943
966
  this.emit(bc, [null, {
944
967
  type: "defineLabel",
945
968
  label: loopTopLabel
946
969
  }], node);
947
970
 
948
- // Load iterator, attempt to get next key
949
- emitLoadIter();
950
- this.emit(bc, [this.OP.FOR_IN_NEXT, {
971
+ // FOR_IN_NEXT keyDst, iter, exitTarget
972
+ const keyReg = ctx.allocReg();
973
+ this.emit(bc, [this.OP.FOR_IN_NEXT, keyReg, iterSlot, {
951
974
  type: "label",
952
975
  label: exitLabel
953
976
  }], node);
954
977
 
955
- // Assign the key (now on top of stack) to the loop variable
978
+ // Assign the key to the loop variable.
956
979
  if (node.left.type === "VariableDeclaration") {
957
980
  const identifier = node.left.declarations[0].id;
958
981
  ok(identifier.type === "Identifier", "Only simple identifiers can be declared in for-in loops");
959
982
  const name = identifier.name;
960
983
  if (scope) {
961
- const slot = scope.define(name);
962
- this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
984
+ const slot = scope._locals.get(name);
985
+ if (keyReg !== slot) this.emit(bc, [this.OP.MOVE, slot, keyReg], node);
963
986
  } else {
964
- this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name)], node);
987
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), keyReg], node);
965
988
  }
966
989
  } else if (node.left.type === "Identifier") {
967
990
  const res = this._resolve(node.left.name, this._currentCtx);
968
991
  if (res.kind === "local") {
969
- this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
992
+ if (keyReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, keyReg], node);
970
993
  } else if (res.kind === "upvalue") {
971
- this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
994
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
972
995
  } else {
973
- this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)], node);
996
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name), keyReg], node);
974
997
  }
975
998
  } else {
976
999
  const src = generate(node.left).code;
977
1000
  throw new Error(`Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`);
978
1001
  }
979
-
980
- // Compile the loop body
981
1002
  const fiBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
982
1003
  for (const stmt of fiBody) {
983
1004
  this._compileStatement(stmt, scope, bc);
@@ -999,72 +1020,44 @@ export class Compiler {
999
1020
  throw new Error("try..finally is not supported. Use a helper function instead");
1000
1021
  }
1001
1022
  if (!node.handler) {
1002
- // try without catch requires finally — not supported
1003
1023
  throw new Error("try without catch is not supported (requires finally).");
1004
1024
  }
1005
1025
  const catchLabel = this._makeLabel("catch");
1006
1026
  const afterCatchLabel = this._makeLabel("after_catch");
1007
1027
 
1008
- // Emit TRY_SETUP with the catch block's label as the handler PC.
1009
- // At runtime: saves stack depth + frame stack depth, pushes handler.
1028
+ // Determine where the caught exception is written.
1029
+ const exceptionReg = node.handler.param?.type === "Identifier" ? scope?._locals.get(node.handler.param.name) ?? ctx.allocReg() // shouldn't normally reach here
1030
+ : node._exceptionSlot;
1010
1031
  this.emit(bc, [this.OP.TRY_SETUP, {
1011
1032
  type: "label",
1012
1033
  label: catchLabel
1013
- }], node);
1014
-
1015
- // Track the open try block so that break/continue/return inside the
1016
- // try body can emit the matching TRY_END before their jump.
1034
+ }, exceptionReg], node);
1017
1035
  this._loopStack.push({
1018
1036
  type: "try",
1019
1037
  label: null,
1020
1038
  breakLabel: "",
1021
- // unused
1022
- continueLabel: "" // unused
1039
+ continueLabel: ""
1023
1040
  });
1024
-
1025
- // Compile try body
1026
1041
  for (const stmt of node.block.body) {
1027
1042
  this._compileStatement(stmt, scope, bc);
1028
1043
  }
1029
-
1030
- // Done compiling the try body — pop the tracking entry.
1031
1044
  this._loopStack.pop();
1032
-
1033
- // Normal exit: disarm the exception handler.
1034
1045
  this.emit(bc, [this.OP.TRY_END], node);
1035
-
1036
- // Jump over the catch block on normal path.
1037
1046
  this.emit(bc, [this.OP.JUMP, {
1038
1047
  type: "label",
1039
1048
  label: afterCatchLabel
1040
1049
  }], node);
1041
1050
 
1042
- // Catch block: exception is on top of the stack (pushed by the VM).
1051
+ // Catch block: exceptionReg already holds the caught value.
1043
1052
  this.emit(bc, [null, {
1044
1053
  type: "defineLabel",
1045
1054
  label: catchLabel
1046
1055
  }], node);
1047
- const handler = node.handler;
1048
- if (handler.param) {
1049
- // Bind the exception value to the catch variable.
1050
- const name = handler.param.name;
1051
- if (scope) {
1052
- const slot = scope.define(name);
1053
- this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
1054
- } else {
1055
- this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name)], node);
1056
- }
1057
- } else {
1058
- // Optional catch binding (catch without a variable — ES2019+)
1059
- this.emit(bc, [this.OP.POP], node);
1060
- }
1061
1056
 
1062
- // Compile catch body
1063
- for (const stmt of handler.body.body) {
1057
+ // If no param binding, just ignore the exception (it's in the dummy slot).
1058
+ for (const stmt of node.handler.body.body) {
1064
1059
  this._compileStatement(stmt, scope, bc);
1065
1060
  }
1066
-
1067
- // Normal-path jump lands here (after the catch block).
1068
1061
  this.emit(bc, [null, {
1069
1062
  type: "defineLabel",
1070
1063
  label: afterCatchLabel
@@ -1073,139 +1066,154 @@ export class Compiler {
1073
1066
  }
1074
1067
  default:
1075
1068
  {
1076
- // Use @babel/generator to reproduce the source of unsupported nodes
1077
- // so we can emit a clear error with context.
1078
1069
  const src = generate(node).code;
1079
1070
  throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
1080
1071
  }
1081
1072
  }
1082
1073
  }
1083
1074
 
1084
- // Expressions
1075
+ // ── Expressions ───────────────────────────────────────────────────────────
1076
+ // Returns the register index that holds the result.
1077
+ // For local variables: returns their slot directly (no instruction emitted).
1078
+ // For all others: allocates a fresh temp register, emits the instruction(s),
1079
+ // and returns the allocated register.
1085
1080
  _compileExpr(node, scope, bc) {
1081
+ const ctx = this._currentCtx;
1086
1082
  switch (node.type) {
1087
1083
  case "NumericLiteral":
1088
1084
  case "StringLiteral":
1089
- {
1090
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.value)], node);
1091
- break;
1092
- }
1093
1085
  case "BooleanLiteral":
1094
1086
  {
1095
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.value)], node);
1096
- break;
1087
+ const dst = ctx.allocReg();
1088
+ this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(node.value)], node);
1089
+ return dst;
1097
1090
  }
1098
1091
  case "NullLiteral":
1099
1092
  {
1100
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(null)], node);
1101
- break;
1093
+ const dst = ctx.allocReg();
1094
+ this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(null)], node);
1095
+ return dst;
1102
1096
  }
1103
1097
  case "Identifier":
1104
1098
  {
1105
- // scope=null means we're at the top-level -> always global
1106
1099
  const res = this._resolve(node.name, this._currentCtx);
1107
- if (res.kind === "local") {
1108
- this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1109
- } else if (res.kind === "upvalue") {
1110
- this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1111
- } else {
1112
- this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.name)], node);
1100
+ if (res.kind === "local") return res.slot; // register IS the local
1101
+ if (res.kind === "upvalue") {
1102
+ const dst = ctx.allocReg();
1103
+ this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
1104
+ return dst;
1113
1105
  }
1114
- break;
1106
+ // global
1107
+ const dst = ctx.allocReg();
1108
+ this.emit(bc, [this.OP.LOAD_GLOBAL, dst, b.constantOperand(node.name)], node);
1109
+ return dst;
1115
1110
  }
1116
1111
  case "ThisExpression":
1117
1112
  {
1118
- this.emit(bc, [this.OP.LOAD_THIS], node);
1119
- break;
1113
+ const dst = ctx.allocReg();
1114
+ this.emit(bc, [this.OP.LOAD_THIS, dst], node);
1115
+ return dst;
1120
1116
  }
1121
1117
  case "NewExpression":
1122
1118
  {
1123
- // Push callee, then args -- identical layout to CALL but uses NEW opcode
1124
- this._compileExpr(node.callee, scope, bc);
1125
- for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1126
- this.emit(bc, [this.OP.NEW, node.arguments.length], node);
1127
- break;
1119
+ const calleeReg = this._compileExpr(node.callee, scope, bc);
1120
+ const argRegs = node.arguments.map(a => this._compileExpr(a, scope, bc));
1121
+ const dst = ctx.allocReg();
1122
+ this.emit(bc, [this.OP.NEW, dst, calleeReg, node.arguments.length, ...argRegs], node);
1123
+ return dst;
1128
1124
  }
1129
1125
  case "SequenceExpression":
1130
1126
  {
1131
- // (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
1132
- for (let i = 0; i < node.expressions.length - 1; i++) {
1133
- this._compileExpr(node.expressions[i], scope, bc);
1134
- this.emit(bc, [this.OP.POP], node); // discard intermediate result
1127
+ const exprs = node.expressions;
1128
+ for (let i = 0; i < exprs.length - 1; i++) {
1129
+ const savedTop = ctx.regTop;
1130
+ this._compileExpr(exprs[i], scope, bc);
1131
+ ctx.regTop = savedTop; // discard intermediate result
1135
1132
  }
1136
- // Last expression -- its value is the result of the whole sequence
1137
- this._compileExpr(node.expressions[node.expressions.length - 1], scope, bc);
1138
- break;
1133
+ return this._compileExpr(exprs[exprs.length - 1], scope, bc);
1139
1134
  }
1140
1135
  case "ConditionalExpression":
1141
1136
  {
1142
- // test ? consequent : alternate
1137
+ const n = node;
1143
1138
  const elseLabel = this._makeLabel("ternary_else");
1144
1139
  const endLabel = this._makeLabel("ternary_end");
1145
- this._compileExpr(node.test, scope, bc);
1146
- this.emit(bc, [this.OP.JUMP_IF_FALSE, {
1140
+
1141
+ // Compile test; free its temps after the jump is emitted.
1142
+ const baseTop = ctx.regTop;
1143
+ const testReg = this._compileExpr(n.test, scope, bc);
1144
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
1147
1145
  type: "label",
1148
1146
  label: elseLabel
1149
1147
  }], node);
1150
- this._compileExpr(node.consequent, scope, bc);
1148
+ ctx.regTop = baseTop; // free test temps
1149
+
1150
+ // Reserve reg_result at the base of the temp space.
1151
+ const reg_result = ctx.allocReg();
1152
+
1153
+ // Consequent branch.
1154
+ const consReg = this._compileExpr(n.consequent, scope, bc);
1155
+ if (consReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, consReg], node);
1151
1156
  this.emit(bc, [this.OP.JUMP, {
1152
1157
  type: "label",
1153
1158
  label: endLabel
1154
1159
  }], node);
1160
+
1161
+ // Alternate branch: reset to baseTop then re-reserve reg_result.
1155
1162
  this.emit(bc, [null, {
1156
1163
  type: "defineLabel",
1157
1164
  label: elseLabel
1158
1165
  }], node);
1159
- this._compileExpr(node.alternate, scope, bc);
1166
+ ctx.regTop = baseTop;
1167
+ ctx.allocReg(); // re-occupy reg_result slot
1168
+ const altReg = this._compileExpr(n.alternate, scope, bc);
1169
+ if (altReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
1160
1170
  this.emit(bc, [null, {
1161
1171
  type: "defineLabel",
1162
1172
  label: endLabel
1163
1173
  }], node);
1164
- break;
1174
+
1175
+ // Leave reg_result allocated above baseTop.
1176
+ ctx.regTop = baseTop + 1;
1177
+ return reg_result;
1165
1178
  }
1166
1179
  case "LogicalExpression":
1167
1180
  {
1168
- // Pattern (CPython-style):
1169
- // eval LHS
1170
- // JUMP_IF_*_OR_POP -> target (past RHS)
1171
- // eval RHS ← only reached if LHS didn't short-circuit
1172
- // [target lands here, stack top is the result either way]
1173
-
1174
- this._compileExpr(node.left, scope, bc);
1175
- if (node.operator === "||") {
1176
- // Short-circuit if LHS is TRUTHY -- keep it, skip RHS
1177
- const endLabel = this._makeLabel("or_end");
1178
- this.emit(bc, [this.OP.JUMP_IF_TRUE_OR_POP, {
1179
- type: "label",
1180
- label: endLabel
1181
- }], node);
1182
- this._compileExpr(node.right, scope, bc);
1183
- this.emit(bc, [null, {
1184
- type: "defineLabel",
1185
- label: endLabel
1186
- }], node);
1187
- } else if (node.operator === "&&") {
1188
- // Short-circuit if LHS is FALSY -- keep it, skip RHS
1189
- const endLabel = this._makeLabel("and_end");
1190
- this.emit(bc, [this.OP.JUMP_IF_FALSE_OR_POP, {
1191
- type: "label",
1192
- label: endLabel
1193
- }], node);
1194
- this._compileExpr(node.right, scope, bc);
1195
- this.emit(bc, [null, {
1196
- type: "defineLabel",
1197
- label: endLabel
1198
- }], node);
1199
- } else {
1200
- throw new Error(`Unsupported logical operator: ${node.operator}`);
1201
- }
1202
- break;
1181
+ const n = node;
1182
+ const endLabel = this._makeLabel("logical_end");
1183
+ const isOr = n.operator === "||";
1184
+ if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
1185
+ const baseTop = ctx.regTop;
1186
+ const lhsReg = this._compileExpr(n.left, scope, bc);
1187
+ ctx.regTop = baseTop;
1188
+ const reg_result = ctx.allocReg();
1189
+ if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
1190
+
1191
+ // For ||: if truthy keep LHS, jump past RHS.
1192
+ // For &&: if falsy keep LHS, jump past RHS.
1193
+ this.emit(bc, [isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE, reg_result, {
1194
+ type: "label",
1195
+ label: endLabel
1196
+ }], node);
1197
+
1198
+ // Compile RHS into reg_result.
1199
+ ctx.regTop = baseTop;
1200
+ ctx.allocReg(); // re-occupy reg_result
1201
+ const rhsReg = this._compileExpr(n.right, scope, bc);
1202
+ if (rhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
1203
+ this.emit(bc, [null, {
1204
+ type: "defineLabel",
1205
+ label: endLabel
1206
+ }], node);
1207
+ ctx.regTop = baseTop + 1;
1208
+ return reg_result;
1203
1209
  }
1204
1210
  case "BinaryExpression":
1205
1211
  {
1206
- this._compileExpr(node.left, scope, bc);
1207
- this._compileExpr(node.right, scope, bc);
1208
- const arithOp = {
1212
+ const n = node;
1213
+ const lhsReg = this._compileExpr(n.left, scope, bc);
1214
+ const rhsReg = this._compileExpr(n.right, scope, bc);
1215
+ const dst = ctx.allocReg();
1216
+ const op = {
1209
1217
  "+": this.OP.ADD,
1210
1218
  "-": this.OP.SUB,
1211
1219
  "*": this.OP.MUL,
@@ -1216,9 +1224,7 @@ export class Compiler {
1216
1224
  "^": this.OP.BXOR,
1217
1225
  "<<": this.OP.SHL,
1218
1226
  ">>": this.OP.SHR,
1219
- ">>>": this.OP.USHR
1220
- }[node.operator];
1221
- const cmpOp = {
1227
+ ">>>": this.OP.USHR,
1222
1228
  "<": this.OP.LT,
1223
1229
  ">": this.OP.GT,
1224
1230
  "===": this.OP.EQ,
@@ -1228,38 +1234,65 @@ export class Compiler {
1228
1234
  "!==": this.OP.NEQ,
1229
1235
  "!=": this.OP.LOOSE_NEQ,
1230
1236
  in: this.OP.IN,
1231
- // ← add
1232
- instanceof: this.OP.INSTANCEOF // ← add
1233
- }[node.operator];
1234
- const resolvedOp = arithOp ?? cmpOp;
1235
- if (resolvedOp === undefined) throw new Error(`Unsupported operator: ${node.operator}`);
1236
- this.emit(bc, [resolvedOp], node);
1237
- break;
1237
+ instanceof: this.OP.INSTANCEOF
1238
+ }[n.operator];
1239
+ if (op === undefined) throw new Error(`Unsupported operator: ${n.operator}`);
1240
+ this.emit(bc, [op, dst, lhsReg, rhsReg], node);
1241
+ return dst;
1238
1242
  }
1239
1243
  case "UpdateExpression":
1240
1244
  {
1241
- const res = this._resolve(node.argument.name, this._currentCtx);
1242
- const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
1243
- const one = b.constantOperand(1);
1244
-
1245
- // Helper closures: emit load / store for whichever resolution kind we have
1246
- const emitLoad = () => {
1247
- if (res.kind === "local") this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);else if (res.kind === "upvalue") this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);else this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.argument.name)], node);
1248
- };
1249
- const emitStore = () => {
1250
- if (res.kind === "local") this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);else if (res.kind === "upvalue") this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);else this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.argument.name)], node);
1251
- };
1252
- emitLoad();
1253
- if (!node.prefix) this.emit(bc, [this.OP.DUP], node); // post: save old value before mutating
1254
- this.emit(bc, [this.OP.LOAD_CONST, one], node);
1255
- this.emit(bc, [bumpOp], node);
1256
- emitStore();
1257
- if (node.prefix) emitLoad(); // pre: reload new value as result
1245
+ const n = node;
1246
+ ok(n.argument.type === "Identifier", "UpdateExpression requires identifier");
1247
+ const name = n.argument.name;
1248
+ const res = this._resolve(name, this._currentCtx);
1249
+ const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
1250
+
1251
+ // Load current value into a register (locals are already in place)
1252
+ let curReg;
1253
+ if (res.kind === "local") {
1254
+ curReg = res.slot;
1255
+ } else if (res.kind === "upvalue") {
1256
+ curReg = ctx.allocReg();
1257
+ this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
1258
+ } else {
1259
+ curReg = ctx.allocReg();
1260
+ this.emit(bc, [this.OP.LOAD_GLOBAL, curReg, b.constantOperand(name)], node);
1261
+ }
1258
1262
 
1259
- break;
1263
+ // For postfix we need to preserve the *old* value as the result
1264
+ let resultReg;
1265
+ if (!n.prefix) {
1266
+ resultReg = ctx.allocReg();
1267
+ this.emit(bc, [this.OP.MOVE, resultReg, curReg], node);
1268
+ } else {
1269
+ resultReg = -1; // placeholder – will become newReg below
1270
+ }
1271
+ const oneReg = ctx.allocReg();
1272
+ this.emit(bc, [this.OP.LOAD_CONST, oneReg, b.constantOperand(1)], node);
1273
+
1274
+ // Compute new value (always into a fresh register)
1275
+ const newReg = ctx.allocReg();
1276
+ this.emit(bc, [bumpOp, newReg, curReg, oneReg], node);
1277
+
1278
+ // Write the new value back (local = MOVE, others = STORE_xxx)
1279
+ if (res.kind === "local") {
1280
+ this.emit(bc, [this.OP.MOVE, res.slot, newReg], node);
1281
+ } else if (res.kind === "upvalue") {
1282
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index, newReg], node);
1283
+ } else {
1284
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), newReg], node);
1285
+ }
1286
+
1287
+ // Prefix returns the *new* value (we already have it in newReg – no reload needed)
1288
+ if (n.prefix) {
1289
+ resultReg = newReg;
1290
+ }
1291
+ return resultReg;
1260
1292
  }
1261
1293
  case "AssignmentExpression":
1262
1294
  {
1295
+ const n = node;
1263
1296
  const compoundOp = {
1264
1297
  "+=": this.OP.ADD,
1265
1298
  "-=": this.OP.SUB,
@@ -1272,268 +1305,242 @@ export class Compiler {
1272
1305
  "<<=": this.OP.SHL,
1273
1306
  ">>=": this.OP.SHR,
1274
1307
  ">>>=": this.OP.USHR
1275
- }[node.operator];
1308
+ }[n.operator];
1276
1309
  const isCompound = compoundOp !== undefined;
1277
- if (node.operator !== "=" && !isCompound) {
1278
- throw new Error(`Unsupported assignment operator: ${node.operator}`);
1279
- }
1310
+ if (n.operator !== "=" && !isCompound) throw new Error(`Unsupported assignment operator: ${n.operator}`);
1280
1311
 
1281
1312
  // Member assignment: obj.x = val or arr[i] = val
1282
- if (node.left.type === "MemberExpression") {
1283
- this._compileExpr(node.left.object, scope, bc); // push obj
1284
-
1285
- if (node.left.computed) {
1286
- this._compileExpr(node.left.property, scope, bc); // push key (runtime)
1313
+ if (n.left.type === "MemberExpression") {
1314
+ const objReg = this._compileExpr(n.left.object, scope, bc);
1315
+ let keyReg;
1316
+ if (n.left.computed) {
1317
+ keyReg = this._compileExpr(n.left.property, scope, bc);
1287
1318
  } else {
1288
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)], node);
1319
+ keyReg = ctx.allocReg();
1320
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(n.left.property.name)], node);
1289
1321
  }
1322
+ let valReg;
1290
1323
  if (isCompound) {
1291
- // Duplicate obj+key on the stack so we can read before we write.
1292
- // Stack before DUP2: [..., obj, key]
1293
- // We need: [..., obj, key, obj, key] -> GET_PROP_COMPUTED -> [..., obj, key, currentVal]
1294
- // Cheapest approach without a DUP opcode: re-compile the member read.
1295
- // (emits obj + key again; a future peephole pass could DUP instead)
1296
- this._compileExpr(node.left.object, scope, bc);
1297
- if (node.left.computed) {
1298
- this._compileExpr(node.left.property, scope, bc);
1299
- } else {
1300
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)], node);
1301
- }
1302
- this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
1303
- this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
1304
- this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
1324
+ const curReg = ctx.allocReg();
1325
+ this.emit(bc, [this.OP.GET_PROP, curReg, objReg, keyReg], node);
1326
+ const rhsReg = this._compileExpr(n.right, scope, bc);
1327
+ valReg = ctx.allocReg();
1328
+ this.emit(bc, [compoundOp, valReg, curReg, rhsReg], node);
1305
1329
  } else {
1306
- this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
1330
+ valReg = this._compileExpr(n.right, scope, bc);
1307
1331
  }
1308
- this.emit(bc, [this.OP.SET_PROP], node); // obj[key] = val, leaves val on stack
1309
- break;
1332
+ this.emit(bc, [this.OP.SET_PROP, objReg, keyReg, valReg], node);
1333
+ return valReg;
1310
1334
  }
1311
1335
 
1312
- // Plain identifier assignment
1313
- const res = this._resolve(node.left.name, this._currentCtx);
1336
+ // Plain identifier assignment.
1337
+ const res = this._resolve(n.left.name, this._currentCtx);
1338
+ let rhsReg;
1314
1339
  if (isCompound) {
1315
- // Load the current value of the target first
1340
+ // Load current value of the variable.
1341
+ let curReg;
1316
1342
  if (res.kind === "local") {
1317
- this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1343
+ curReg = res.slot;
1318
1344
  } else if (res.kind === "upvalue") {
1319
- this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1345
+ curReg = ctx.allocReg();
1346
+ this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
1320
1347
  } else {
1321
- this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.left.name)], node);
1348
+ curReg = ctx.allocReg();
1349
+ this.emit(bc, [this.OP.LOAD_GLOBAL, curReg, b.constantOperand(n.left.name)], node);
1322
1350
  }
1323
- }
1324
- this._compileExpr(node.right, scope, bc); // push RHS
1325
-
1326
- if (isCompound) {
1327
- this.emit(bc, [compoundOp], node); // apply binary op -> leaves newVal on stack
1351
+ const rhs2 = this._compileExpr(n.right, scope, bc);
1352
+ rhsReg = ctx.allocReg();
1353
+ this.emit(bc, [compoundOp, rhsReg, curReg, rhs2], node);
1354
+ } else {
1355
+ rhsReg = this._compileExpr(n.right, scope, bc);
1328
1356
  }
1329
1357
 
1330
- // Store & leave value on stack (assignment is an expression)
1358
+ // Store result and return it.
1331
1359
  if (res.kind === "local") {
1332
- this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
1333
- this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1360
+ if (rhsReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, rhsReg], node);
1361
+ return res.slot;
1334
1362
  } else if (res.kind === "upvalue") {
1335
- this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
1336
- this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1363
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
1364
+ return rhsReg;
1337
1365
  } else {
1338
- const nameIdx = b.constantOperand(node.left.name);
1339
- this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx], node);
1340
- this.emit(bc, [this.OP.LOAD_GLOBAL, nameIdx], node);
1366
+ const nameIdx = b.constantOperand(n.left.name);
1367
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx, rhsReg], node);
1368
+ return rhsReg;
1341
1369
  }
1342
- break;
1343
1370
  }
1344
1371
  case "CallExpression":
1345
1372
  {
1346
- if (node.callee.type === "MemberExpression") {
1347
- // ── Method call: console.log(...)
1348
- // Push receiver first (GET_PROP leaves it; CALL_METHOD pops it as `this`)
1349
- this._compileExpr(node.callee.object, scope, bc);
1350
- const prop = node.callee.property.name;
1351
- const propIdx = b.constantOperand(prop);
1352
- this.emit(bc, [this.OP.LOAD_CONST, propIdx], node);
1353
- this.emit(bc, [this.OP.GET_PROP], node);
1354
- for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1355
- this.emit(bc, [this.OP.CALL_METHOD, node.arguments.length], node);
1373
+ const n = node;
1374
+ if (n.callee.type === "MemberExpression") {
1375
+ // Method call: receiver.method(args)
1376
+ const receiverReg = this._compileExpr(n.callee.object, scope, bc);
1377
+ let methodKeyReg;
1378
+ if (n.callee.computed) {
1379
+ methodKeyReg = this._compileExpr(n.callee.property, scope, bc);
1380
+ } else {
1381
+ methodKeyReg = ctx.allocReg();
1382
+ this.emit(bc, [this.OP.LOAD_CONST, methodKeyReg, b.constantOperand(n.callee.property.name)], node);
1383
+ }
1384
+ const calleeReg = ctx.allocReg();
1385
+ this.emit(bc, [this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg], node);
1386
+ const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1387
+ const dst = ctx.allocReg();
1388
+ this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, n.arguments.length, ...argRegs], node);
1389
+ return dst;
1356
1390
  } else {
1357
- // ── Plain call: add(5, 10)
1358
- this._compileExpr(node.callee, scope, bc);
1359
- for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1360
- this.emit(bc, [this.OP.CALL, node.arguments.length], node);
1391
+ // Plain call: fn(args)
1392
+ const calleeReg = this._compileExpr(n.callee, scope, bc);
1393
+ const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1394
+ const dst = ctx.allocReg();
1395
+ this.emit(bc, [this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs], node);
1396
+ return dst;
1361
1397
  }
1362
- break;
1363
1398
  }
1364
1399
  case "UnaryExpression":
1365
1400
  {
1366
- // Special case: typeof on a bare identifier must not throw if undeclared.
1367
- // We emit TYPEOF_SAFE (operand = name constant index) instead of
1368
- // compiling the argument first. The VM does the guard itself.
1369
- if (node.operator === "typeof" && node.argument.type === "Identifier") {
1370
- const res = this._resolve(node.argument.name, this._currentCtx);
1401
+ const n = node;
1402
+
1403
+ // typeof on a potentially-undeclared global -- safe guard.
1404
+ if (n.operator === "typeof" && n.argument.type === "Identifier") {
1405
+ const res = this._resolve(n.argument.name, this._currentCtx);
1371
1406
  if (res.kind === "global") {
1372
- // Potentially undeclared -- let VM guard it
1373
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.argument.name)], node);
1374
- this.emit(bc, [this.OP.TYPEOF_SAFE], node);
1375
- break;
1407
+ const dst = ctx.allocReg();
1408
+ this.emit(bc, [this.OP.TYPEOF_SAFE, dst, b.constantOperand(n.argument.name)], node);
1409
+ return dst;
1376
1410
  }
1377
- // Known local or upvalue -- safe to load first, then typeof
1378
1411
  }
1379
1412
 
1380
- // Special case: delete -- argument must NOT be pre-evaluated.
1381
- if (node.operator === "delete") {
1382
- const arg = node.argument;
1413
+ // delete expression.
1414
+ if (n.operator === "delete") {
1415
+ const arg = n.argument;
1383
1416
  if (arg.type === "MemberExpression") {
1384
- this._compileExpr(arg.object, scope, bc);
1417
+ const objReg = this._compileExpr(arg.object, scope, bc);
1418
+ let keyReg;
1385
1419
  if (arg.computed) {
1386
- this._compileExpr(arg.property, scope, bc);
1420
+ keyReg = this._compileExpr(arg.property, scope, bc);
1387
1421
  } else {
1388
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(arg.property.name)], node);
1422
+ keyReg = ctx.allocReg();
1423
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(arg.property.name)], node);
1389
1424
  }
1390
- this.emit(bc, [this.OP.DELETE_PROP], node);
1425
+ const dst = ctx.allocReg();
1426
+ this.emit(bc, [this.OP.DELETE_PROP, dst, objReg, keyReg], node);
1427
+ return dst;
1391
1428
  } else {
1392
- // delete x, delete 0, etc. -- always true in non-strict, just push true
1393
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(true)], node);
1429
+ // delete x or delete 0 -- always true in sloppy mode.
1430
+ const dst = ctx.allocReg();
1431
+ this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(true)], node);
1432
+ return dst;
1394
1433
  }
1395
- break;
1396
1434
  }
1397
1435
 
1398
- // All other unary ops: compile argument first, then apply operator
1399
- this._compileExpr(node.argument, scope, bc);
1400
- switch (node.operator) {
1401
- case "-":
1402
- this.emit(bc, [this.OP.UNARY_NEG], node);
1403
- break;
1404
- case "+":
1405
- this.emit(bc, [this.OP.UNARY_POS], node);
1406
- break;
1407
- case "!":
1408
- this.emit(bc, [this.OP.UNARY_NOT], node);
1409
- break;
1410
- case "~":
1411
- this.emit(bc, [this.OP.UNARY_BITNOT], node);
1412
- break;
1413
- case "typeof":
1414
- this.emit(bc, [this.OP.TYPEOF], node);
1415
- break;
1416
- case "void":
1417
- this.emit(bc, [this.OP.VOID], node);
1418
- break;
1419
- default:
1420
- throw new Error(`Unsupported unary operator: ${node.operator}`);
1421
- }
1422
- break;
1436
+ // All other unary operators.
1437
+ const srcReg = this._compileExpr(n.argument, scope, bc);
1438
+ const dst = ctx.allocReg();
1439
+ const unaryOp = {
1440
+ "-": this.OP.UNARY_NEG,
1441
+ "+": this.OP.UNARY_POS,
1442
+ "!": this.OP.UNARY_NOT,
1443
+ "~": this.OP.UNARY_BITNOT,
1444
+ typeof: this.OP.TYPEOF,
1445
+ void: this.OP.VOID
1446
+ }[n.operator];
1447
+ if (unaryOp === undefined) throw new Error(`Unsupported unary operator: ${n.operator}`);
1448
+ this.emit(bc, [unaryOp, dst, srcReg], node);
1449
+ return dst;
1423
1450
  }
1424
1451
  case "RegExpLiteral":
1425
1452
  {
1426
- // Emit: new RegExp(pattern, flags)
1427
- // Fresh object per evaluation -- correct for stateful g/y flags.
1428
- this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand("RegExp")], node);
1429
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.pattern)], node);
1430
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.flags)], node);
1431
- this.emit(bc, [this.OP.NEW, 2], node);
1432
- break;
1453
+ const n = node;
1454
+ // new RegExp(pattern, flags)
1455
+ const regExpReg = ctx.allocReg();
1456
+ this.emit(bc, [this.OP.LOAD_GLOBAL, regExpReg, b.constantOperand("RegExp")], node);
1457
+ const patternReg = ctx.allocReg();
1458
+ this.emit(bc, [this.OP.LOAD_CONST, patternReg, b.constantOperand(n.pattern)], node);
1459
+ const flagsReg = ctx.allocReg();
1460
+ this.emit(bc, [this.OP.LOAD_CONST, flagsReg, b.constantOperand(n.flags)], node);
1461
+ const dst = ctx.allocReg();
1462
+ this.emit(bc, [this.OP.NEW, dst, regExpReg, 2, patternReg, flagsReg], node);
1463
+ return dst;
1433
1464
  }
1434
1465
  case "FunctionExpression":
1435
1466
  {
1436
- // Compile into a descriptor exactly like a declaration,
1437
- // but leave the resulting closure ON THE STACK -- no store.
1438
- // The surrounding expression (assignment, call arg, return) consumes it.
1439
1467
  const desc = this._compileFunctionDecl(node);
1440
- this._emitMakeClosure(desc, node, bc);
1441
- break;
1468
+ return this._emitMakeClosure(desc, node, bc);
1442
1469
  }
1443
1470
  case "MemberExpression":
1444
1471
  {
1445
- this._compileExpr(node.object, scope, bc);
1446
- if (node.computed) {
1447
- // nums[i] -- key is runtime value
1448
- this._compileExpr(node.property, scope, bc);
1472
+ const n = node;
1473
+ const objReg = this._compileExpr(n.object, scope, bc);
1474
+ let keyReg;
1475
+ if (n.computed) {
1476
+ keyReg = this._compileExpr(n.property, scope, bc);
1449
1477
  } else {
1450
- // point.x -- push key as string, same opcode handles both
1451
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.property.name)], node);
1478
+ keyReg = ctx.allocReg();
1479
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(n.property.name)], node);
1452
1480
  }
1453
-
1454
- // GET_PROP_COMPUTED pops the object -- correct for value access.
1455
- // GET_PROP (peek) is only used in CallExpression's method call path
1456
- // where the receiver must survive on the stack for CALL_METHOD.
1457
- this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
1458
- break;
1481
+ const dst = ctx.allocReg();
1482
+ this.emit(bc, [this.OP.GET_PROP, dst, objReg, keyReg], node);
1483
+ return dst;
1459
1484
  }
1460
1485
  case "ArrayExpression":
1461
1486
  {
1462
- // Compile each element left->right, then BUILD_ARRAY collapses them.
1463
- // Sparse arrays (holes) get explicit undefined per slot.
1464
- for (const el of node.elements) {
1487
+ const n = node;
1488
+ const elemRegs = n.elements.map(el => {
1465
1489
  if (el === null) {
1466
- // hole: e.g. [1,,3]
1467
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
1468
- } else {
1469
- this._compileExpr(el, scope, bc);
1490
+ const r = ctx.allocReg();
1491
+ this.emit(bc, [this.OP.LOAD_CONST, r, b.constantOperand(undefined)], node);
1492
+ return r;
1470
1493
  }
1471
- }
1472
- this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
1473
- break;
1494
+ return this._compileExpr(el, scope, bc);
1495
+ });
1496
+ const dst = ctx.allocReg();
1497
+ this.emit(bc, [this.OP.BUILD_ARRAY, dst, n.elements.length, ...elemRegs], node);
1498
+ return dst;
1474
1499
  }
1475
1500
  case "ObjectExpression":
1476
1501
  {
1477
- // Separate regular data properties from ES5 accessor methods (get/set).
1502
+ const n = node;
1478
1503
  const regularProps = [];
1479
1504
  const accessorProps = [];
1480
- for (const prop of node.properties) {
1481
- if (prop.type === "SpreadElement") {
1482
- throw new Error("Object spread not supported");
1483
- }
1505
+ for (const prop of n.properties) {
1506
+ if (prop.type === "SpreadElement") throw new Error("Object spread not supported");
1484
1507
  if (prop.type === "ObjectMethod") {
1485
1508
  if (prop.kind === "get" || prop.kind === "set") {
1486
- if (prop.computed) {
1487
- throw new Error("Computed getter/setter keys are not supported");
1488
- }
1509
+ if (prop.computed) throw new Error("Computed getter/setter keys are not supported");
1489
1510
  accessorProps.push(prop);
1490
1511
  } else {
1491
- throw new Error(`Shorthand method syntax is not supported`);
1512
+ throw new Error("Shorthand method syntax is not supported");
1492
1513
  }
1493
1514
  } else {
1494
1515
  regularProps.push(prop);
1495
1516
  }
1496
1517
  }
1497
1518
 
1498
- // Build the base object from data properties.
1519
+ // Build flat [key, val, key, val, …] register list.
1520
+ const pairRegs = [];
1499
1521
  for (const prop of regularProps) {
1500
- const key = prop.key;
1501
1522
  let keyStr;
1502
- if (key.type === "Identifier") {
1503
- keyStr = key.name;
1504
- } else if (key.type === "StringLiteral" || key.type === "NumericLiteral") {
1505
- keyStr = String(key.value);
1506
- } else {
1507
- throw new Error(`Unsupported object key type: ${key.type}`);
1508
- }
1509
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1510
- this._compileExpr(prop.value, scope, bc);
1523
+ const key = prop.key;
1524
+ if (key.type === "Identifier") keyStr = key.name;else if (key.type === "StringLiteral" || key.type === "NumericLiteral") keyStr = String(key.value);else throw new Error(`Unsupported object key type: ${key.type}`);
1525
+ const keyReg = ctx.allocReg();
1526
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1527
+ const valReg = this._compileExpr(prop.value, scope, bc);
1528
+ pairRegs.push(keyReg, valReg);
1511
1529
  }
1512
- this.emit(bc, [this.OP.BUILD_OBJECT, regularProps.length], node);
1530
+ const dst = ctx.allocReg();
1531
+ this.emit(bc, [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs], node);
1513
1532
 
1514
- // Define each accessor on the object that is now on top of the stack.
1515
- // Stack after BUILD_OBJECT: [..., obj]
1516
- // For each accessor: DUP obj, push key, compile fn, DEFINE_GETTER/DEFINE_SETTER
1517
- // DEFINE_GETTER/DEFINE_SETTER pops fn+key+obj, leaving the original obj.
1533
+ // Define accessors on the object now sitting in `dst`.
1518
1534
  for (const prop of accessorProps) {
1519
1535
  const key = prop.key;
1520
1536
  let keyStr;
1521
- if (key.type === "Identifier") {
1522
- keyStr = key.name;
1523
- } else if (key.type === "StringLiteral" || key.type === "NumericLiteral") {
1524
- keyStr = String(key.value);
1525
- } else {
1526
- throw new Error(`Unsupported object key type: ${key.type}`);
1527
- }
1528
- this.emit(bc, [this.OP.DUP], node); // dup so the original obj stays after the define
1529
- this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1530
-
1531
- // Compile the accessor body as an anonymous function descriptor.
1532
- const desc = this._compileFunctionDecl(prop);
1533
- this._emitMakeClosure(desc, prop, bc);
1534
- this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER], node);
1537
+ if (key.type === "Identifier") keyStr = key.name;else if (key.type === "StringLiteral" || key.type === "NumericLiteral") keyStr = String(key.value);else throw new Error(`Unsupported object key type: ${key.type}`);
1538
+ const keyReg = ctx.allocReg();
1539
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1540
+ const fnReg = this._emitMakeClosure(this._compileFunctionDecl(prop), prop, bc);
1541
+ this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
1535
1542
  }
1536
- break;
1543
+ return dst;
1537
1544
  }
1538
1545
  default:
1539
1546
  {
@@ -1543,10 +1550,7 @@ export class Compiler {
1543
1550
  }
1544
1551
  }
1545
1552
 
1546
- // Serializer
1547
- // Turns the compiled output into a commented JS source string.
1548
- // Expects fully-resolved bytecode (all label refs and constant refs already
1549
- // converted to plain integers by resolveLabels + resolveConstants passes).
1553
+ // ── Serializer ────────────────────────────────────────────────────────────────
1550
1554
  class Serializer {
1551
1555
  constructor(compiler) {
1552
1556
  this.compiler = compiler;
@@ -1563,18 +1567,27 @@ class Serializer {
1563
1567
  get JUMP_OPS() {
1564
1568
  return this.compiler.JUMP_OPS;
1565
1569
  }
1566
-
1567
- // Produce a JS literal for a constant pool entry
1568
1570
  _serializeConst(val) {
1569
1571
  if (val === null) return "null";
1570
1572
  if (val === undefined) return "undefined";
1571
- return JSON.stringify(val); // number / string / bool
1573
+ return JSON.stringify(val);
1572
1574
  }
1573
1575
 
1574
- // One instruction -> "[op, op1, op2, ...] // MNEMONIC description"
1575
- // Expects a fully-resolved instruction: all operands are plain numbers.
1576
- // Returns { text, values } where values is the flat u16 slots for this
1577
- // instruction (opcode first, then one entry per operand).
1576
+ // Reverse the concealment applied by resolveConstants so disassembly comments
1577
+ // always show the plaintext value regardless of the concealConstants option.
1578
+ _decryptConst(constants, idx, key) {
1579
+ const v = constants[idx];
1580
+ if (!key) return v;
1581
+ if (typeof v === "number") return v ^ key;
1582
+ // String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
1583
+ const bytes = Buffer.from(v, "base64");
1584
+ let out = "";
1585
+ for (let i = 0; i < bytes.length / 2; i++) {
1586
+ const code = bytes[i * 2] | bytes[i * 2 + 1] << 8;
1587
+ out += String.fromCharCode(code ^ key + i & 0xffff);
1588
+ }
1589
+ return out;
1590
+ }
1578
1591
  _serializeInstr(instr, constants) {
1579
1592
  const op = instr[0];
1580
1593
  const operands = instr.slice(1);
@@ -1584,66 +1597,75 @@ class Serializer {
1584
1597
  ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
1585
1598
  }
1586
1599
  ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
1587
- const operand = resolvedOperands[0]; // first operand for single-operand comment cases
1588
1600
  const name = this.OP_NAME[op] || `OP_${op}`;
1589
1601
  let comment = name;
1602
+ function formatLoc(loc) {
1603
+ return loc ? `${loc.line}:${loc.column}` : "";
1604
+ }
1590
1605
  const sourceNode = instr[SOURCE_NODE_SYM];
1591
- const sourceLocation = sourceNode ? sourceNode.loc.start?.line + ":" + sourceNode.loc.start?.column + "-" + (sourceNode.loc.end?.line + ":" + sourceNode.loc.end?.column) : "";
1592
-
1593
- // Annotate with human-readable operand meaning
1606
+ const sourceLocation = sourceNode?.loc ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)].filter(Boolean).join("-") : "";
1594
1607
  if (resolvedOperands.length > 0) {
1608
+ // Operand[0] is always `dst` for instruction types that produce a value.
1609
+ const dst = resolvedOperands[0];
1595
1610
  switch (op) {
1596
1611
  case this.OP.LOAD_CONST:
1597
1612
  {
1598
- const val = constants[operand];
1599
- comment += ` ${this._serializeConst(val)}`;
1613
+ // resolvedOperands: [dst, constIdx, concealKey]
1614
+ const val = this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2]);
1615
+ comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
1600
1616
  break;
1601
1617
  }
1602
- case this.OP.MAKE_CLOSURE:
1603
- {
1604
- comment += ` PC ${operand} (params=${resolvedOperands[1]} locals=${resolvedOperands[2]} upvalues=${resolvedOperands[3]})`;
1605
- break;
1606
- }
1607
- case this.OP.LOAD_LOCAL:
1608
- case this.OP.STORE_LOCAL:
1609
- comment += ` slot[${operand}]`;
1618
+ case this.OP.LOAD_GLOBAL:
1619
+ // resolvedOperands: [dst, constIdx, concealKey]
1620
+ comment += ` reg[${dst}] = ${this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2])}`;
1621
+ break;
1622
+ case this.OP.STORE_GLOBAL:
1623
+ // resolvedOperands: [constIdx, concealKey, srcReg]
1624
+ comment += ` ${this._decryptConst(constants, resolvedOperands[0], resolvedOperands[1])} = reg[${resolvedOperands[2]}]`;
1610
1625
  break;
1611
1626
  case this.OP.LOAD_UPVALUE:
1627
+ comment += ` reg[${dst}] = upvalue[${resolvedOperands[1]}]`;
1628
+ break;
1612
1629
  case this.OP.STORE_UPVALUE:
1613
- comment += ` upvalue[${operand}]`;
1630
+ comment += ` upvalue[${resolvedOperands[0]}] = reg[${resolvedOperands[1]}]`;
1614
1631
  break;
1615
- case this.OP.LOAD_GLOBAL:
1616
- case this.OP.STORE_GLOBAL:
1617
- comment += ` "${constants[operand]}"`;
1632
+ case this.OP.MOVE:
1633
+ comment += ` reg[${dst}] = reg[${resolvedOperands[1]}]`;
1634
+ break;
1635
+ case this.OP.MAKE_CLOSURE:
1636
+ comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
1618
1637
  break;
1619
1638
  case this.OP.CALL:
1639
+ comment += ` reg[${dst}] = call(reg[${resolvedOperands[1]}], ${resolvedOperands[2]} args)`;
1640
+ break;
1620
1641
  case this.OP.CALL_METHOD:
1621
- comment += ` (${operand} args)`;
1642
+ comment += ` reg[${dst}] = method(recv=reg[${resolvedOperands[1]}], fn=reg[${resolvedOperands[2]}], ${resolvedOperands[3]} args)`;
1643
+ break;
1644
+ case this.OP.NEW:
1645
+ comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
1646
+ break;
1647
+ case this.OP.RETURN:
1648
+ comment += ` reg[${resolvedOperands[0]}]`;
1622
1649
  break;
1623
1650
  case this.OP.BUILD_ARRAY:
1624
- comment += ` (${operand} elements)`;
1651
+ comment += ` reg[${dst}] = [${resolvedOperands[2]} elems]`;
1625
1652
  break;
1626
1653
  case this.OP.BUILD_OBJECT:
1627
- comment += ` (${operand} pairs)`;
1628
- break;
1629
- case this.OP.NEW:
1630
- comment += ` (${operand} args)`;
1654
+ comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
1631
1655
  break;
1632
1656
  default:
1633
- comment += resolvedOperands.length === 1 ? ` ${operand}` : ` [${resolvedOperands.join(", ")}]`;
1657
+ comment += resolvedOperands.length === 1 ? ` ${resolvedOperands[0]}` : ` [${resolvedOperands.join(", ")}]`;
1634
1658
  }
1635
1659
  }
1636
- comment = comment.padEnd(40) + sourceLocation;
1660
+ comment = comment.padEnd(50) + sourceLocation;
1637
1661
  const values = [op, ...resolvedOperands];
1638
1662
  const instrText = `[${values.join(", ")}]`;
1639
- const text = `${(instrText + ",").padEnd(12)} ${comment}`;
1663
+ const text = `${(instrText + ",").padEnd(20)} ${comment}`;
1640
1664
  return {
1641
1665
  text,
1642
1666
  values
1643
1667
  };
1644
1668
  }
1645
-
1646
- // Serialize the CONSTANTS array
1647
1669
  _serializeConstants(constants) {
1648
1670
  const lines = ["var CONSTANTS = ["];
1649
1671
  constants.forEach((val, idx) => {
@@ -1652,19 +1674,17 @@ class Serializer {
1652
1674
  lines.push("];");
1653
1675
  return lines.join("\n");
1654
1676
  }
1655
-
1656
- // Filter out any remaining null-opcode pseudo-instructions.
1657
- // (defineLabel pseudo-ops are already stripped by resolveLabels.)
1658
1677
  _serializeBytecode(bytecode, compiler) {
1659
1678
  const serialized = [];
1660
1679
  for (const instr of bytecode) {
1661
1680
  if (instr[0] === null) continue;
1662
1681
  const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
1663
1682
  if (specializedOpInfo) {
1664
- const resolvedValue = instr[1]?.resolvedValue ?? instr[1];
1683
+ const operands = instr.slice(1);
1684
+ const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
1665
1685
  const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
1666
- compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValue}`;
1667
- specializedOpInfo.resolvedOperand = instr[1];
1686
+ compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValues.join("_")}`;
1687
+ specializedOpInfo.resolvedOperands = operands;
1668
1688
  }
1669
1689
  serialized.push(instr);
1670
1690
  }
@@ -1673,7 +1693,6 @@ class Serializer {
1673
1693
  };
1674
1694
  }
1675
1695
  _encodeBytecode(flat) {
1676
- // Encode as little-endian Uint16Array -> base64.
1677
1696
  const buf = new Uint8Array(flat.length * 2);
1678
1697
  flat.forEach((w, i) => {
1679
1698
  buf[i * 2] = w & 0xff;
@@ -1683,6 +1702,7 @@ class Serializer {
1683
1702
  }
1684
1703
  serialize(bytecode, constants, compiler) {
1685
1704
  const mainStartPc = compiler.mainStartPc;
1705
+ const mainRegCount = compiler.mainRegCount;
1686
1706
  let sections = [];
1687
1707
  var textForm = [];
1688
1708
  var initBody = [];
@@ -1700,24 +1720,16 @@ class Serializer {
1700
1720
  if (this.options.encodeBytecode) {
1701
1721
  sections.push(`var BYTECODE = "${this._encodeBytecode(flat)}";`);
1702
1722
  } else {
1703
- // Flatten each [op, ...operands] instruction into individual u16 slots.
1704
-
1705
1723
  sections.push(`var BYTECODE = [${flat.join(",")}]`);
1706
1724
  }
1707
-
1708
- // MAIN_START_PC
1709
1725
  sections.push(`var MAIN_START_PC = ${mainStartPc};`);
1726
+ sections.push(`var MAIN_REG_COUNT = ${mainRegCount};`);
1710
1727
  sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
1711
1728
  sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
1712
- // Opcodes
1713
1729
  const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
1714
1730
  sections.push(`var OP = ${generate(object).code};`);
1715
-
1716
- // Constants must be defined before the bytecode
1717
1731
  initBody.push(this._serializeConstants(constants));
1718
1732
  sections = [...initBody, ...sections];
1719
-
1720
- // VM runtime
1721
1733
  sections.push(VM_RUNTIME);
1722
1734
  return sections.join("\n\n");
1723
1735
  }
@@ -1725,12 +1737,8 @@ class Serializer {
1725
1737
  export async function compileAndSerialize(sourceCode, options) {
1726
1738
  const compiler = new Compiler(options);
1727
1739
  let bytecode = compiler.compile(sourceCode);
1728
-
1729
- // User transform passes (operate on unresolved IR with label/constant refs)
1730
- // macroOpcodes must run after selfModifying (so PATCH-stub bodies are in place)
1731
1740
  const passes = [];
1732
-
1733
- // Due to current implementation, specialized must run BEFORE macroOpcodes
1741
+ passes.push(concealConstants);
1734
1742
  if (options.specializedOpcodes) {
1735
1743
  passes.push(specializedOpcodes);
1736
1744
  }
@@ -1740,22 +1748,30 @@ export async function compileAndSerialize(sourceCode, options) {
1740
1748
  if (options.selfModifying) {
1741
1749
  passes.push(selfModifying);
1742
1750
  }
1751
+ if (options.aliasedOpcodes) {
1752
+ passes.push(aliasedOpcodes);
1753
+ }
1743
1754
  for (const pass of passes) {
1744
1755
  const passResult = pass(bytecode, compiler);
1745
1756
  bytecode = passResult.bytecode;
1746
1757
  }
1747
1758
 
1748
- // Assembler phases: resolve IR operands to plain integers before printing
1749
- const {
1750
- bytecode: labelResolved
1751
- } = resolveLabels(bytecode, compiler);
1752
- let {
1753
- bytecode: finalBytecode,
1754
- constants
1755
- } = resolveConstants(labelResolved);
1756
- const output = compiler.serializer.serialize(finalBytecode, constants, compiler);
1757
- const finalOutput = await obfuscateRuntime(output, finalBytecode, options, compiler);
1759
+ // Resolve label references to flat bytecode indices.
1760
+ const labelsResult = resolveLabels(bytecode, compiler);
1761
+ bytecode = labelsResult.bytecode;
1762
+
1763
+ // Set mainStartPc from the first function descriptor (or 0 for top-level start).
1764
+ compiler.mainStartPc = compiler.mainFn.startPc;
1765
+
1766
+ // Resolve constant references to pool indices (+ conceal key operand).
1767
+ const constResult = resolveConstants(bytecode, compiler);
1768
+ bytecode = constResult.bytecode;
1769
+ compiler.constants = constResult.constants;
1770
+
1771
+ // Build and obfuscate the runtime.
1772
+ const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
1773
+ const code = await obfuscateRuntime(runtimeSource, bytecode, options, compiler);
1758
1774
  return {
1759
- code: finalOutput
1775
+ code
1760
1776
  };
1761
1777
  }