js-confuser-vm 0.0.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.gitmodules +4 -0
  2. package/CHANGELOG.md +125 -2
  3. package/README.md +128 -53
  4. package/bench.ts +146 -0
  5. package/disassemble.ts +12 -0
  6. package/dist/build-runtime.js +41 -15
  7. package/dist/compiler.js +328 -181
  8. package/dist/disassembler.js +317 -0
  9. package/dist/index.js +7 -2
  10. package/dist/runtime.js +255 -176
  11. package/dist/template.js +258 -0
  12. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  13. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  14. package/dist/transforms/bytecode/dispatcher.js +266 -0
  15. package/dist/transforms/bytecode/macroOpcodes.js +3 -3
  16. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  17. package/dist/transforms/bytecode/resolveLabels.js +21 -18
  18. package/dist/transforms/bytecode/resolveRegisters.js +216 -0
  19. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  20. package/dist/transforms/bytecode/specializedOpcodes.js +22 -12
  21. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  22. package/dist/transforms/runtime/classObfuscation.js +43 -0
  23. package/dist/transforms/runtime/handlerTable.js +91 -0
  24. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  25. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  26. package/dist/types.js +42 -1
  27. package/dist/utils/ast-utils.js +14 -0
  28. package/dist/utils/op-utils.js +1 -2
  29. package/dist/utils/pass-utils.js +100 -0
  30. package/dist/utils/profile-utils.js +3 -0
  31. package/index.ts +22 -16
  32. package/jest.config.js +19 -2
  33. package/output.disassembled.js +41 -0
  34. package/package.json +2 -1
  35. package/src/build-runtime.ts +113 -78
  36. package/src/compiler.ts +2703 -2482
  37. package/src/disassembler.ts +329 -0
  38. package/src/index.ts +12 -2
  39. package/src/options.ts +8 -1
  40. package/src/runtime.ts +294 -180
  41. package/src/template.ts +265 -0
  42. package/src/transforms/bytecode/aliasedOpcodes.ts +5 -2
  43. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  44. package/src/transforms/bytecode/dispatcher.ts +292 -0
  45. package/src/transforms/bytecode/macroOpcodes.ts +4 -4
  46. package/src/transforms/bytecode/resolveLabels.ts +31 -27
  47. package/src/transforms/bytecode/resolveRegisters.ts +226 -0
  48. package/src/transforms/bytecode/specializedOpcodes.ts +27 -20
  49. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  50. package/src/transforms/runtime/classObfuscation.ts +59 -0
  51. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  52. package/src/types.ts +106 -5
  53. package/src/utils/ast-utils.ts +19 -0
  54. package/src/utils/op-utils.ts +2 -2
  55. package/src/utils/pass-utils.ts +126 -0
  56. package/src/utils/profile-utils.ts +3 -0
  57. package/tsconfig.json +1 -1
  58. package/dist/transforms/utils/op-utils.js +0 -25
  59. package/dist/transforms/utils/random-utils.js +0 -27
  60. package/dist/utilts.js +0 -3
  61. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  62. package/src/transforms/runtime/internalVariables.ts +0 -270
  63. package/src/transforms/runtime/microOpcodes.ts +0 -93
  64. /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
package/dist/compiler.js CHANGED
@@ -5,20 +5,24 @@ import traverseImport from "@babel/traverse";
5
5
  import { generate } from "@babel/generator";
6
6
  import { stripTypeScriptTypes } from "module";
7
7
  import { ok } from "assert";
8
- import { obfuscateRuntime } from "./build-runtime.js";
8
+ import { buildRuntime } from "./build-runtime.js";
9
9
  import { DEFAULT_OPTIONS } from "./options.js";
10
10
  import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
11
- import { resolveConstants } from "./transforms/bytecode/resolveContants.js";
11
+ import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.js";
12
+ import { resolveConstants } from "./transforms/bytecode/resolveConstants.js";
12
13
  import { selfModifying } from "./transforms/bytecode/selfModifying.js";
13
14
  import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
14
- import { microOpcodes } from "./transforms/bytecode/microOpcodes.js";
15
15
  import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
16
16
  import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
17
17
  import { getRandomInt } from "./utils/random-utils.js";
18
18
  import { U16_MAX } from "./utils/op-utils.js";
19
19
  import { concealConstants } from "./transforms/bytecode/concealConstants.js";
20
+ import { dispatcher } from "./transforms/bytecode/dispatcher.js";
21
+ import { controlFlowFlattening } from "./transforms/bytecode/controlFlowFlattening.js";
22
+ import { stringConcealing } from "./transforms/bytecode/stringConcealing.js";
23
+ import { now } from "./utils/profile-utils.js";
20
24
  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";
25
+ 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 base64ToBytes(s) {\n return typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n}\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b = base64ToBytes(s);\n // Each slot is a u32 stored as 4 little-endian bytes.\n var r = new Uint32Array(b.length / 4);\n for (var i = 0; i < r.length; i++)\n r[i] =\n (b[i * 4] |\n (b[i * 4 + 1] << 8) |\n (b[i * 4 + 2] << 16) |\n (b[i * 4 + 3] << 24)) >>>\n 0;\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}\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 = base64ToBytes(v);\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\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 // globals[globalName] = regs[src]\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 (non-strict mode behavior)\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n // regs[dst] = delete regs[obj][regs[key]]\n // The delete operator returns true if successful which is most cases\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 // Arithmetic (dst, src1, src2)\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 // Comparison (dst, src1, src2)\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 // regs[dst] = regs[obj] instanceof regs[ctor]\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 // TODO: Why is this needed?\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 // Unary (dst, src)\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(); // consumes argument (intended)\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // regs[dst] = typeof window[name]\n // Never throws ReferenceError, instead returns undefined for undeclared variables\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 // Control flow\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 // Calls\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 if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\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 if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\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 if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\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\n // Zero out callee's register window to limit exposing runtime values\n var hi = frame._base + frame.closure.fn.regCount;\n for (var i = frame._base ; i < hi; i++)\n this._regs[i] = undefined;\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)\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 // Closures\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, hasRest, [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 var hasRest = this._operand(); // 1 if last param is a rest element\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 hasRest: hasRest,\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 if (c.fn.hasRest) {\n var restSlot = c.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n sub._regs[i] = i < args.length ? args[i] : undefined;\n sub._regs[restSlot] = args.slice(restSlot);\n } else {\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 // Collections\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 // Object methods (getters / setters)\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 // Self-modifying bytecode\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: allows VM to jump based on runtime values.\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\n // Walk from the current frame upward until we find a frame that has an open 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; // if there's no handler, propagate back 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 */ // <- This comment can't be removed!\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
26
  export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
