js-confuser-vm 0.0.4 → 0.0.6

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