js-confuser-vm 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/compiler.js CHANGED
@@ -8,6 +8,7 @@ import { ok } from "assert";
8
8
  import { obfuscateRuntime } from "./build-runtime.js";
9
9
  import { DEFAULT_OPTIONS } from "./options.js";
10
10
  import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
11
+ import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.js";
11
12
  import { resolveConstants } from "./transforms/bytecode/resolveContants.js";
12
13
  import { selfModifying } from "./transforms/bytecode/selfModifying.js";
13
14
  import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
@@ -17,8 +18,9 @@ import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
17
18
  import { getRandomInt } from "./utils/random-utils.js";
18
19
  import { U16_MAX } from "./utils/op-utils.js";
19
20
  import { concealConstants } from "./transforms/bytecode/concealConstants.js";
21
+ import { dispatcher } from "./transforms/bytecode/dispatcher.js";
20
22
  const traverse = traverseImport.default || traverseImport;
21
- 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 this._internals = {};\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 for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\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";
23
+ 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 \u2014 Lua/CPython style.\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\n// _absSlot is the absolute index in VM._regs (frame._base + local slot).\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\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\n// Frame \u2014 analogous to Lua CallInfo / CPython PyFrameObject.\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\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 = [];\n\n // \u2500\u2500 Flat register file (Lua-style) \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 // All frames share a single array. Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n this._internals = {};\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 // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\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 \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\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 for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + 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 regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + 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()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + 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 = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + 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 = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + 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 = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + 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 = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var ctor = regs[base + this._operand()];\n if (typeof ctor === \"function\") {\n regs[base + 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 regs[base + 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 regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + 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 regs[base + 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 regs[base + 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 (!regs[base + 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 (regs[base + 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 = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + 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 = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + 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 = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\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 regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n this._regsTop = frame._base;\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 this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + 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 0,\n );\n sub._currentFrame = f;\n for (var i = 0; i < args.length && i < c.fn.regCount; i++) {\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + 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] = regs[base + this._operand()];\n regs[base + 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 = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + 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 = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + 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 = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + 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 = regs[base + 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 regs[base + 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 = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + 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.JUMP_REG: {\n // Indirect jump: target PC is read from a register rather than a\n // bytecode immediate. Used by the jumpDispatcher pass so that static\n // analysis cannot determine the jump destination without tracking the\n // register value (which contains an encoded PC resolved at runtime).\n frame._pc = regs[base + this._operand()];\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 this._regsTop = searchFrame._base;\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 this._regs[handledFrame._base + h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;\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 for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\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";
22
24
  export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
23
25
  export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
24
26
 
@@ -149,21 +151,26 @@ export const OP_ORIGINAL = {
149
151
  // destPc, sliceStart, sliceEnd
150
152
 
151
153
  // ── Debug ─────────────────────────────────────────────────────────────────
152
- DEBUGGER: 57
154
+ DEBUGGER: 57,
155
+ // ── Indirect jump (register-addressed) ───────────────────────────────────
156
+ // Used by the jumpDispatcher pass. The target PC is read from a register
157
+ // rather than encoded as a bytecode immediate, so static analysis cannot
158
+ // determine the destination without tracking register values at runtime.
159
+ JUMP_REG: 58 // src — frame._pc = regs[src]
153
160
  };
154
161
 
155
162
  // ── Scope ─────────────────────────────────────────────────────────────────────