23
27
  export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
24
28
 
@@ -149,21 +153,26 @@ export const OP_ORIGINAL = {
149
153
  // destPc, sliceStart, sliceEnd
150
154
 
151
155
  // ── Debug ─────────────────────────────────────────────────────────────────
152
- DEBUGGER: 57
156
+ DEBUGGER: 57,
157
+ // ── Indirect jump (register-addressed) ───────────────────────────────────
158
+ // Used by Dispatcher pass. The target PC is read from a register
159
+ // rather than encoded as a bytecode immediate, so static analysis cannot
160
+ // determine the destination without tracking register values at runtime.
161
+ JUMP_REG: 58 // src — frame._pc = regs[src]
153
162
  };
154
163
 
155
164
  // ── Scope ─────────────────────────────────────────────────────────────────────
156
- // Maps variable names to register indices (slot numbers).
157
- // Locals are allocated at compile time; zero name lookups at runtime.
165
+ // Maps variable names to virtual RegisterOperands.
166
+ // Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
167
+ // resolveRegisters() assigns concrete slot indices before serialization.
158
168
  class Scope {
159
169
  constructor(parent = null) {
160
170
  this.parent = parent;
161
171
  this._locals = new Map();
162
- this._next = 0;
163
172
  }
164
- define(name) {
173
+ define(name, ctx) {
165
174
  if (!this._locals.has(name)) {
166
- this._locals.set(name, this._next++);
175
+ this._locals.set(name, ctx._newReg());
167
176
  }
168
177
  return this._locals.get(name);
169
178
  }
@@ -171,7 +180,7 @@ class Scope {
171
180
  if (this._locals.has(name)) {
172
181
  return {
173
182
  kind: "local",
174
- slot: this._locals.get(name)
183
+ reg: this._locals.get(name)
175
184
  };
176
185
  }
177
186
  if (this.parent) return this.parent.resolve(name);
@@ -179,38 +188,68 @@ class Scope {
179
188
  kind: "global"
180
189
  };
181
190
  }
182
- get localCount() {
183
- return this._next;
184
- }
185
191
  }
186
192
 
187
193
  // ── FnContext ─────────────────────────────────────────────────────────────────
188
194
  // Compiler-side state for the function currently being compiled.
189
195
  // Distinct from the runtime Frame — this is compile-time only.
196
+ //
197
+ // Virtual-register model (Lua/LLVM style):
198
+ // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
199
+ // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
200
+ // liveness-aware slot assignment and sets desc.regCount at the end of the
201
+ // pipeline, just like resolveLabels() fills in jump targets.
190
202
  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) {
203
+ // index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
204
+
205
+ // Unique ID for this function — matches the index in compiler.fnDescriptors.
206
+
207
+ // Monotonically increasing counter; each call to _newReg() bumps it.
208
+ _nextId = 0;
209
+ constructor(compiler, parentCtx = null, fnId = 0) {
196
210
  this.compiler = compiler;
197
211
  this.parentCtx = parentCtx;
198
212
  this.scope = new Scope();
199
213
  this.bc = [];
200
214
  this.upvalues = [];
215
+ this._fnId = fnId;
201
216
  }
202
217
 
203
- /** Allocate the next free temp register and return its index. */
218
+ /** Create a new virtual register owned by this function. */
219
+ _newReg() {
220
+ return b.registerOperand(this._nextId++, this._fnId);
221
+ }
222
+
223
+ /**
224
+ * Allocate a short-lived temporary register (pool "temp::").
225
+ * resolveRegisters() will reuse its concrete slot once its live range ends.
226
+ * Do NOT use for named locals or upvalue-captured variables — use _newReg()
227
+ * via scope.define() for those, so they stay in the stable "local::" pool.
228
+ */
204
229
  allocReg() {
205
- const r = this.regTop++;
206
- if (this.regTop > this.maxRegTop) this.maxRegTop = this.regTop;
207
- return r;
230
+ return b.registerOperand(this._nextId++, this._fnId, {
231
+ kind: "temp"
232
+ });
208
233
  }
209
234
 
210
- /** Release all temps above the local region. Called at each statement boundary. */
211
- resetTemps() {
212
- this.regTop = this.scope.localCount;
235
+ /**
236
+ * Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
237
+ *
238
+ * NOTE: This is extraneous for any programmatically generated IR.
239
+ * resolveRegisters() already computes lastUse as the last instruction index
240
+ * where the register appears as a real operand — which is always the tightest
241
+ * correct bound when you stop emitting a register after its last logical use.
242
+ * freeReg is only needed in the rare case where a register has a late syntactic
243
+ * appearance that does NOT represent its true logical death (e.g. a dummy read
244
+ * emitted for side-effects long after the value is logically dead). No current
245
+ * pass in this codebase uses it; it is kept as an extension point only.
246
+ */
247
+ freeReg(bc, reg) {
248
+ bc.push([null, b.freeRegOperand(reg)]);
213
249
  }
250
+
251
+ /** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
252
+ resetTemps() {}
214
253
  addUpvalue(name, isLocal, index) {
215
254
  const existing = this.upvalues.findIndex(u => u.name === name);
216
255
  if (existing !== -1) return existing;
@@ -225,11 +264,20 @@ class FnContext {
225
264
  }
226
265
  // ── Compiler ──────────────────────────────────────────────────────────────────
227
266
  export class Compiler {
228
- /** Internal variable slot registry.
229
- * globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
230
- * opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
231
-
267
+ log(...messages) {
268
+ if (this.options.verbose) {
269
+ console.log(...messages);
270
+ }
271
+ }
272
+ _cloneRegisterOperand(operand) {
273
+ if (!operand || typeof operand !== "object") return operand;
274
+ if (operand.type !== "register") return operand;
275
+ return JSON.parse(JSON.stringify(operand));
276
+ }
232
277
  emit(bc, instr, node) {
278
+ for (let i = 1; i < instr.length; i++) {
279
+ instr[i] = this._cloneRegisterOperand(instr[i]);
280
+ }
233
281
  bc.push(instr);
234
282
  instr[SOURCE_NODE_SYM] = node;
235
283
  }
@@ -249,10 +297,6 @@ export class Compiler {
249
297
  this.MICRO_OPS = {};
250
298
  this.SPECIALIZED_OPS = {};
251
299
  this.ALIASED_OPS = {};
252
- this._internals = {
253
- globally: new Map(),
254
- opcodes: new Map()
255
- };
256
300
  this.OP = {
257
301
  ...OP_ORIGINAL
258
302
  };
@@ -280,7 +324,7 @@ export class Compiler {
280
324
  if (ctx.scope._locals.has(name)) {
281
325
  return {
282
326
  kind: "local",
283
- slot: ctx.scope._locals.get(name)
327
+ reg: ctx.scope._locals.get(name)
284
328
  };
285
329
  }
286
330
  if (!ctx.parentCtx) return {
@@ -291,7 +335,7 @@ export class Compiler {
291
335
  kind: "global"
292
336
  };
293
337
  const isLocal = parentResult.kind === "local";
294
- const index = isLocal ? parentResult.slot : parentResult.index;
338
+ const index = isLocal ? parentResult.reg : parentResult.index;
295
339
  const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
296
340
  return {
297
341
  kind: "upvalue",
@@ -300,31 +344,30 @@ export class Compiler {
300
344
  }
301
345
 
302
346
  // ── 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) {
347
+ // Pre-scan a statement list and reserve virtual registers for every var
348
+ // declaration, function declaration, for-in iterator, and try-catch binding.
349
+ // Must be called before any emit so that locals are allocated before temps.
350
+ _hoistVars(stmts, scope, ctx) {
308
351
  for (const stmt of stmts) {
309
352
  switch (stmt.type) {
310
353
  case "VariableDeclaration":
311
354
  for (const decl of stmt.declarations) {
312
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
355
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
313
356
  }
314
357
  break;
315
358
  case "FunctionDeclaration":
316
- if (stmt.id) scope.define(stmt.id.name);
359
+ if (stmt.id) scope.define(stmt.id.name, ctx);
317
360
  break;
318
361
  case "BlockStatement":
319
- this._hoistVars(stmt.body, scope);
362
+ this._hoistVars(stmt.body, scope, ctx);
320
363
  break;
321
364
  case "IfStatement":
322
365
  {
323
366
  const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
324
- this._hoistVars(cons, scope);
367
+ this._hoistVars(cons, scope, ctx);
325
368
  if (stmt.alternate) {
326
369
  const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
327
- this._hoistVars(alt, scope);
370
+ this._hoistVars(alt, scope, ctx);
328
371
  }
329
372
  break;
330
373
  }
@@ -332,65 +375,73 @@ export class Compiler {
332
375
  case "DoWhileStatement":
333
376
  {
334
377
  const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
335
- this._hoistVars(body, scope);
378
+ this._hoistVars(body, scope, ctx);
336
379
  break;
337
380
  }
338
381
  case "ForStatement":
339
382
  {
340
383
  if (stmt.init?.type === "VariableDeclaration") {
341
384
  for (const decl of stmt.init.declarations) {
342
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
385
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
343
386
  }
344
387
  }
345
388
  const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
346
- this._hoistVars(body, scope);
389
+ this._hoistVars(body, scope, ctx);
347
390
  break;
348
391
  }
349
392
  case "ForInStatement":
350
393
  {
351
- // Reserve a hidden register for the iterator object.
352
- stmt._iterSlot = scope._next++;
394
+ // Reserve a hidden virtual register for the iterator object.
395
+ stmt._iterSlot = ctx._newReg();
353
396
  if (stmt.left.type === "VariableDeclaration") {
354
397
  for (const decl of stmt.left.declarations) {
355
- if (decl.id.type === "Identifier") scope.define(decl.id.name);
398
+ if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
356
399
  }
357
400
  }
358
401
  const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
359
- this._hoistVars(body, scope);
402
+ this._hoistVars(body, scope, ctx);
360
403
  break;
361
404
  }
362
405
  case "SwitchStatement":
363
- for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
406
+ for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
364
407
  break;
365
408
  case "TryStatement":
366
- this._hoistVars(stmt.block.body, scope);
409
+ this._hoistVars(stmt.block.body, scope, ctx);
367
410
  if (stmt.handler) {
368
411
  if (stmt.handler.param?.type === "Identifier") {
369
412
  // Catch parameter IS the exception register.
370
- scope.define(stmt.handler.param.name);
413
+ scope.define(stmt.handler.param.name, ctx);
371
414
  } else {
372
- // No catch binding – reserve a dummy slot for the exception value.
373
- stmt._exceptionSlot = scope._next++;
415
+ // No catch binding – reserve a dummy virtual register for the exception value.
416
+ stmt._exceptionSlot = ctx._newReg();
374
417
  }
375
- this._hoistVars(stmt.handler.body.body, scope);
418
+ this._hoistVars(stmt.handler.body.body, scope, ctx);
376
419
  }
377
420
  break;
378
421
  case "LabeledStatement":
379
- this._hoistVars([stmt.body], scope);
422
+ this._hoistVars([stmt.body], scope, ctx);
380
423
  break;
381
424
  }
382
425
  }
383
426
  }
427
+ profileData = {
428
+ transforms: {}
429
+ };
384
430
 
385
431
  // ── Entry point ───────────────────────────────────────────────────────────
386
432
  compile(source) {
433
+ let startedAt = now();
387
434
  const ast = parse(source, {
388
- sourceType: "script"
435
+ sourceType: "script",
436
+ allowReturnOutsideFunction: true
389
437
  });
438
+ this.profileData.parseTime = now() - startedAt;
390
439
  return this.compileAST(ast);
391
440
  }
392
441
  compileAST(ast) {
442
+ let startedAt = now();
393
443
  this._compileMain(ast.program.body);
444
+ this.profileData.compileTime = now() - startedAt;
394
445
  return this.bytecode;
395
446
  }
396
447
 
@@ -402,28 +453,31 @@ export class Compiler {
402
453
  const entryLabel = this._makeLabel(`fn_${fnIdx}`);
403
454
  var desc = {};
404
455
  this.fnDescriptors.push(desc);
405
- const ctx = new FnContext(this, this._currentCtx);
456
+ const ctx = new FnContext(this, this._currentCtx, fnIdx);
406
457
  const savedCtx = this._currentCtx;
407
458
  this._currentCtx = ctx;
408
459
  const savedLoopStack = this._loopStack;
409
460
  this._loopStack = [];
410
461
 
411
- // 1. Define parameters (occupy regs 0..paramCount-1).
462
+ // 1. Define parameters as virtual registers (occupy the first IDs in order).
463
+ let hasRest = false;
412
464
  for (const param of node.params) {
413
- let identifier = param.type === "AssignmentPattern" ? param.left : param;
414
- ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
415
- ctx.scope.define(identifier.name);
465
+ if (param.type === "RestElement") {
466
+ ok(param.argument.type === "Identifier", "Rest element must be a simple identifier");
467
+ hasRest = true;
468
+ ctx.scope.define(param.argument.name, ctx);
469
+ } else {
470
+ let identifier = param.type === "AssignmentPattern" ? param.left : param;
471
+ ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
472
+ ctx.scope.define(identifier.name, ctx);
473
+ }
416
474
  }
417
475
 
418
- // 2. Reserve the `arguments` slot (reg index = paramCount).
419
- ctx.scope.define("arguments");
476
+ // 2. Reserve the `arguments` virtual register (immediately after params).
477
+ ctx.scope.define("arguments", ctx);
420
478
 
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;
479
+ // 3. Hoist all var declarations so locals are allocated before any temps.
480
+ this._hoistVars(node.body.body, ctx.scope, ctx);
427
481
 
428
482
  // 5. Emit default-value guards.
429
483
  for (const param of node.params) {
@@ -469,25 +523,32 @@ export class Compiler {
469
523
  desc.bytecode = ctx.bc;
470
524
  desc._fnIdx = fnIdx;
471
525
  desc.paramCount = node.params.length;
472
- desc.regCount = ctx.maxRegTop; // total registers needed at runtime
526
+ desc.hasRest = hasRest;
527
+ // regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
473
528
  desc.upvalues = ctx.upvalues.slice();
529
+ desc.ctx = ctx;
474
530
  return desc;
475
531
  }
476
532
 
477
533
  // Emit MAKE_CLOSURE with all metadata as inline operands.
478
534
  // Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
535
+ // regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
479
536
  _emitMakeClosure(desc, node, bc) {
480
537
  const ctx = this._currentCtx;
481
538
  const dst = ctx.allocReg();
482
539
  const uvOperands = [];
483
540
  for (const uv of desc.upvalues) {
484
541
  uvOperands.push(uv.isLocal ? 1 : 0);
485
- uvOperands.push(uv.index);
542
+ uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
486
543
  }
487
544
  this.emit(bc, [this.OP.MAKE_CLOSURE, dst, {
488
545
  type: "label",
489
546
  label: desc.entryLabel
490
- }, desc.paramCount, desc.regCount, desc.upvalues.length, ...uvOperands], node);
547
+ }, desc.paramCount, b.fnRegCountOperand(desc._fnIdx),
548
+ // resolved by resolveRegisters()
549
+ desc.upvalues.length, desc.hasRest ? 1 : 0,
550
+ // 1 = last param is a rest element
551
+ ...uvOperands], node);
491
552
  return dst;
492
553
  }
493
554
 
@@ -501,7 +562,7 @@ export class Compiler {
501
562
  async: false,
502
563
  generator: false,
503
564
  params: [],
504
- id: null,
565
+ id: t.identifier("main"),
505
566
  body: t.blockStatement([...body])
506
567
  });
507
568
  for (const descriptor of this.fnDescriptors) {
@@ -513,7 +574,8 @@ export class Compiler {
513
574
  this.bytecode.push(instr);
514
575
  }
515
576
  }
516
- this.mainRegCount = mainCtx.maxRegTop;
577
+
578
+ // mainRegCount is set by resolveRegisters() after the pipeline runs.
517
579
  this.mainFn = desc;
518
580
  this._currentCtx = savedCtx;
519
581
  }
@@ -592,14 +654,8 @@ export class Compiler {
592
654
  this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
593
655
  }
594
656
  } 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);
657
+ // No initializer: var x; → load undefined directly into the local's register.
658
+ this.emit(bc, [this.OP.LOAD_CONST, slot, b.constantOperand(undefined)], node);
603
659
  }
604
660
  } else {
605
661
  if (decl.init) {
@@ -617,14 +673,11 @@ export class Compiler {
617
673
  case "IfStatement":
618
674
  {
619
675
  const elseOrEndLabel = this._makeLabel("if_else");
620
- const savedTop = ctx.regTop;
621
676
  const testReg = this._compileExpr(node.test, scope, bc);
622
677
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
623
678
  type: "label",
624
679
  label: elseOrEndLabel
625
680
  }], node);
626
- ctx.regTop = savedTop; // free test temps
627
-
628
681
  const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
629
682
  for (const stmt of consequentBody) {
630
683
  this._compileStatement(stmt, scope, bc);
@@ -671,13 +724,11 @@ export class Compiler {
671
724
  type: "defineLabel",
672
725
  label: loopTopLabel
673
726
  }], node);
674
- const savedTop = ctx.regTop;
675
727
  const testReg = this._compileExpr(node.test, scope, bc);
676
728
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
677
729
  type: "label",
678
730
  label: exitLabel
679
731
  }], node);
680
- ctx.regTop = savedTop;
681
732
  const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
682
733
  for (const stmt of whileBody) {
683
734
  this._compileStatement(stmt, scope, bc);
@@ -718,13 +769,11 @@ export class Compiler {
718
769
  type: "defineLabel",
719
770
  label: continueLabel
720
771
  }], node);
721
- const savedTop = ctx.regTop;
722
772
  const testReg = this._compileExpr(node.test, scope, bc);
723
773
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
724
774
  type: "label",
725
775
  label: exitLabel
726
776
  }], node);
727
- ctx.regTop = savedTop;
728
777
  this.emit(bc, [this.OP.JUMP, {
729
778
  type: "label",
730
779
  label: loopTopLabel
@@ -762,13 +811,11 @@ export class Compiler {
762
811
  label: loopTopLabel
763
812
  }], node);
764
813
  if (node.test) {
765
- const savedTop = ctx.regTop;
766
814
  const testReg = this._compileExpr(node.test, scope, bc);
767
815
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
768
816
  type: "label",
769
817
  label: exitLabel
770
818
  }], node);
771
- ctx.regTop = savedTop;
772
819
  }
773
820
  const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
774
821
  for (const stmt of forBody) {
@@ -880,7 +927,6 @@ export class Compiler {
880
927
  const cas = cases[i];
881
928
  if (cas.test === null) continue;
882
929
  const nextCheckLabel = this._makeLabel("sw_next");
883
- const savedTop = ctx.regTop;
884
930
  const caseValReg = this._compileExpr(cas.test, scope, bc);
885
931
  const cmpReg = ctx.allocReg();
886
932
  this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
@@ -888,7 +934,6 @@ export class Compiler {
888
934
  type: "label",
889
935
  label: nextCheckLabel
890
936
  }], node);
891
- ctx.regTop = savedTop;
892
937
  this.emit(bc, [this.OP.JUMP, {
893
938
  type: "label",
894
939
  label: caseLabels[i]
@@ -992,7 +1037,7 @@ export class Compiler {
992
1037
  } else if (node.left.type === "Identifier") {
993
1038
  const res = this._resolve(node.left.name, this._currentCtx);
994
1039
  if (res.kind === "local") {
995
- if (keyReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, keyReg], node);
1040
+ if (keyReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
996
1041
  } else if (res.kind === "upvalue") {
997
1042
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
998
1043
  } else {
@@ -1076,12 +1121,38 @@ export class Compiler {
1076
1121
  }
1077
1122
 
1078
1123
  // ── 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),
1124
+ // Returns the virtual RegisterOperand that holds the result.
1125
+ // For local variables: returns their RegisterOperand directly (no instruction emitted).
1126
+ // For all others: allocates a fresh virtual register, emits the instruction(s),
1082
1127
  // and returns the allocated register.
1083
1128
  _compileExpr(node, scope, bc) {
1084
1129
  const ctx = this._currentCtx;
1130
+
1131
+ // Intrinsic for emitting raw bytecode, useful for emitting register address
1132
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_") {
1133
+ const argJSONStrng = node.arguments[0].value;
1134
+ console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
1135
+ const arg = JSON.parse(argJSONStrng);
1136
+ console.log("Parsed bytecode:", arg);
1137
+ const dst = ctx.allocReg();
1138
+ let operand = arg[0];
1139
+ this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
1140
+
1141
+ return dst;
1142
+ }
1143
+
1144
+ // _VM_JUMP_("labelName") — emits JUMP with a label operand.
1145
+ // Used by bytecode transforms (e.g. CFF) via Template to express jumps
1146
+ // to labels that exist in the parent compiler's bytecode stream.
1147
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_JUMP_") {
1148
+ const label = node.arguments[0].value;
1149
+ bc.push([this.OP.JUMP, {
1150
+ type: "label",
1151
+ label
1152
+ }]);
1153
+ // Return a dummy register — caller (ExpressionStatement) discards it.
1154
+ return ctx.allocReg();
1155
+ }
1085
1156
  switch (node.type) {
1086
1157
  case "NumericLiteral":
1087
1158
  case "StringLiteral":
@@ -1100,7 +1171,7 @@ export class Compiler {
1100
1171
  case "Identifier":
1101
1172
  {
1102
1173
  const res = this._resolve(node.name, this._currentCtx);
1103
- if (res.kind === "local") return res.slot; // register IS the local
1174
+ if (res.kind === "local") return res.reg; // register IS the local
1104
1175
  if (res.kind === "upvalue") {
1105
1176
  const dst = ctx.allocReg();
1106
1177
  this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
@@ -1129,9 +1200,7 @@ export class Compiler {
1129
1200
  {
1130
1201
  const exprs = node.expressions;
1131
1202
  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
1203
+ this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
1135
1204
  }
1136
1205
  return this._compileExpr(exprs[exprs.length - 1], scope, bc);
1137
1206
  }
@@ -1140,17 +1209,13 @@ export class Compiler {
1140
1209
  const n = node;
1141
1210
  const elseLabel = this._makeLabel("ternary_else");
1142
1211
  const endLabel = this._makeLabel("ternary_end");
1143
-
1144
- // Compile test; free its temps after the jump is emitted.
1145
- const baseTop = ctx.regTop;
1146
1212
  const testReg = this._compileExpr(n.test, scope, bc);
1147
1213
  this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
1148
1214
  type: "label",
1149
1215
  label: elseLabel
1150
1216
  }], node);
1151
- ctx.regTop = baseTop; // free test temps
1152
1217
 
1153
- // Reserve reg_result at the base of the temp space.
1218
+ // reg_result is a stable virtual register both branches write into.
1154
1219
  const reg_result = ctx.allocReg();
1155
1220
 
1156
1221
  // Consequent branch.
@@ -1161,22 +1226,18 @@ export class Compiler {
1161
1226
  label: endLabel
1162
1227
  }], node);
1163
1228
 
1164
- // Alternate branch: reset to baseTop then re-reserve reg_result.
1229
+ // Alternate branch each allocReg() gets a unique virtual ID so no
1230
+ // slot collision is possible; no need to "re-occupy" reg_result.
1165
1231
  this.emit(bc, [null, {
1166
1232
  type: "defineLabel",
1167
1233
  label: elseLabel
1168
1234
  }], node);
1169
- ctx.regTop = baseTop;
1170
- ctx.allocReg(); // re-occupy reg_result slot
1171
1235
  const altReg = this._compileExpr(n.alternate, scope, bc);
1172
1236
  if (altReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
1173
1237
  this.emit(bc, [null, {
1174
1238
  type: "defineLabel",
1175
1239
  label: endLabel
1176
1240
  }], node);
1177
-
1178
- // Leave reg_result allocated above baseTop.
1179
- ctx.regTop = baseTop + 1;
1180
1241
  return reg_result;
1181
1242
  }
1182
1243
  case "LogicalExpression":
@@ -1185,9 +1246,7 @@ export class Compiler {
1185
1246
  const endLabel = this._makeLabel("logical_end");
1186
1247
  const isOr = n.operator === "||";
1187
1248
  if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
1188
- const baseTop = ctx.regTop;
1189
1249
  const lhsReg = this._compileExpr(n.left, scope, bc);
1190
- ctx.regTop = baseTop;
1191
1250
  const reg_result = ctx.allocReg();
1192
1251
  if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
1193
1252
 
@@ -1199,15 +1258,12 @@ export class Compiler {
1199
1258
  }], node);
1200
1259
 
1201
1260
  // Compile RHS into reg_result.
1202
- ctx.regTop = baseTop;
1203
- ctx.allocReg(); // re-occupy reg_result
1204
1261
  const rhsReg = this._compileExpr(n.right, scope, bc);
1205
1262
  if (rhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
1206
1263
  this.emit(bc, [null, {
1207
1264
  type: "defineLabel",
1208
1265
  label: endLabel
1209
1266
  }], node);
1210
- ctx.regTop = baseTop + 1;
1211
1267
  return reg_result;
1212
1268
  }
1213
1269
  case "TemplateLiteral":
@@ -1269,7 +1325,8 @@ export class Compiler {
1269
1325
 
1270
1326
  // Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
1271
1327
  const applyBump = curReg => {
1272
- const postfixReg = n.prefix ? -1 : (() => {
1328
+ const postfixReg = n.prefix ? curReg // prefix: postfix copy unused; caller returns newReg instead
1329
+ : (() => {
1273
1330
  const r = ctx.allocReg();
1274
1331
  this.emit(bc, [this.OP.MOVE, r, curReg], node);
1275
1332
  return r;
@@ -1301,7 +1358,7 @@ export class Compiler {
1301
1358
  const res = this._resolve(name, this._currentCtx);
1302
1359
  let curReg;
1303
1360
  if (res.kind === "local") {
1304
- curReg = res.slot;
1361
+ curReg = res.reg;
1305
1362
  } else if (res.kind === "upvalue") {
1306
1363
  curReg = ctx.allocReg();
1307
1364
  this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
@@ -1311,7 +1368,7 @@ export class Compiler {
1311
1368
  }
1312
1369
  const [postfixReg, newReg] = applyBump(curReg);
1313
1370
  if (res.kind === "local") {
1314
- this.emit(bc, [this.OP.MOVE, res.slot, newReg], node);
1371
+ this.emit(bc, [this.OP.MOVE, res.reg, newReg], node);
1315
1372
  } else if (res.kind === "upvalue") {
1316
1373
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, newReg], node);
1317
1374
  } else {
@@ -1369,7 +1426,7 @@ export class Compiler {
1369
1426
  // Load current value of the variable.
1370
1427
  let curReg;
1371
1428
  if (res.kind === "local") {
1372
- curReg = res.slot;
1429
+ curReg = res.reg;
1373
1430
  } else if (res.kind === "upvalue") {
1374
1431
  curReg = ctx.allocReg();
1375
1432
  this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
@@ -1386,8 +1443,8 @@ export class Compiler {
1386
1443
 
1387
1444
  // Store result and return it.
1388
1445
  if (res.kind === "local") {
1389
- if (rhsReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, rhsReg], node);
1390
- return res.slot;
1446
+ if (rhsReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
1447
+ return res.reg;
1391
1448
  } else if (res.kind === "upvalue") {
1392
1449
  this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
1393
1450
  return rhsReg;
@@ -1608,6 +1665,7 @@ class Serializer {
1608
1665
  const v = constants[idx];
1609
1666
  if (!key) return v;
1610
1667
  if (typeof v === "number") return v ^ key;
1668
+ if (typeof v !== "string") return v;
1611
1669
  // String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
1612
1670
  const bytes = Buffer.from(v, "base64");
1613
1671
  let out = "";
@@ -1617,16 +1675,25 @@ class Serializer {
1617
1675
  }
1618
1676
  return out;
1619
1677
  }
1620
- _serializeInstr(instr) {
1678
+ _generateComment(instr) {
1621
1679
  const op = instr[0];
1622
1680
  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}`);
1681
+ if (op === null && operands[0]?.type === "defineLabel") {
1682
+ const label = operands[0].label;
1683
+ return `${label}:`;
1628
1684
  }
1629
- ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
1685
+ const constants = this.compiler.constants;
1686
+ const emittedOperands = operands.filter(operand => operand?.placeholder !== true);
1687
+ const resolvedOperands = emittedOperands.map(o => o?.resolvedValue ?? o);
1688
+ const displayOperands = operands.map((o, i) => {
1689
+ const resolvedValue = resolvedOperands[i];
1690
+ const label = o?.label;
1691
+ let displayOperand = resolvedValue;
1692
+ if (label) {
1693
+ return label;
1694
+ }
1695
+ return displayOperand;
1696
+ });
1630
1697
  let name = this.OP_NAME[op];
1631
1698
  if (!name || name.includes("{")) {
1632
1699
  name = `OP_${op}`;
@@ -1637,73 +1704,79 @@ class Serializer {
1637
1704
  }
1638
1705
  const sourceNode = instr[SOURCE_NODE_SYM];
1639
1706
  const sourceLocation = sourceNode?.loc ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)].filter(Boolean).join("-") : "";
1640
- if (resolvedOperands.length > 0) {
1707
+ if (displayOperands.length > 0) {
1641
1708
  // Operand[0] is always `dst` for instruction types that produce a value.
1642
- const dst = resolvedOperands[0];
1709
+ const dst = displayOperands[0];
1643
1710
  switch (op) {
1644
1711
  case this.OP.LOAD_CONST:
1645
1712
  {
1646
1713
  // resolvedOperands: [dst, constIdx, concealKey]
1647
- const val = this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2]);
1714
+ const val = this._decryptConst(constants, displayOperands[1], displayOperands[2]);
1648
1715
  comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
1649
1716
  break;
1650
1717
  }
1718
+ case this.OP.LOAD_INT:
1719
+ {
1720
+ // resolvedOperands: [dst, intValue]
1721
+ comment += ` reg[${dst}] = ${displayOperands[1]}`;
1722
+ break;
1723
+ }
1651
1724
  case this.OP.LOAD_GLOBAL:
1652
1725
  // resolvedOperands: [dst, constIdx, concealKey]
1653
- comment += ` reg[${dst}] = ${this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2])}`;
1726
+ comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
1654
1727
  break;
1655
1728
  case this.OP.STORE_GLOBAL:
1656
1729
  // resolvedOperands: [constIdx, concealKey, srcReg]
1657
- comment += ` ${this._decryptConst(constants, resolvedOperands[0], resolvedOperands[1])} = reg[${resolvedOperands[2]}]`;
1730
+ comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
1658
1731
  break;
1659
1732
  case this.OP.LOAD_UPVALUE:
1660
- comment += ` reg[${dst}] = upvalue[${resolvedOperands[1]}]`;
1733
+ comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
1661
1734
  break;
1662
1735
  case this.OP.STORE_UPVALUE:
1663
- comment += ` upvalue[${resolvedOperands[0]}] = reg[${resolvedOperands[1]}]`;
1736
+ comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
1664
1737
  break;
1665
1738
  case this.OP.MOVE:
1666
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}]`;
1739
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
1667
1740
  break;
1668
1741
  case this.OP.MAKE_CLOSURE:
1669
- comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
1742
+ comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
1670
1743
  break;
1671
1744
  case this.OP.CALL:
1672
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
1745
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands.slice(3).map(v => `reg[${v}]`).join(", ")})`;
1673
1746
  break;
1674
1747
  case this.OP.CALL_METHOD:
1675
- comment += ` reg[${dst}] = reg[${resolvedOperands[2]}](recv=reg[${resolvedOperands[1]}], ${resolvedOperands[3]} args)`;
1748
+ comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
1676
1749
  break;
1677
1750
  case this.OP.NEW:
1678
- comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
1751
+ comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
1679
1752
  break;
1680
1753
  case this.OP.RETURN:
1681
- comment += ` reg[${resolvedOperands[0]}]`;
1754
+ comment += ` reg[${displayOperands[0]}]`;
1682
1755
  break;
1683
1756
  case this.OP.BUILD_ARRAY:
1684
- comment += ` reg[${dst}] = [${resolvedOperands[2]} elems]`;
1757
+ comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
1685
1758
  break;
1686
1759
  case this.OP.BUILD_OBJECT:
1687
- comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
1760
+ comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
1688
1761
  break;
1689
1762
  case this.OP.GET_PROP:
1690
- comment += ` reg[${dst}] = reg[${resolvedOperands[1]}][reg[${resolvedOperands[2]}]]`;
1763
+ comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
1691
1764
  break;
1692
1765
  case this.OP.SET_PROP:
1693
- comment += ` reg[${resolvedOperands[0]}][reg[${resolvedOperands[1]}]] = reg[${resolvedOperands[2]}]`;
1766
+ comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
1767
+ break;
1768
+ case this.OP.JUMP_REG:
1769
+ comment += ` PC = reg[${displayOperands[0]}]`;
1694
1770
  break;
1695
1771
  default:
1696
- comment += resolvedOperands.length === 1 ? ` ${resolvedOperands[0]}` : ` [${resolvedOperands.join(", ")}]`;
1772
+ comment += displayOperands.length === 1 ? ` ${displayOperands[0]}` : ` [${displayOperands.join(", ")}]`;
1697
1773
  }
1698
1774
  }
1699
1775
  comment = comment.padEnd(50) + sourceLocation;
1700
1776
  const values = [op, ...resolvedOperands];
1701
1777
  const instrText = `[${values.join(", ")}]`;
1702
1778
  const text = `${(instrText + ",").padEnd(20)} ${comment}`;
1703
- return {
1704
- text,
1705
- values
1706
- };
1779
+ return text;
1707
1780
  }
1708
1781
  _serializeConstants(constants) {
1709
1782
  const lines = ["var CONSTANTS = ["];
@@ -1716,14 +1789,23 @@ class Serializer {
1716
1789
  _serializeBytecode(bytecode, compiler) {
1717
1790
  const serialized = [];
1718
1791
  for (const instr of bytecode) {
1719
- if (instr[0] === null) continue;
1792
+ const op = instr[0];
1793
+ const operands = instr.slice(1);
1794
+ if (instr[0] === null) continue; // null opcodes are not emitted
1795
+
1796
+ const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
1720
1797
  const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
1721
1798
  if (specializedOpInfo) {
1722
- const operands = instr.slice(1);
1723
- const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
1724
1799
  const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
1725
1800
  compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValues.join("_")}`;
1726
1801
  }
1802
+
1803
+ // Validate no opcode or operand exceeds u16 limit
1804
+ for (const o of resolvedValues) {
1805
+ ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
1806
+ ok(o >= 0 && o <= 0xffffffff, `Operand overflow (max 0xFFFFFFFF u32): ${o}`);
1807
+ }
1808
+ ok(op >= 0 && op <= 0xffffffff, `Opcode overflow (max 0xFFFFFFFF u32): ${op}`);
1727
1809
  serialized.push(instr);
1728
1810
  }
1729
1811
  return {
@@ -1731,10 +1813,12 @@ class Serializer {
1731
1813
  };
1732
1814
  }
1733
1815
  _encodeBytecode(flat) {
1734
- const buf = new Uint8Array(flat.length * 2);
1816
+ const buf = new Uint8Array(flat.length * 4);
1735
1817
  flat.forEach((w, i) => {
1736
- buf[i * 2] = w & 0xff;
1737
- buf[i * 2 + 1] = w >>> 8 & 0xff;
1818
+ buf[i * 4] = w & 0xff;
1819
+ buf[i * 4 + 1] = w >>> 8 & 0xff;
1820
+ buf[i * 4 + 2] = w >>> 16 & 0xff;
1821
+ buf[i * 4 + 3] = w >>> 24 & 0xff;
1738
1822
  });
1739
1823
  return Buffer.from(buf).toString("base64");
1740
1824
  }
@@ -1767,58 +1851,121 @@ class Serializer {
1767
1851
  }
1768
1852
  }
1769
1853
  export async function compileAndSerialize(sourceCode, options) {
1854
+ let obfuscationStartedAt = now();
1770
1855
  const compiler = new Compiler(options);
1771
1856
  let bytecode = compiler.compile(sourceCode);
1772
1857
  const passes = [];
1773
- passes.push(concealConstants);
1774
- if (options.specializedOpcodes) {
1775
- passes.push(specializedOpcodes);
1858
+ if (options.stringConcealing) {
1859
+ passes.push({
1860
+ pass: stringConcealing,
1861
+ name: "stringConcealing"
1862
+ });
1776
1863
  }
1777
- if (options.microOpcodes) {
1778
- passes.push(microOpcodes);
1864
+
1865
+ // CFF and Dispatcher both run before resolveRegisters and resolveLabels
1866
+ if (options.controlFlowFlattening) {
1867
+ passes.push({
1868
+ pass: controlFlowFlattening,
1869
+ name: "controlFlowFlattening"
1870
+ });
1779
1871
  }
1780
- if (options.macroOpcodes) {
1781
- passes.push(macroOpcodes);
1872
+ if (options.dispatcher) {
1873
+ passes.push({
1874
+ pass: dispatcher,
1875
+ name: "dispatcher"
1876
+ });
1782
1877
  }
1783
- if (options.selfModifying) {
1784
- passes.push(selfModifying);
1878
+ passes.push({
1879
+ pass: concealConstants,
1880
+ name: "concealConstants"
1881
+ });
1882
+ if (options.specializedOpcodes) {
1883
+ passes.push({
1884
+ pass: specializedOpcodes,
1885
+ name: "specializedOpcodes"
1886
+ });
1887
+ }
1888
+ if (options.macroOpcodes) {
1889
+ passes.push({
1890
+ pass: macroOpcodes,
1891
+ name: "macroOpcodes"
1892
+ });
1785
1893
  }
1786
1894
  if (options.aliasedOpcodes) {
1787
- passes.push(aliasedOpcodes);
1895
+ passes.push({
1896
+ pass: aliasedOpcodes,
1897
+ name: "aliasedOpcodes"
1898
+ });
1788
1899
  }
1789
- for (const pass of passes) {
1900
+ const timings = {};
1901
+ function runAndTime(pass, name) {
1902
+ const startedAt = now();
1903
+ compiler.log(`Running bytecode pass ${name}...`);
1790
1904
  const passResult = pass(bytecode, compiler);
1791
1905
  bytecode = passResult.bytecode;
1906
+ const endedAt = now();
1907
+ const elapsedMs = endedAt - startedAt;
1908
+ timings[name] = elapsedMs;
1909
+ compiler.profileData.transforms[name] = {
1910
+ transformTime: elapsedMs,
1911
+ bytecodeSize: bytecode.length
1912
+ };
1913
+ compiler.log(`Bytecode pass ${name} completed in ${Math.floor(elapsedMs)}ms`);
1914
+ return passResult;
1915
+ }
1916
+ for (const pass of passes) {
1917
+ runAndTime(pass.pass, pass.name);
1918
+ }
1919
+
1920
+ // Resolve virtual registers to concrete slot indices and set regCount per fn.
1921
+ // Must run BEFORE selfModifying: that pass moves body instructions to the end
1922
+ // of the bytecode while leaving RETURN in place, splitting a function's code
1923
+ // into two non-contiguous regions. Linear-scan liveness then sees incorrect
1924
+ // firstUse/lastUse for registers that span the gap, causing slot collisions.
1925
+ const regsResult = runAndTime(resolveRegisters, "resolveRegisters");
1926
+ bytecode = regsResult.bytecode;
1927
+
1928
+ // selfModifying runs after register resolution so concrete slot indices are
1929
+ // already in place; only label operands remain unresolved at this stage.
1930
+ if (options.selfModifying) {
1931
+ const smResult = runAndTime(selfModifying, "selfModifying");
1932
+ bytecode = smResult.bytecode;
1792
1933
  }
1793
1934
 
1794
1935
  // Resolve label references to flat bytecode indices.
1795
- const labelsResult = resolveLabels(bytecode, compiler);
1936
+ const labelsResult = runAndTime(resolveLabels, "resolveLabels");
1796
1937
  bytecode = labelsResult.bytecode;
1797
1938
 
1798
1939
  // Set mainStartPc from the first function descriptor (or 0 for top-level start).
1799
1940
  compiler.mainStartPc = compiler.mainFn.startPc;
1800
1941
 
1801
1942
  // Resolve constant references to pool indices (+ conceal key operand).
1802
- const constResult = resolveConstants(bytecode, compiler);
1943
+ const constResult = runAndTime(resolveConstants, "resolveConstants");
1803
1944
  bytecode = constResult.bytecode;
1804
1945
  compiler.constants = constResult.constants;
1805
1946
 
1806
1947
  // Build and obfuscate the runtime.
1807
1948
  const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
1808
1949
 
1809
- // This part was purposefully pulled out Serializer as OP_NAME's get resolved during obfuscateRuntime
1950
+ // for (const key of Object.keys(timings)) {
1951
+ // console.log(` ${key}: ${timings[key]}ms`);
1952
+ // }
1953
+
1954
+ // This part was purposefully pulled out Serializer as OP_NAME's get resolved during buildRuntime
1810
1955
  // So for the most useful comments, it's ran absolutely last
1811
1956
  // Tests also rely on correct comments so it's required
1812
1957
  const generateBytecodeComment = () => {
1813
1958
  var lines = [];
1814
1959
  for (const instr of bytecode) {
1815
- const serialized = compiler.serializer._serializeInstr(instr);
1816
- lines.push("// " + serialized.text);
1960
+ const comment = compiler.serializer._generateComment(instr);
1961
+ lines.push("// " + comment);
1817
1962
  }
1818
1963
  return lines.join("\n");
1819
1964
  };
1820
- const code = await obfuscateRuntime(runtimeSource, bytecode, options, compiler, generateBytecodeComment);
1965
+ const code = await buildRuntime(runtimeSource, bytecode, options, compiler, generateBytecodeComment);
1966
+ compiler.profileData.obfuscationTime = now() - obfuscationStartedAt;
1821
1967
  return {
1822
- code
1968
+ code,
1969
+ profileData: compiler.profileData
1823
1970
  };
1824
1971
  }