156
- // Maps variable names to register indices (slot numbers).
157
- // Locals are allocated at compile time; zero name lookups at runtime.
163
+ // Maps variable names to virtual RegisterOperands.
164
+ // Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
165
+ // resolveRegisters() assigns concrete slot indices before serialisation.
158
166
  class Scope {
159
167
  constructor(parent = null) {
160
168
  this.parent = parent;
161
169
  this._locals = new Map();
162
- this._next = 0;
163
170
  }
164
- define(name) {
171
+ define(name, ctx) {
165
172
  if (!this._locals.has(name)) {
166
- this._locals.set(name, this._next++);
173
+ this._locals.set(name, ctx._newReg());
167
174
  }
168
175
  return this._locals.get(name);
169
176
  }
@@ -171,7 +178,7 @@ class Scope {
171
178
  if (this._locals.has(name)) {
172
179
  return {
173
180
  kind: "local",
174
- slot: this._locals.get(name)
181
+ reg: this._locals.get(name)
175
182
  };
176
183
  }
177
184
  if (this.parent) return this.parent.resolve(name);
@@ -179,38 +186,68 @@ class Scope {
179
186
  kind: "global"
180
187
  };
181
188
  }
182
- get localCount() {
183
- return this._next;
184
- }
185
189
  }
186
190
 
187
191
  // ── FnContext ─────────────────────────────────────────────────────────────────
188
192
  // Compiler-side state for the function currently being compiled.
189
193
  // Distinct from the runtime Frame — this is compile-time only.
194
+ //
195
+ // Virtual-register model (Lua/LLVM style):
196
+ // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
197
+ // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
198
+ // liveness-aware slot assignment and sets desc.regCount at the end of the
199
+ // pipeline, just like resolveLabels() fills in jump targets.
190
200
  class FnContext {
191
- // Register allocator. Locals occupy regs[0..scope.localCount-1];
192
- // temps are allocated above that and freed at statement boundaries.
193
- regTop = 0;
194
- maxRegTop = 0;
195
- constructor(compiler, parentCtx = null) {
201
+ // index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
202
+
203
+ // Unique ID for this function — matches the index in compiler.fnDescriptors.
204
+
205
+ // Monotonically increasing counter; each call to _newReg() bumps it.
206
+ _nextId = 0;
207
+ constructor(compiler, parentCtx = null, fnId = 0) {
196
208
  this.compiler = compiler;
197
209
  this.parentCtx = parentCtx;
198
210
  this.scope = new Scope();
199
211
  this.bc = [];
200
212
  this.upvalues = [];
213
+ this._fnId = fnId;
214
+ }
215
+
216
+ /** Create a new virtual register owned by this function. */
217
+ _newReg() {
218
+ return b.registerOperand(this._nextId++, this._fnId);
201
219
  }
202
220
 
203
- /** Allocate the next free temp register and return its index. */
221
+ /**
222
+ * Allocate a short-lived temporary register (pool "temp::").
223
+ * resolveRegisters() will reuse its concrete slot once its live range ends.
224
+ * Do NOT use for named locals or upvalue-captured variables — use _newReg()
225
+ * via scope.define() for those, so they stay in the stable "local::" pool.
226
+ */
204
227
  allocReg() {
205
- const r = this.regTop++;
206
- if (this.regTop > this.maxRegTop) this.maxRegTop = this.regTop;
207
- return r;
228
+ return b.registerOperand(this._nextId++, this._fnId, {
229
+ kind: "temp"
230
+ });
208
231
  }
209
232
 
210
- /** Release all temps above the local region. Called at each statement boundary. */
211
- resetTemps() {
212
- this.regTop = this.scope.localCount;
233
+ /**
234
+ * Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
235
+ *
236
+ * NOTE: This is extraneous for any programmatically generated IR.
237
+ * resolveRegisters() already computes lastUse as the last instruction index
238
+ * where the register appears as a real operand — which is always the tightest
239
+ * correct bound when you stop emitting a register after its last logical use.
240
+ * freeReg is only needed in the rare case where a register has a late syntactic
241
+ * appearance that does NOT represent its true logical death (e.g. a dummy read
242
+ * emitted for side-effects long after the value is logically dead). No current
243
+ * pass in this codebase uses it; it is kept as an extension point only.
244
+ */
245
+ freeReg(bc, reg) {
246
+ bc.push([null, b.freeRegOperand(reg)]);
213
247
  }
248
+
249
+ /** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
250
+ resetTemps() {}
214
251
  addUpvalue(name, isLocal, index) {
215
252
  const existing = this.upvalues.findIndex(u => u.name === name);
216
253
  if (existing !== -1) return existing;
@@ -229,7 +266,15 @@ export class Compiler {
229
266
  * globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
230
267
  * opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
231
268
 
269
+ _cloneRegisterOperand(operand) {
270
+ if (!operand || typeof operand !== "object") return operand;
271
+ if (operand.type !== "register") return operand;
272
+ return JSON.parse(JSON.stringify(operand));
273
+ }
232
274
  emit(bc, instr, node) {
275
+ for (let i = 1; i < instr.length; i++) {
276
+ instr[i] = this._cloneRegisterOperand(instr[i]);
277
+ }
233
278
  bc.push(instr);
234
279
  instr[SOURCE_NODE_SYM] = node;
235
280
  }
@@ -280,7 +325,7 @@ export class Compiler {
280
325
  if (ctx.scope._locals.has(name)) {
281
326
  return {
282
327
  kind: "local",
283
- slot: ctx.scope._locals.get(name)
328
+ reg: ctx.scope._locals.get(name)
284
329
  };
285
330
  }
286
331
  if (!ctx.parentCtx) return {
@@ -291,7 +336,7 @@ export class Compiler {
291
336
  kind: "global"
292
337
  };
293
338
  const isLocal = parentResult.kind === "local";
294
- const index = isLocal ? parentResult.slot : parentResult.index;
339
+ const index = isLocal ? parentResult.reg : parentResult.index;
295
340
  const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
296
341
  return {
297
342
  kind: "upvalue",
@@ -300,31 +345,30 @@ export class Compiler {
300
345
  }
301
346
 
302
347
  // ── Variable hoisting ──────────────────────────────────────────────────────
303
- // Pre-scan a statement list and reserve scope slots for every var declaration,
304
- // function declaration, for-in iterator, and try-catch binding.
305
- // Must be called before compilation begins so that ctx.regTop can be set
306
- // safely above ALL locals (including those declared late in the body).
307
- _hoistVars(stmts, scope) {
348
+ // Pre-scan a statement list and reserve virtual registers for every var
349
+ // declaration, function declaration, for-in iterator, and try-catch binding.
350
+ // Must be called before any emit so that locals are allocated before temps.
351
+ _hoistVars(stmts, scope, ctx) {
308
352
  for (const stmt of stmts) {
309
353
  switch (stmt.type) {
310
354
  case "VariableDeclaration":
311
355
  for (const decl of stmt.declarations) {
312
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
356
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
313
357
  }
314
358
  break;
315
359
  case "FunctionDeclaration":
316
- if (stmt.id) scope.define(stmt.id.name);
360
+ if (stmt.id) scope.define(stmt.id.name, ctx);
317
361
  break;
318
362
  case "BlockStatement":
319
- this._hoistVars(stmt.body, scope);
363
+ this._hoistVars(stmt.body, scope, ctx);
320
364
  break;
321
365
  case "IfStatement":
322
366
  {
323
367
  const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
324
- this._hoistVars(cons, scope);
368
+ this._hoistVars(cons, scope, ctx);
325
369
  if (stmt.alternate) {
326
370
  const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
327
- this._hoistVars(alt, scope);
371
+ this._hoistVars(alt, scope, ctx);
328
372
  }
329
373
  break;
330
374
  }
@@ -332,51 +376,51 @@ export class Compiler {
332
376
  case "DoWhileStatement":
333
377
  {
334
378
  const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
335
- this._hoistVars(body, scope);
379
+ this._hoistVars(body, scope, ctx);
336
380
  break;
337
381
  }
338
382
  case "ForStatement":
339
383
  {
340
384
  if (stmt.init?.type === "VariableDeclaration") {
341
385
  for (const decl of stmt.init.declarations) {
342
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
386
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
343
387
  }
344
388
  }
345
389
  const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
346
- this._hoistVars(body, scope);
390
+ this._hoistVars(body, scope, ctx);
347
391
  break;
348
392
  }
349
393
  case "ForInStatement":
350
394
  {
351
- // Reserve a hidden register for the iterator object.
352
- stmt._iterSlot = scope._next++;
395
+ // Reserve a hidden virtual register for the iterator object.
396
+ stmt._iterSlot = ctx._newReg();
353
397
  if (stmt.left.type === "VariableDeclaration") {
354
398
  for (const decl of stmt.left.declarations) {
355
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
399
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
356
400
  }
357
401
  }
358
402
  const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
359
- this._hoistVars(body, scope);
403
+ this._hoistVars(body, scope, ctx);
360
404
  break;
361
405
  }
362
406
  case "SwitchStatement":
363
- for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
407
+ for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
364
408
  break;
365
409
  case "TryStatement":
366
- this._hoistVars(stmt.block.body, scope);
410
+ this._hoistVars(stmt.block.body, scope, ctx);
367
411
  if (stmt.handler) {
368
412
  if (stmt.handler.param?.type === "Identifier") {
369
413
  // Catch parameter IS the exception register.
370
- scope.define(stmt.handler.param.name);
414
+ scope.define(stmt.handler.param.name, ctx);
371
415
  } else {
372
- // No catch binding – reserve a dummy slot for the exception value.
373
- stmt._exceptionSlot = scope._next++;
416
+ // No catch binding – reserve a dummy virtual register for the exception value.
417
+ stmt._exceptionSlot = ctx._newReg();
374
418
  }
375
- this._hoistVars(stmt.handler.body.body, scope);
419
+ this._hoistVars(stmt.handler.body.body, scope, ctx);
376
420
  }
377
421
  break;
378
422
  case "LabeledStatement":
379
- this._hoistVars([stmt.body], scope);
423
+ this._hoistVars([stmt.body], scope, ctx);
380
424
  break;
381
425
  }
382
426
  }
@@ -385,7 +429,8 @@ export class Compiler {
385
429
  // ── Entry point ───────────────────────────────────────────────────────────
386
430
  compile(source) {
387
431
  const ast = parse(source, {
388
- sourceType: "script"
432
+ sourceType: "script",
433
+ allowReturnOutsideFunction: true
389
434
  });
390
435
  return this.compileAST(ast);
391
436
  }
@@ -402,28 +447,24 @@ export class Compiler {
402
447
  const entryLabel = this._makeLabel(`fn_${fnIdx}`);
403
448
  var desc = {};
404
449
  this.fnDescriptors.push(desc);
405
- const ctx = new FnContext(this, this._currentCtx);
450
+ const ctx = new FnContext(this, this._currentCtx, fnIdx);
406
451
  const savedCtx = this._currentCtx;
407
452
  this._currentCtx = ctx;
408
453
  const savedLoopStack = this._loopStack;
409
454
  this._loopStack = [];
410
455
 
411
- // 1. Define parameters (occupy regs 0..paramCount-1).
456
+ // 1. Define parameters as virtual registers (occupy the first IDs in order).
412
457
  for (const param of node.params) {
413
458
  let identifier = param.type === "AssignmentPattern" ? param.left : param;
414
459
  ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
415
- ctx.scope.define(identifier.name);
460
+ ctx.scope.define(identifier.name, ctx);
416
461
  }
417
462
 
418
- // 2. Reserve the `arguments` slot (reg index = paramCount).
419
- ctx.scope.define("arguments");
463
+ // 2. Reserve the `arguments` virtual register (immediately after params).
464
+ ctx.scope.define("arguments", ctx);
420
465
 
421
- // 3. Hoist all var declarations so temps start above every local.
422
- this._hoistVars(node.body.body, ctx.scope);
423
-
424
- // 4. Temps now start above all locals.
425
- ctx.regTop = ctx.scope.localCount;
426
- ctx.maxRegTop = ctx.regTop;
466
+ // 3. Hoist all var declarations so locals are allocated before any temps.
467
+ this._hoistVars(node.body.body, ctx.scope, ctx);
427
468
 
428
469
  // 5. Emit default-value guards.
429
470
  for (const param of node.params) {
@@ -469,25 +510,29 @@ export class Compiler {
469
510
  desc.bytecode = ctx.bc;
470
511
  desc._fnIdx = fnIdx;
471
512
  desc.paramCount = node.params.length;
472
- desc.regCount = ctx.maxRegTop; // total registers needed at runtime
513
+ // regCount is NOT set here resolveRegisters() fills it after liveness analysis.
473
514
  desc.upvalues = ctx.upvalues.slice();
515
+ desc.ctx = ctx;
474
516
  return desc;
475
517
  }
476
518
 
477
519
  // Emit MAKE_CLOSURE with all metadata as inline operands.
478
520
  // Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
521
+ // regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
479
522
  _emitMakeClosure(desc, node, bc) {
480
523
  const ctx = this._currentCtx;
481
524
  const dst = ctx.allocReg();
482
525
  const uvOperands = [];
483
526
  for (const uv of desc.upvalues) {
484
527
  uvOperands.push(uv.isLocal ? 1 : 0);
485
- uvOperands.push(uv.index);
528
+ uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
486
529
  }
487
530
  this.emit(bc, [this.OP.MAKE_CLOSURE, dst, {
488
531
  type: "label",
489
532
  label: desc.entryLabel
490
- }, desc.paramCount, desc.regCount, desc.upvalues.length, ...uvOperands], node);
533
+ }, desc.paramCount, b.fnRegCountOperand(desc._fnIdx),
534
+ // resolved by resolveRegisters()
535
+ desc.upvalues.length, ...uvOperands], node);
491
536
  return dst;
492
537
  }
493
538
 
@@ -501,7 +546,7 @@ export class Compiler {
501
546
  async: false,
502
547
  generator: false,
503
548
  params: [],
504
- id: null,
549
+ id: t.identifier("main"),
505
550
  body: t.blockStatement([...body])
506
551
  });
507
552
  for (const descriptor of this.fnDescriptors) {
@@ -513,7 +558,8 @@ export class Compiler {
513
558
  this.bytecode.push(instr);
514
559
  }
515
560
  }
516
- this.mainRegCount = mainCtx.maxRegTop;
561
+
562
+ // mainRegCount is set by resolveRegisters() after the pipeline runs.
517
563
  this.mainFn = desc;
518
564
  this._currentCtx = savedCtx;
519
565
  }
@@ -592,14 +638,8 @@ export class Compiler {
592
638
  this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
593
639
  }
594
640
  } else {
595
- this.emit(bc, [this.OP.MOVE, slot, ctx.allocReg()], node);
596
- // Load undefined into the just-allocated temp, then move.
597
- // Actually: just emit LOAD_CONST directly into slot.
598
- // Undo the allocReg – instead emit directly:
599
- ctx.regTop--; // undo the allocReg above
600
- const tmp = ctx.allocReg();
601
- this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
602
- if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
641
+ // No initializer: var x; → load undefined directly into the local's register.
642
+ this.emit(bc, [this.OP.LOAD_CONST, slot, b.constantOperand(undefined)], node);
603
643
  }
604
644
  } else {
605
645
  if (decl.init) {
@@ -617,14 +657,11 @@ export class Compiler {
617
657
  case "IfStatement":
618
658
  {
619
659
  const elseOrEndLabel = this._makeLabel("if_else");
620
- const savedTop = ctx.regTop;
621
660
  const testReg = this._compileExpr(node.test, scope, bc);
622
661
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
623
662
  type: "label",
624
663
  label: elseOrEndLabel
625
664
  }], node);
626
- ctx.regTop = savedTop; // free test temps
627
-
628
665
  const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
629
666
  for (const stmt of consequentBody) {
630
667
  this._compileStatement(stmt, scope, bc);
@@ -671,13 +708,11 @@ export class Compiler {
671
708
  type: "defineLabel",
672
709
  label: loopTopLabel
673
710
  }], node);
674
- const savedTop = ctx.regTop;
675
711
  const testReg = this._compileExpr(node.test, scope, bc);
676
712
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
677
713
  type: "label",
678
714
  label: exitLabel
679
715
  }], node);
680
- ctx.regTop = savedTop;
681
716
  const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
682
717
  for (const stmt of whileBody) {
683
718
  this._compileStatement(stmt, scope, bc);
@@ -718,13 +753,11 @@ export class Compiler {
718
753
  type: "defineLabel",
719
754
  label: continueLabel
720
755
  }], node);
721
- const savedTop = ctx.regTop;
722
756
  const testReg = this._compileExpr(node.test, scope, bc);
723
757
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
724
758
  type: "label",
725
759
  label: exitLabel
726
760
  }], node);
727
- ctx.regTop = savedTop;
728
761
  this.emit(bc, [this.OP.JUMP, {
729
762
  type: "label",
730
763
  label: loopTopLabel
@@ -762,13 +795,11 @@ export class Compiler {
762
795
  label: loopTopLabel
763
796
  }], node);
764
797
  if (node.test) {
765
- const savedTop = ctx.regTop;
766
798
  const testReg = this._compileExpr(node.test, scope, bc);
767
799
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
768
800
  type: "label",
769
801
  label: exitLabel
770
802
  }], node);
771
- ctx.regTop = savedTop;
772
803
  }
773
804
  const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
774
805
  for (const stmt of forBody) {
@@ -880,7 +911,6 @@ export class Compiler {
880
911
  const cas = cases[i];
881
912
  if (cas.test === null) continue;
882
913
  const nextCheckLabel = this._makeLabel("sw_next");
883
- const savedTop = ctx.regTop;
884
914
  const caseValReg = this._compileExpr(cas.test, scope, bc);
885
915
  const cmpReg = ctx.allocReg();
886
916
  this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
@@ -888,7 +918,6 @@ export class Compiler {
888
918
  type: "label",
889
919
  label: nextCheckLabel
890
920
  }], node);
891
- ctx.regTop = savedTop;
892
921
  this.emit(bc, [this.OP.JUMP, {
893
922
  type: "label",
894
923
  label: caseLabels[i]
@@ -992,7 +1021,7 @@ export class Compiler {
992
1021
  } else if (node.left.type === "Identifier") {
993
1022
  const res = this._resolve(node.left.name, this._currentCtx);
994
1023
  if (res.kind === "local") {
995
- if (keyReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, keyReg], node);
1024
+ if (keyReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
996
1025
  } else if (res.kind === "upvalue") {
997
1026
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
998
1027
  } else {
@@ -1076,12 +1105,25 @@ export class Compiler {
1076
1105
  }
1077
1106
 
1078
1107
  // ── Expressions ───────────────────────────────────────────────────────────
1079
- // Returns the register index that holds the result.
1080
- // For local variables: returns their slot directly (no instruction emitted).
1081
- // For all others: allocates a fresh temp register, emits the instruction(s),
1108
+ // Returns the virtual RegisterOperand that holds the result.
1109
+ // For local variables: returns their RegisterOperand directly (no instruction emitted).
1110
+ // For all others: allocates a fresh virtual register, emits the instruction(s),
1082
1111
  // and returns the allocated register.
1083
1112
  _compileExpr(node, scope, bc) {
1084
1113
  const ctx = this._currentCtx;
1114
+
1115
+ // Intrinsic for emitting raw bytecode, useful for emitting register address
1116
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_") {
1117
+ const argJSONStrng = node.arguments[0].value;
1118
+ console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
1119
+ const arg = JSON.parse(argJSONStrng);
1120
+ console.log("Parsed bytecode:", arg);
1121
+ const dst = ctx.allocReg();
1122
+ let operand = arg[0];
1123
+ this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
1124
+
1125
+ return dst;
1126
+ }
1085
1127
  switch (node.type) {
1086
1128
  case "NumericLiteral":
1087
1129
  case "StringLiteral":
@@ -1100,7 +1142,7 @@ export class Compiler {
1100
1142
  case "Identifier":
1101
1143
  {
1102
1144
  const res = this._resolve(node.name, this._currentCtx);
1103
- if (res.kind === "local") return res.slot; // register IS the local
1145
+ if (res.kind === "local") return res.reg; // register IS the local
1104
1146
  if (res.kind === "upvalue") {
1105
1147
  const dst = ctx.allocReg();
1106
1148
  this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
@@ -1129,9 +1171,7 @@ export class Compiler {
1129
1171
  {
1130
1172
  const exprs = node.expressions;
1131
1173
  for (let i = 0; i < exprs.length - 1; i++) {
1132
- const savedTop = ctx.regTop;
1133
- this._compileExpr(exprs[i], scope, bc);
1134
- ctx.regTop = savedTop; // discard intermediate result
1174
+ this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
1135
1175
  }
1136
1176
  return this._compileExpr(exprs[exprs.length - 1], scope, bc);
1137
1177
  }
@@ -1140,17 +1180,13 @@ export class Compiler {
1140
1180
  const n = node;
1141
1181
  const elseLabel = this._makeLabel("ternary_else");
1142
1182
  const endLabel = this._makeLabel("ternary_end");
1143
-
1144
- // Compile test; free its temps after the jump is emitted.
1145
- const baseTop = ctx.regTop;
1146
1183
  const testReg = this._compileExpr(n.test, scope, bc);
1147
1184
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
1148
1185
  type: "label",
1149
1186
  label: elseLabel
1150
1187
  }], node);
1151
- ctx.regTop = baseTop; // free test temps
1152
1188
 
1153
- // Reserve reg_result at the base of the temp space.
1189
+ // reg_result is a stable virtual register both branches write into.
1154
1190
  const reg_result = ctx.allocReg();
1155
1191
 
1156
1192
  // Consequent branch.
@@ -1161,22 +1197,18 @@ export class Compiler {
1161
1197
  label: endLabel
1162
1198
  }], node);
1163
1199
 
1164
- // Alternate branch: reset to baseTop then re-reserve reg_result.
1200
+ // Alternate branch each allocReg() gets a unique virtual ID so no
1201
+ // slot collision is possible; no need to "re-occupy" reg_result.
1165
1202
  this.emit(bc, [null, {
1166
1203
  type: "defineLabel",
1167
1204
  label: elseLabel
1168
1205
  }], node);
1169
- ctx.regTop = baseTop;
1170
- ctx.allocReg(); // re-occupy reg_result slot
1171
1206
  const altReg = this._compileExpr(n.alternate, scope, bc);
1172
1207
  if (altReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
1173
1208
  this.emit(bc, [null, {
1174
1209
  type: "defineLabel",
1175
1210
  label: endLabel
1176
1211
  }], node);
1177
-
1178
- // Leave reg_result allocated above baseTop.
1179
- ctx.regTop = baseTop + 1;
1180
1212
  return reg_result;
1181
1213
  }
1182
1214
  case "LogicalExpression":
@@ -1185,9 +1217,7 @@ export class Compiler {
1185
1217
  const endLabel = this._makeLabel("logical_end");
1186
1218
  const isOr = n.operator === "||";
1187
1219
  if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
1188
- const baseTop = ctx.regTop;
1189
1220
  const lhsReg = this._compileExpr(n.left, scope, bc);
1190
- ctx.regTop = baseTop;
1191
1221
  const reg_result = ctx.allocReg();
1192
1222
  if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
1193
1223
 
@@ -1199,15 +1229,12 @@ export class Compiler {
1199
1229
  }], node);
1200
1230
 
1201
1231
  // Compile RHS into reg_result.
1202
- ctx.regTop = baseTop;
1203
- ctx.allocReg(); // re-occupy reg_result
1204
1232
  const rhsReg = this._compileExpr(n.right, scope, bc);
1205
1233
  if (rhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
1206
1234
  this.emit(bc, [null, {
1207
1235
  type: "defineLabel",
1208
1236
  label: endLabel
1209
1237
  }], node);
1210
- ctx.regTop = baseTop + 1;
1211
1238
  return reg_result;
1212
1239
  }
1213
1240
  case "TemplateLiteral":
@@ -1269,7 +1296,8 @@ export class Compiler {
1269
1296
 
1270
1297
  // Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
1271
1298
  const applyBump = curReg => {
1272
- const postfixReg = n.prefix ? -1 : (() => {
1299
+ const postfixReg = n.prefix ? curReg // prefix: postfix copy unused; caller returns newReg instead
1300
+ : (() => {
1273
1301
  const r = ctx.allocReg();
1274
1302
  this.emit(bc, [this.OP.MOVE, r, curReg], node);
1275
1303
  return r;
@@ -1301,7 +1329,7 @@ export class Compiler {
1301
1329
  const res = this._resolve(name, this._currentCtx);
1302
1330
  let curReg;
1303
1331
  if (res.kind === "local") {
1304
- curReg = res.slot;
1332
+ curReg = res.reg;
1305
1333
  } else if (res.kind === "upvalue") {
1306
1334
  curReg = ctx.allocReg();
1307
1335
  this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
@@ -1311,7 +1339,7 @@ export class Compiler {
1311
1339
  }
1312
1340
  const [postfixReg, newReg] = applyBump(curReg);
1313
1341
  if (res.kind === "local") {
1314
- this.emit(bc, [this.OP.MOVE, res.slot, newReg], node);
1342
+ this.emit(bc, [this.OP.MOVE, res.reg, newReg], node);
1315
1343
  } else if (res.kind === "upvalue") {
1316
1344
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, newReg], node);
1317
1345
  } else {
@@ -1369,7 +1397,7 @@ export class Compiler {
1369
1397
  // Load current value of the variable.
1370
1398
  let curReg;
1371
1399
  if (res.kind === "local") {
1372
- curReg = res.slot;
1400
+ curReg = res.reg;
1373
1401
  } else if (res.kind === "upvalue") {
1374
1402
  curReg = ctx.allocReg();
1375
1403
  this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
@@ -1386,8 +1414,8 @@ export class Compiler {
1386
1414
 
1387
1415
  // Store result and return it.
1388
1416
  if (res.kind === "local") {
1389
- if (rhsReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, rhsReg], node);
1390
- return res.slot;
1417
+ if (rhsReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
1418
+ return res.reg;
1391
1419
  } else if (res.kind === "upvalue") {
1392
1420
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
1393
1421
  return rhsReg;
@@ -1608,6 +1636,7 @@ class Serializer {
1608
1636
  const v = constants[idx];
1609
1637
  if (!key) return v;
1610
1638
  if (typeof v === "number") return v ^ key;
1639
+ if (typeof v !== "string") return v;
1611
1640
  // String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
1612
1641
  const bytes = Buffer.from(v, "base64");
1613
1642
  let out = "";
@@ -1617,16 +1646,25 @@ class Serializer {
1617
1646
  }
1618
1647
  return out;
1619
1648
  }
1620
- _serializeInstr(instr) {
1649
+ _generateComment(instr) {
1621
1650
  const op = instr[0];
1622
1651
  const operands = instr.slice(1);
1623
- const constants = this.compiler.constants;
1624
- const resolvedOperands = operands.filter(operand => operand?.placeholder !== true).map(o => o?.resolvedValue ?? o);
1625
- for (const o of resolvedOperands) {
1626
- ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
1627
- ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
1652
+ if (op === null && operands[0]?.type === "defineLabel") {
1653
+ const label = operands[0].label;
1654
+ return `${label}:`;
1628
1655
  }
1629
- ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
1656
+ const constants = this.compiler.constants;
1657
+ const emittedOperands = operands.filter(operand => operand?.placeholder !== true);
1658
+ const resolvedOperands = emittedOperands.map(o => o?.resolvedValue ?? o);
1659
+ const displayOperands = operands.map((o, i) => {
1660
+ const resolvedValue = resolvedOperands[i];
1661
+ const label = o?.label;
1662
+ let displayOperand = resolvedValue;
1663
+ if (label) {
1664
+ return label;
1665
+ }
1666
+ return displayOperand;
1667
+ });
1630
1668
  let name = this.OP_NAME[op];
1631
1669
  if (!name || name.includes("{")) {
1632
1670
  name = `OP_${op}`;
@@ -1637,73 +1675,79 @@ class Serializer {
1637
1675
  }
1638
1676
  const sourceNode = instr[SOURCE_NODE_SYM];
1639
1677
  const sourceLocation = sourceNode?.loc ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)].filter(Boolean).join("-") : "";
1640
- if (resolvedOperands.length > 0) {
1678
+ if (displayOperands.length > 0) {
1641
1679
  // Operand[0] is always `dst` for instruction types that produce a value.
1642
- const dst = resolvedOperands[0];
1680
+ const dst = displayOperands[0];
1643
1681
  switch (op) {
1644
1682
  case this.OP.LOAD_CONST:
1645
1683
  {
1646
1684
  // resolvedOperands: [dst, constIdx, concealKey]
1647
- const val = this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2]);
1685
+ const val = this._decryptConst(constants, displayOperands[1], displayOperands[2]);
1648
1686
  comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
1649
1687
  break;
1650
1688
  }
1689
+ case this.OP.LOAD_INT:
1690
+ {
1691
+ // resolvedOperands: [dst, intValue]
1692
+ comment += ` reg[${dst}] = ${displayOperands[1]}`;
1693
+ break;
1694
+ }
1651
1695
  case this.OP.LOAD_GLOBAL:
1652
1696
  // resolvedOperands: [dst, constIdx, concealKey]
1653
- comment += ` reg[${dst}] = ${this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2])}`;
1697
+ comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
1654
1698
  break;
1655
1699
  case this.OP.STORE_GLOBAL:
1656
1700
  // resolvedOperands: [constIdx, concealKey, srcReg]
1657
- comment += ` ${this._decryptConst(constants, resolvedOperands[0], resolvedOperands[1])} = reg[${resolvedOperands[2]}]`;
1701
+ comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
1658
1702
  break;
1659
1703
  case this.OP.LOAD_UPVALUE:
1660
- comment += ` reg[${dst}] = upvalue[${resolvedOperands[1]}]`;
1704
+ comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
1661
1705
  break;
1662
1706
  case this.OP.STORE_UPVALUE:
1663
- comment += ` upvalue[${resolvedOperands[0]}] = reg[${resolvedOperands[1]}]`;
1707
+ comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
1664
1708
  break;
1665
1709
  case this.OP.MOVE:
1666
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}]`;
1710
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
1667
1711
  break;
1668
1712
  case this.OP.MAKE_CLOSURE:
1669
- comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
1713
+ comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
1670
1714
  break;
1671
1715
  case this.OP.CALL:
1672
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
1716
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands.slice(3).map(v => `reg[${v}]`).join(", ")})`;
1673
1717
  break;
1674
1718
  case this.OP.CALL_METHOD:
1675
- comment += ` reg[${dst}] = reg[${resolvedOperands[2]}](recv=reg[${resolvedOperands[1]}], ${resolvedOperands[3]} args)`;
1719
+ comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
1676
1720
  break;
1677
1721
  case this.OP.NEW:
1678
- comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
1722
+ comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
1679
1723
  break;
1680
1724
  case this.OP.RETURN:
1681
- comment += ` reg[${resolvedOperands[0]}]`;
1725
+ comment += ` reg[${displayOperands[0]}]`;
1682
1726
  break;
1683
1727
  case this.OP.BUILD_ARRAY:
1684
- comment += ` reg[${dst}] = [${resolvedOperands[2]} elems]`;
1728
+ comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
1685
1729
  break;
1686
1730
  case this.OP.BUILD_OBJECT:
1687
- comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
1731
+ comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
1688
1732
  break;
1689
1733
  case this.OP.GET_PROP:
1690
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}][reg[${resolvedOperands[2]}]]`;
1734
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
1691
1735
  break;
1692
1736
  case this.OP.SET_PROP:
1693
- comment += ` reg[${resolvedOperands[0]}][reg[${resolvedOperands[1]}]] = reg[${resolvedOperands[2]}]`;
1737
+ comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
1738
+ break;
1739
+ case this.OP.JUMP_REG:
1740
+ comment += ` PC = reg[${displayOperands[0]}]`;
1694
1741
  break;
1695
1742
  default:
1696
- comment += resolvedOperands.length === 1 ? ` ${resolvedOperands[0]}` : ` [${resolvedOperands.join(", ")}]`;
1743
+ comment += displayOperands.length === 1 ? ` ${displayOperands[0]}` : ` [${displayOperands.join(", ")}]`;
1697
1744
  }
1698
1745
  }
1699
1746
  comment = comment.padEnd(50) + sourceLocation;
1700
1747
  const values = [op, ...resolvedOperands];
1701
1748
  const instrText = `[${values.join(", ")}]`;
1702
1749
  const text = `${(instrText + ",").padEnd(20)} ${comment}`;
1703
- return {
1704
- text,
1705
- values
1706
- };
1750
+ return text;
1707
1751
  }
1708
1752
  _serializeConstants(constants) {
1709
1753
  const lines = ["var CONSTANTS = ["];
@@ -1716,14 +1760,23 @@ class Serializer {
1716
1760
  _serializeBytecode(bytecode, compiler) {
1717
1761
  const serialized = [];
1718
1762
  for (const instr of bytecode) {
1719
- if (instr[0] === null) continue;
1763
+ const op = instr[0];
1764
+ const operands = instr.slice(1);
1765
+ if (instr[0] === null) continue; // null opcodes are not emitted
1766
+
1767
+ const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
1720
1768
  const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
1721
1769
  if (specializedOpInfo) {
1722
- const operands = instr.slice(1);
1723
- const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
1724
1770
  const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
1725
1771
  compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValues.join("_")}`;
1726
1772
  }
1773
+
1774
+ // Validate no opcode or operand exceeds u16 limit
1775
+ for (const o of resolvedValues) {
1776
+ ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
1777
+ ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
1778
+ }
1779
+ ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
1727
1780
  serialized.push(instr);
1728
1781
  }
1729
1782
  return {
@@ -1769,6 +1822,14 @@ class Serializer {
1769
1822
  export async function compileAndSerialize(sourceCode, options) {
1770
1823
  const compiler = new Compiler(options);
1771
1824
  let bytecode = compiler.compile(sourceCode);
1825
+
1826
+ // jumpDispatcher must run before resolveRegisters so that the new rDisp/rKey
1827
+ // RegisterOperand objects it injects are visible to the liveness analysis.
1828
+ // It must also run before resolveLabels since it emits encodedLabel IR operands.
1829
+ if (options.dispatcher) {
1830
+ const dispatcherResult = dispatcher(bytecode, compiler);
1831
+ bytecode = dispatcherResult.bytecode;
1832
+ }
1772
1833
  const passes = [];
1773
1834
  passes.push(concealConstants);
1774
1835
  if (options.specializedOpcodes) {
@@ -1780,9 +1841,6 @@ export async function compileAndSerialize(sourceCode, options) {
1780
1841
  if (options.macroOpcodes) {
1781
1842
  passes.push(macroOpcodes);
1782
1843
  }
1783
- if (options.selfModifying) {
1784
- passes.push(selfModifying);
1785
- }
1786
1844
  if (options.aliasedOpcodes) {
1787
1845
  passes.push(aliasedOpcodes);
1788
1846
  }
@@ -1791,6 +1849,21 @@ export async function compileAndSerialize(sourceCode, options) {
1791
1849
  bytecode = passResult.bytecode;
1792
1850
  }
1793
1851
 
1852
+ // Resolve virtual registers to concrete slot indices and set regCount per fn.
1853
+ // Must run BEFORE selfModifying: that pass moves body instructions to the end
1854
+ // of the bytecode while leaving RETURN in place, splitting a function's code
1855
+ // into two non-contiguous regions. Linear-scan liveness then sees incorrect
1856
+ // firstUse/lastUse for registers that span the gap, causing slot collisions.
1857
+ const regsResult = resolveRegisters(bytecode, compiler);
1858
+ bytecode = regsResult.bytecode;
1859
+
1860
+ // selfModifying runs after register resolution so concrete slot indices are
1861
+ // already in place; only label operands remain unresolved at this stage.
1862
+ if (options.selfModifying) {
1863
+ const smResult = selfModifying(bytecode, compiler);
1864
+ bytecode = smResult.bytecode;
1865
+ }
1866
+
1794
1867
  // Resolve label references to flat bytecode indices.
1795
1868
  const labelsResult = resolveLabels(bytecode, compiler);
1796
1869
  bytecode = labelsResult.bytecode;
@@ -1812,8 +1885,8 @@ export async function compileAndSerialize(sourceCode, options) {
1812
1885
  const generateBytecodeComment = () => {
1813
1886
  var lines = [];
1814
1887
  for (const instr of bytecode) {
1815
- const serialized = compiler.serializer._serializeInstr(instr);
1816
- lines.push("// " + serialized.text);
1888
+ const comment = compiler.serializer._generateComment(instr);
1889
+ lines.push("// " + comment);
1817
1890
  }
1818
1891
  return lines.join("\n");
1819
1892
  };