js-confuser-vm 0.1.1 → 0.1.2

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 (58) hide show
  1. package/README.md +242 -89
  2. package/dist/compiler.js +583 -208
  3. package/dist/disassembler.js +58 -8
  4. package/dist/runtime.js +93 -74
  5. package/dist/template.js +81 -76
  6. package/dist/transforms/bytecode/concealConstants.js +2 -2
  7. package/dist/transforms/bytecode/controlFlowFlattening.js +143 -25
  8. package/dist/transforms/bytecode/dispatcher.js +3 -3
  9. package/dist/transforms/bytecode/resolveRegisters.js +19 -4
  10. package/dist/transforms/bytecode/selfModifying.js +88 -21
  11. package/dist/transforms/bytecode/specializedOpcodes.js +6 -3
  12. package/dist/transforms/bytecode/stringConcealing.js +253 -75
  13. package/dist/utils/ast-utils.js +61 -0
  14. package/dist/utils/op-utils.js +1 -0
  15. package/package.json +7 -1
  16. package/.gitmodules +0 -4
  17. package/.prettierignore +0 -1
  18. package/CHANGELOG.md +0 -358
  19. package/babel-plugin-inline-runtime.cjs +0 -34
  20. package/babel.config.json +0 -23
  21. package/bench.ts +0 -146
  22. package/disassemble.ts +0 -12
  23. package/index.ts +0 -43
  24. package/jest-strip-types.js +0 -10
  25. package/jest.config.js +0 -64
  26. package/output.disassembled.js +0 -41
  27. package/src/build-runtime.ts +0 -113
  28. package/src/compiler.ts +0 -2703
  29. package/src/disassembler.ts +0 -329
  30. package/src/index.ts +0 -24
  31. package/src/minify.ts +0 -21
  32. package/src/options.ts +0 -24
  33. package/src/runtime.ts +0 -956
  34. package/src/template.ts +0 -265
  35. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -151
  36. package/src/transforms/bytecode/concealConstants.ts +0 -52
  37. package/src/transforms/bytecode/controlFlowFlattening.ts +0 -566
  38. package/src/transforms/bytecode/dispatcher.ts +0 -292
  39. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  40. package/src/transforms/bytecode/resolveConstants.ts +0 -126
  41. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  42. package/src/transforms/bytecode/resolveRegisters.ts +0 -226
  43. package/src/transforms/bytecode/selfModifying.ts +0 -121
  44. package/src/transforms/bytecode/specializedOpcodes.ts +0 -164
  45. package/src/transforms/bytecode/stringConcealing.ts +0 -130
  46. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  47. package/src/transforms/runtime/classObfuscation.ts +0 -59
  48. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  49. package/src/transforms/runtime/minify.ts +0 -1
  50. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  51. package/src/transforms/runtime/specializedOpcodes.ts +0 -161
  52. package/src/types.ts +0 -134
  53. package/src/utils/ast-utils.ts +0 -19
  54. package/src/utils/op-utils.ts +0 -46
  55. package/src/utils/pass-utils.ts +0 -126
  56. package/src/utils/profile-utils.ts +0 -3
  57. package/src/utils/random-utils.ts +0 -31
  58. package/tsconfig.json +0 -12
package/dist/compiler.js CHANGED
@@ -15,32 +15,33 @@ import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
15
15
  import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
16
16
  import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
17
17
  import { getRandomInt } from "./utils/random-utils.js";
18
- import { U16_MAX } from "./utils/op-utils.js";
18
+ import { U16_MAX, U32_MAX } from "./utils/op-utils.js";
19
19
  import { concealConstants } from "./transforms/bytecode/concealConstants.js";
20
20
  import { dispatcher } from "./transforms/bytecode/dispatcher.js";
21
21
  import { controlFlowFlattening } from "./transforms/bytecode/controlFlowFlattening.js";
22
22
  import { stringConcealing } from "./transforms/bytecode/stringConcealing.js";
23
23
  import { now } from "./utils/profile-utils.js";
24
+ import { walkHoistScope } from "./utils/ast-utils.js";
24
25
  const traverse = traverseImport.default || traverseImport;
25
- const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction base64ToBytes(s) {\n return typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n}\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b = base64ToBytes(s);\n // Each slot is a u32 stored as 4 little-endian bytes.\n var r = new Uint32Array(b.length / 4);\n for (var i = 0; i < r.length; i++)\n r[i] =\n (b[i * 4] |\n (b[i * 4 + 1] << 8) |\n (b[i * 4 + 2] << 16) |\n (b[i * 4 + 3] << 24)) >>>\n 0;\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue \u2014 Lua/CPython style.\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\n// _absSlot is the absolute index in VM._regs (frame._base + local slot).\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\n// Frame \u2014 analogous to Lua CallInfo / CPython PyFrameObject.\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // \u2500\u2500 Flat register file (Lua-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // All frames share a single array. Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b = base64ToBytes(v);\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // globals[globalName] = regs[src]\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n // regs[dst] = delete regs[obj][regs[key]]\n // The delete operator returns true if successful which is most cases\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // Arithmetic (dst, src1, src2)\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // Comparison (dst, src1, src2)\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n // regs[dst] = regs[obj] instanceof regs[ctor]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var ctor = regs[base + this._operand()];\n if (typeof ctor === \"function\") {\n regs[base + dst] = obj instanceof ctor;\n } else {\n // TODO: Why is this needed?\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n regs[base + dst] = result;\n }\n break;\n }\n\n // Unary (dst, src)\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consumes argument (intended)\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // regs[dst] = typeof window[name]\n // Never throws ReferenceError, instead returns undefined for undeclared variables\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // Control flow\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // Calls\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n // Zero out callee's register window to limit exposing runtime values\n var hi = frame._base + frame.closure.fn.regCount;\n for (var i = frame._base ; i < hi; i++)\n this._regs[i] = undefined;\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // Closures\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n var hasRest = this._operand(); // 1 if last param is a rest element\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n hasRest: hasRest,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n if (c.fn.hasRest) {\n var restSlot = c.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n sub._regs[i] = i < args.length ? args[i] : undefined;\n sub._regs[restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < c.fn.regCount; i++)\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // Collections\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // Object methods (getters / setters)\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // Self-modifying bytecode\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: allows VM to jump based on runtime values.\n frame._pc = regs[base + this._operand()];\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding\n // Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // if there's no handler, propagate back to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n this._regs[handledFrame._base + h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n/* @BOOT */ // <- This comment can't be removed!\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals.window = window;\n for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
26
+ 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;\nconst SENTINELS = { CALL_SPREAD: 0 };\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction base64ToBytes(s) {\n return typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n}\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b = base64ToBytes(s);\n // Each slot is a u32 stored as 4 little-endian bytes.\n var r = new Uint32Array(b.length / 4);\n for (var i = 0; i < r.length; i++)\n r[i] =\n (b[i * 4] |\n (b[i * 4 + 1] << 8) |\n (b[i * 4 + 2] << 16) |\n (b[i * 4 + 3] << 24)) >>>\n 0;\n return r;\n}\n\n// Closure map\n// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls.\n// A WeakMap is used over a Symbol to prevent leaking information to debuggers\nvar CLOSURE_MAP = new WeakMap();\n\n// Upvalue (Lua style)\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\n// Frame (Lua CallInfo / CPython PyFrameObject)\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // Flat register array (Lua-style)\n // Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx: pool index (first operand of the constant pair emitted by resolveConstants).\n// key: conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b = base64ToBytes(v);\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`);\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // globals[globalName] = regs[src]\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n // regs[dst] = delete regs[obj][regs[key]]\n // The delete operator returns true if successful which is most cases\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // Arithmetic (dst, src1, src2)\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.EXP: {\n // Math.pow instead of `**`\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = Math.pow(a, regs[base + this._operand()]);\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // Comparison (dst, src1, src2)\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n // regs[dst] = regs[obj] instanceof regs[ctor]\n // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n regs[base + dst] = obj instanceof regs[base + this._operand()];\n break;\n }\n\n // Unary (dst, src)\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consumes argument (intended)\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // regs[dst] = typeof window[name]\n // Never throws ReferenceError, instead returns undefined for undeclared variables\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // Control flow\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // Calls\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg)\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args;\n if (argc === SENTINELS.CALL_SPREAD) {\n args = regs[base + this._operand()];\n } else {\n args = new Array(argc);\n for (var i = 0; i < argc; i++)\n args[i] = regs[base + this._operand()];\n }\n\n var closure = callee && CLOSURE_MAP.get(callee);\n if (closure) {\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg)\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args;\n if (argc === SENTINELS.CALL_SPREAD) {\n args = regs[base + this._operand()];\n } else {\n args = new Array(argc);\n for (var i = 0; i < argc; i++)\n args[i] = regs[base + this._operand()];\n }\n\n var closure = callee && CLOSURE_MAP.get(callee);\n if (closure) {\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg)\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args;\n if (argc === SENTINELS.CALL_SPREAD) {\n args = regs[base + this._operand()];\n } else {\n args = new Array(argc);\n for (var i = 0; i < argc; i++)\n args[i] = regs[base + this._operand()];\n }\n\n var closure = callee && CLOSURE_MAP.get(callee);\n if (closure) {\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n // Zero out callee's register window to limit exposing runtime values\n var hi = frame._base + frame.closure.fn.regCount;\n for (var i = frame._base ; i < hi; i++)\n this._regs[i] = undefined;\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // Closures\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n var hasRest = this._operand(); // 1 if last param is a rest element\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n hasRest: hasRest,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n if (c.fn.hasRest) {\n var restSlot = c.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n sub._regs[i] = i < args.length ? args[i] : undefined;\n sub._regs[restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < c.fn.regCount; i++)\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n CLOSURE_MAP.set(shell, closure);\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // Collections\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // Object methods (getters / setters)\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the top handler record\n // (works for both catch and finally regions; they share the stack).\n frame._handlerStack.pop();\n break;\n }\n\n case OP.FINALLY_SETUP: {\n // finallyPc, contReg, payloadReg, throwPad\n // Arm a finalizer for the current region. Unlike a catch record this\n // carries no exceptionReg; instead the continuation register (contReg)\n // receives the resume PC and payloadReg carries the in-flight value.\n frame._handlerStack.push({\n finallyPc: this._operand(),\n contReg: this._operand(),\n payloadReg: this._operand(),\n throwPad: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n // Self-modifying bytecode\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: allows VM to jump based on runtime values.\n frame._pc = regs[base + this._operand()];\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding\n // Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // if there's no handler, propagate back to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the protected region.\n this._frameStack.length = h.frameStackDepth;\n var hBase = handledFrame._base;\n if (h.exceptionReg !== undefined) {\n // catch region \u2014 deliver the exception to the catch binding and run it.\n this._regs[hBase + h.exceptionReg] = err;\n handledFrame._pc = h.handlerPc;\n } else {\n // finally region: run the finalizer with the exception pending, then\n // resume at its throw pad (which re-raises and continues unwinding).\n this._regs[hBase + h.contReg] = h.throwPad;\n this._regs[hBase + h.payloadReg] = err;\n handledFrame._pc = h.finallyPc;\n }\n this._regsTop = hBase + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n/* @BOOT */ // <- This comment can't be removed!\nvar globals = globalThis;\nif (typeof window !== \"undefined\") {\n globals.window = window;\n globals.document = typeof document !== \"undefined\" ? document : undefined;\n}\nif (typeof module !== \"undefined\") {\n globals.module = module;\n globals.exports = typeof exports !== \"undefined\" ? exports : undefined;\n}\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
26
27
  export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
27
28
  export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
28
29
 
29
- // ── Opcodes ──────────────────────────────────────────────────────────────────
30
+ // Opcodes
30
31
  // Register-based encoding. Operand convention (x86 / CPython style):
31
- // destination register first, then source registers, then immediates.
32
+ // destination register first, then source registers, then immediates.
32
33
  //
33
- // dst – register index that receives the result
34
- // src – register index holding an input value
35
- // imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
34
+ // dst – register index that receives the result
35
+ // src – register index holding an input value
36
+ // imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
36
37
  //
37
38
  // Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
38
39
  // Every load: [op, dst, ...]
39
40
  // Every store: [op, target, src]
40
41
  // Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
41
- // CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
42
+ // CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
42
43
  export const OP_ORIGINAL = {
43
- // ── Loads ─────────────────────────────────────────────────────────────────
44
+ // Loads
44
45
  LOAD_CONST: 0,
45
46
  // dst, constIdx regs[dst] = constants[constIdx]
46
47
  LOAD_INT: 1,
@@ -54,13 +55,13 @@ export const OP_ORIGINAL = {
54
55
  MOVE: 5,
55
56
  // dst, src regs[dst] = regs[src]
56
57
 
57
- // ── Stores ────────────────────────────────────────────────────────────────
58
+ // Stores
58
59
  STORE_GLOBAL: 6,
59
60
  // nameIdx, src globals[constants[nameIdx]] = regs[src]
60
61
  STORE_UPVALUE: 7,
61
62
  // uvIdx, src upvalues[uvIdx].write(regs[src])
62
63
 
63
- // ── Property access ───────────────────────────────────────────────────────
64
+ // Property access
64
65
  GET_PROP: 8,
65
66
  // dst, obj, key regs[dst] = regs[obj][regs[key]]
66
67
  SET_PROP: 9,
@@ -68,19 +69,21 @@ export const OP_ORIGINAL = {
68
69
  DELETE_PROP: 10,
69
70
  // dst, obj, key regs[dst] = delete regs[obj][regs[key]]
70
71
 
71
- // ── Arithmetic / bitwise (dst, src1, src2) ───────────────────────────────
72
+ // Arithmetic / bitwise (dst, src1, src2)
72
73
  ADD: 11,
73
74
  SUB: 12,
74
75
  MUL: 13,
75
76
  DIV: 14,
76
77
  MOD: 15,
78
+ EXP: 60,
79
+ // dst, src1, src2 regs[dst] = regs[src1] ** regs[src2]
77
80
  BAND: 16,
78
81
  BOR: 17,
79
82
  BXOR: 18,
80
83
  SHL: 19,
81
84
  SHR: 20,
82
85
  USHR: 21,
83
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────────
86
+ // Comparison (dst, src1, src2)
84
87
  LT: 22,
85
88
  GT: 23,
86
89
  LTE: 24,
@@ -91,7 +94,7 @@ export const OP_ORIGINAL = {
91
94
  LOOSE_NEQ: 29,
92
95
  IN: 30,
93
96
  INSTANCEOF: 31,
94
- // ── Unary (dst, src) ─────────────────────────────────────────────────────
97
+ // Unary (dst, src)
95
98
  UNARY_NEG: 32,
96
99
  UNARY_POS: 33,
97
100
  UNARY_NOT: 34,
@@ -103,7 +106,7 @@ export const OP_ORIGINAL = {
103
106
  TYPEOF_SAFE: 38,
104
107
  // dst, nameConstIdx – safe typeof for potentially-undeclared globals
105
108
 
106
- // ── Control flow ──────────────────────────────────────────────────────────
109
+ // Control flow
107
110
  JUMP: 39,
108
111
  // target
109
112
  JUMP_IF_FALSE: 40,
@@ -111,7 +114,7 @@ export const OP_ORIGINAL = {
111
114
  JUMP_IF_TRUE: 41,
112
115
  // src, target if regs[src] then pc = target (|| short-circuit)
113
116
 
114
- // ── Calls & constructors ──────────────────────────────────────────────────
117
+ // Calls & constructors
115
118
  CALL: 42,
116
119
  // dst, callee, argc, [argRegs…]
117
120
  CALL_METHOD: 43,
@@ -123,45 +126,56 @@ export const OP_ORIGINAL = {
123
126
  THROW: 46,
124
127
  // src
125
128
 
126
- // ── Closures ──────────────────────────────────────────────────────────────
129
+ // Closures
127
130
  // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
128
131
  MAKE_CLOSURE: 47,
129
- // ── Collections ───────────────────────────────────────────────────────────
132
+ // Collections
130
133
  BUILD_ARRAY: 48,
131
134
  // dst, count, [elemRegs…]
132
135
  BUILD_OBJECT: 49,
133
136
  // dst, pairCount, [keyReg, valReg, …]
134
137
 
135
- // ── Property definitions (getters / setters) ──────────────────────────────
138
+ // Property definitions (getters / setters)
136
139
  DEFINE_GETTER: 50,
137
140
  // obj, key, fn
138
141
  DEFINE_SETTER: 51,
139
142
  // obj, key, fn
140
143
 
141
- // ── For-in iteration ──────────────────────────────────────────────────────
144
+ // For-in iteration
142
145
  FOR_IN_SETUP: 52,
143
146
  // dst, src dst = { _keys: enumKeys(src), i: 0 }
144
147
  FOR_IN_NEXT: 53,
145
148
  // dst, iter, exitTarget
146
149
 
147
- // ── Exception handling ────────────────────────────────────────────────────
150
+ // Exception handling
148
151
  TRY_SETUP: 54,
149
152
  // handlerPc, exceptionReg
150
153
  TRY_END: 55,
151
- // ── Self-modifying bytecode ───────────────────────────────────────────────
154
+ // Self-modifying bytecode
152
155
  PATCH: 56,
153
156
  // destPc, sliceStart, sliceEnd
154
157
 
155
- // ── Debug ─────────────────────────────────────────────────────────────────
158
+ // Debug
156
159
  DEBUGGER: 57,
157
- // ── Indirect jump (register-addressed) ───────────────────────────────────
160
+ // Indirect jump (register-addressed)
158
161
  // Used by Dispatcher pass. The target PC is read from a register
159
162
  // rather than encoded as a bytecode immediate, so static analysis cannot
160
163
  // determine the destination without tracking register values at runtime.
161
- JUMP_REG: 58 // src — frame._pc = regs[src]
164
+ JUMP_REG: 58,
165
+ // src — frame._pc = regs[src]
166
+
167
+ // Exception handling (finally)
168
+ // Arms a finalizer for the current region. Operands:
169
+ // finallyPc, contReg, payloadReg, throwPad
170
+ // The finalizer runs on every exit path (normal, return, break/continue,
171
+ // throw). contReg holds the PC to resume at once the finalizer completes;
172
+ // the finalizer ends with JUMP_REG contReg. payloadReg carries the pending
173
+ // value (return value or in-flight exception). throwPad is the PC the
174
+ // runtime resumes at when an exception is pending (re-raises after finally).
175
+ FINALLY_SETUP: 59
162
176
  };
163
177
 
164
- // ── Scope ─────────────────────────────────────────────────────────────────────
178
+ // Scope
165
179
  // Maps variable names to virtual RegisterOperands.
166
180
  // Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
167
181
  // resolveRegisters() assigns concrete slot indices before serialization.
@@ -190,15 +204,15 @@ class Scope {
190
204
  }
191
205
  }
192
206
 
193
- // ── FnContext ─────────────────────────────────────────────────────────────────
207
+ // FnContext
194
208
  // Compiler-side state for the function currently being compiled.
195
209
  // Distinct from the runtime Frame — this is compile-time only.
196
210
  //
197
211
  // Virtual-register model (Lua/LLVM style):
198
- // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
199
- // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
200
- // liveness-aware slot assignment and sets desc.regCount at the end of the
201
- // pipeline, just like resolveLabels() fills in jump targets.
212
+ // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
213
+ // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
214
+ // liveness-aware slot assignment and sets desc.regCount at the end of the
215
+ // pipeline, just like resolveLabels() fills in jump targets.
202
216
  class FnContext {
203
217
  // index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
204
218
 
@@ -262,7 +276,7 @@ class FnContext {
262
276
  return idx;
263
277
  }
264
278
  }
265
- // ── Compiler ──────────────────────────────────────────────────────────────────
279
+ // Compiler
266
280
  export class Compiler {
267
281
  log(...messages) {
268
282
  if (this.options.verbose) {
@@ -311,8 +325,16 @@ export class Compiler {
311
325
  this.OP[key] = val;
312
326
  }
313
327
  }
328
+
329
+ // SENTINELS: magic values placed in argc slots to signal special call modes.
330
+ // Default to U16_MAX (safely above any valid arg count).
331
+ // When randomizeOpcodes is on, pick a random value in [U16_MAX, U32_MAX] so
332
+ // each obfuscated output looks different to a static analyser.
333
+ this.SENTINELS = {
334
+ CALL_SPREAD: this.options.randomizeOpcodes ? getRandomInt(U16_MAX, U32_MAX) : U16_MAX
335
+ };
314
336
  this.OP_NAME = Object.fromEntries(Object.entries(this.OP).map(([k, v]) => [v, k]));
315
- 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]);
337
+ 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, this.OP.FINALLY_SETUP]);
316
338
  }
317
339
  _makeLabel(hint = "") {
318
340
  return `${hint || "L"}_${this._labelCount++}`;
@@ -343,12 +365,11 @@ export class Compiler {
343
365
  };
344
366
  }
345
367
 
346
- // ── Variable hoisting ──────────────────────────────────────────────────────
347
368
  // Pre-scan a statement list and reserve virtual registers for every var
348
369
  // declaration, function declaration, for-in iterator, and try-catch binding.
349
370
  // Must be called before any emit so that locals are allocated before temps.
350
371
  _hoistVars(stmts, scope, ctx) {
351
- for (const stmt of stmts) {
372
+ walkHoistScope(stmts, stmt => {
352
373
  switch (stmt.type) {
353
374
  case "VariableDeclaration":
354
375
  for (const decl of stmt.declarations) {
@@ -358,77 +379,44 @@ export class Compiler {
358
379
  case "FunctionDeclaration":
359
380
  if (stmt.id) scope.define(stmt.id.name, ctx);
360
381
  break;
361
- case "BlockStatement":
362
- this._hoistVars(stmt.body, scope, ctx);
363
- break;
364
- case "IfStatement":
365
- {
366
- const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
367
- this._hoistVars(cons, scope, ctx);
368
- if (stmt.alternate) {
369
- const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
370
- this._hoistVars(alt, scope, ctx);
371
- }
372
- break;
373
- }
374
- case "WhileStatement":
375
- case "DoWhileStatement":
376
- {
377
- const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
378
- this._hoistVars(body, scope, ctx);
379
- break;
380
- }
381
- case "ForStatement":
382
- {
383
- if (stmt.init?.type === "VariableDeclaration") {
384
- for (const decl of stmt.init.declarations) {
385
- if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
386
- }
387
- }
388
- const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
389
- this._hoistVars(body, scope, ctx);
390
- break;
391
- }
392
382
  case "ForInStatement":
393
- {
394
- // Reserve a hidden virtual register for the iterator object.
395
- stmt._iterSlot = ctx._newReg();
396
- if (stmt.left.type === "VariableDeclaration") {
397
- for (const decl of stmt.left.declarations) {
398
- if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
399
- }
400
- }
401
- const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
402
- this._hoistVars(body, scope, ctx);
403
- break;
404
- }
405
- case "SwitchStatement":
406
- for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
383
+ // Reserve a hidden virtual register for the iterator object.
384
+ stmt._iterSlot = ctx._newReg();
407
385
  break;
408
386
  case "TryStatement":
409
- this._hoistVars(stmt.block.body, scope, ctx);
410
387
  if (stmt.handler) {
411
388
  if (stmt.handler.param?.type === "Identifier") {
412
- // Catch parameter IS the exception register.
413
389
  scope.define(stmt.handler.param.name, ctx);
414
390
  } else {
415
- // No catch binding – reserve a dummy virtual register for the exception value.
391
+ // No catch binding – reserve a dummy register for the exception value.
416
392
  stmt._exceptionSlot = ctx._newReg();
417
393
  }
418
- this._hoistVars(stmt.handler.body.body, scope, ctx);
419
394
  }
420
- break;
421
- case "LabeledStatement":
422
- this._hoistVars([stmt.body], scope, ctx);
395
+ if (stmt.finalizer) {
396
+ // Two stable locals survive the finalizer: contReg (resume PC) and
397
+ // payloadReg (pending return value / in-flight exception).
398
+ stmt._finallyContReg = ctx._newReg();
399
+ stmt._finallyPayloadReg = ctx._newReg();
400
+ }
423
401
  break;
424
402
  }
425
- }
403
+ });
404
+ }
405
+
406
+ // Collect all FunctionDeclaration nodes reachable in the current function
407
+ // scope (does not cross into nested function bodies).
408
+ _collectHoistedFunctions(stmts) {
409
+ const result = [];
410
+ walkHoistScope(stmts, stmt => {
411
+ if (stmt.type === "FunctionDeclaration") result.push(stmt);
412
+ });
413
+ return result;
426
414
  }
427
415
  profileData = {
428
416
  transforms: {}
429
417
  };
430
418
 
431
- // ── Entry point ───────────────────────────────────────────────────────────
419
+ // Entry point
432
420
  compile(source) {
433
421
  let startedAt = now();
434
422
  const ast = parse(source, {
@@ -445,10 +433,23 @@ export class Compiler {
445
433
  return this.bytecode;
446
434
  }
447
435
 
448
- // ── Function compilation ───────────────────────────────────────────────────
436
+ // Function compilation
449
437
  _compileFunctionDecl(node) {
438
+ const isArrow = node.type === "ArrowFunctionExpression";
450
439
  ok(!node.generator, "Generator functions are not supported");
451
440
  ok(!node.async, "Async functions are not supported");
441
+
442
+ // Arrow functions do NOT bind their own `this` or `arguments`; both are
443
+ // inherited lexically from the nearest enclosing non-arrow function. We
444
+ // model this with the ordinary upvalue machinery: a non-arrow function
445
+ // materializes its receiver into a hidden `this` local (see the prologue
446
+ // below) and an arrow that references `this`/`arguments` simply resolves the
447
+ // name up the scope chain, capturing it as an upvalue. The result is that an
448
+ // arrow's MAKE_CLOSURE is byte-for-byte indistinguishable from any other
449
+ // nested closure — there is no "arrow" marker anywhere in the bytecode.
450
+ // `node.body` may be an Expression (concise body: `x => x + 1`) rather than
451
+ // a BlockStatement.
452
+ const isBlockBody = node.body.type === "BlockStatement";
452
453
  var fnIdx = this.fnDescriptors.length;
453
454
  const entryLabel = this._makeLabel(`fn_${fnIdx}`);
454
455
  var desc = {};
@@ -473,11 +474,41 @@ export class Compiler {
473
474
  }
474
475
  }
475
476
 
476
- // 2. Reserve the `arguments` virtual register (immediately after params).
477
- ctx.scope.define("arguments", ctx);
477
+ // 2. Reserve the `arguments` virtual register (immediately after params)
478
+ // and a hidden `this` register (immediately after that). Order matters: the
479
+ // runtime writes the args array into slot `paramCount`, so `arguments` must
480
+ // keep that slot and `this` follows it. Arrow functions bind neither — a
481
+ // reference climbs the scope chain to the enclosing function's register.
482
+ if (!isArrow) {
483
+ ctx.scope.define("arguments", ctx);
484
+ const thisReg = ctx.scope.define("this", ctx);
485
+ // Prologue: materialize the receiver (frame.thisVal) into the hidden
486
+ // `this` local so it reads like any other register and can be captured as
487
+ // an upvalue by nested arrows. This is the only place LOAD_THIS is now
488
+ // emitted, so `this` usage sites become generic register reads.
489
+ this.emit(ctx.bc, [this.OP.LOAD_THIS, thisReg], node);
490
+ }
478
491
 
479
492
  // 3. Hoist all var declarations so locals are allocated before any temps.
480
- this._hoistVars(node.body.body, ctx.scope, ctx);
493
+ // Concise-body arrows have an expression body with no statements to hoist.
494
+ if (isBlockBody) {
495
+ this._hoistVars(node.body.body, ctx.scope, ctx);
496
+ }
497
+
498
+ // 4. Hoist function declarations: compile and emit MAKE_CLOSURE at function
499
+ // entry so they are available before any code in the body runs.
500
+ if (isBlockBody) {
501
+ const hoistedFnDecls = this._collectHoistedFunctions(node.body.body);
502
+ for (const fnDecl of hoistedFnDecls) {
503
+ const fnDesc = this._compileFunctionDecl(fnDecl);
504
+ fnDecl._hoistedDesc = fnDesc;
505
+ const closureReg = this._emitMakeClosure(fnDesc, fnDecl, ctx.bc);
506
+ const slot = ctx.scope._locals.get(fnDecl.id.name);
507
+ if (closureReg !== slot) {
508
+ this.emit(ctx.bc, [this.OP.MOVE, slot, closureReg], fnDecl);
509
+ }
510
+ }
511
+ }
481
512
 
482
513
  // 5. Emit default-value guards.
483
514
  for (const param of node.params) {
@@ -507,14 +538,20 @@ export class Compiler {
507
538
  }
508
539
 
509
540
  // 6. Compile body.
510
- for (const stmt of node.body.body) {
511
- this._compileStatement(stmt, ctx.scope, ctx.bc);
512
- }
541
+ if (!isBlockBody) {
542
+ // Concise-body arrow: `(...) => expr` is equivalent to `{ return expr }`.
543
+ const reg = this._compileExpr(node.body, ctx.scope, ctx.bc);
544
+ this.emit(ctx.bc, [this.OP.RETURN, reg], node);
545
+ } else {
546
+ for (const stmt of node.body.body) {
547
+ this._compileStatement(stmt, ctx.scope, ctx.bc);
548
+ }
513
549
 
514
- // Implicit return undefined at end of function.
515
- const reg_undef = ctx.allocReg();
516
- this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], node);
517
- this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
550
+ // Implicit return undefined at end of function.
551
+ const reg_undef = ctx.allocReg();
552
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], node);
553
+ this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
554
+ }
518
555
  this._currentCtx = savedCtx;
519
556
  this._loopStack = savedLoopStack;
520
557
  node._fnIdx = fnIdx;
@@ -523,6 +560,13 @@ export class Compiler {
523
560
  desc.bytecode = ctx.bc;
524
561
  desc._fnIdx = fnIdx;
525
562
  desc.paramCount = node.params.length;
563
+ // Leading locals whose slots are fixed by position and written by the
564
+ // runtime at call time: the params (slots 0..paramCount-1), plus — for
565
+ // non-arrow functions — `arguments` (slot paramCount) and the hidden `this`
566
+ // (slot paramCount+1). These MUST get an identity slot mapping even when
567
+ // unused, otherwise a later local/upvalue capture slides into a param slot
568
+ // and the runtime's fixed-slot writes corrupt it. See resolveRegisters().
569
+ desc.reservedRegisters = node.params.length + (isArrow ? 0 : 2);
526
570
  desc.hasRest = hasRest;
527
571
  // regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
528
572
  desc.upvalues = ctx.upvalues.slice();
@@ -552,7 +596,92 @@ export class Compiler {
552
596
  return dst;
553
597
  }
554
598
 
555
- // ── Main (top-level) ───────────────────────────────────────────────────────
599
+ // Load a label's resolved PC into a register (resolveLabels fills the value).
600
+ // Used to seed a finalizer's continuation register with a resume target.
601
+ _emitLoadLabel(bc, reg, label, node) {
602
+ this.emit(bc, [this.OP.LOAD_INT, reg, {
603
+ type: "label",
604
+ label
605
+ }], node);
606
+ }
607
+
608
+ // Abrupt-completion unwinding
609
+ // Emits the bytecode that carries an abrupt completion (return / break /
610
+ // continue) out through every enclosing handler on the loop stack:
611
+ // • a "try" (catch-only) region is disarmed with TRY_END and we keep going;
612
+ // • a "finally" region is *routed through* — control is sent to the
613
+ // finalizer first and the remainder of the unwind resumes from the
614
+ // finalizer's continuation pad (see _routeThroughFinally);
615
+ // • loop/switch/block frames are skipped until we reach the break/continue
616
+ // target, where we emit the final JUMP.
617
+ // For a return with no enclosing finalizer this degenerates to the original
618
+ // "TRY_END per crossed try, then RETURN" behavior.
619
+ //
620
+ // action:
621
+ // { kind: "return", valueReg }
622
+ // { kind: "break" | "continue", targetEntry } (targetEntry is the loop-
623
+ // stack record to jump to; identified by object identity so it stays
624
+ // valid even when re-walked from a finalizer pad after the stack shrank)
625
+ _emitUnwind(bc, node, action) {
626
+ const stack = this._loopStack;
627
+ for (let i = stack.length - 1; i >= 0; i--) {
628
+ const entry = stack[i];
629
+ if (action.kind !== "return" && entry === action.targetEntry) {
630
+ const label = action.kind === "break" ? entry.breakLabel : entry.continueLabel;
631
+ this.emit(bc, [this.OP.JUMP, {
632
+ type: "label",
633
+ label
634
+ }], node);
635
+ return;
636
+ }
637
+ if (entry.type === "finally") {
638
+ this._routeThroughFinally(bc, node, entry, action);
639
+ return; // the finalizer's pad continues the unwind
640
+ }
641
+ if (entry.type === "try") {
642
+ this.emit(bc, [this.OP.TRY_END], node);
643
+ }
644
+ // loop / switch / block frames that aren't the target are just skipped.
645
+ }
646
+ if (action.kind === "return") {
647
+ this.emit(bc, [this.OP.RETURN, action.valueReg], node);
648
+ }
649
+ }
650
+
651
+ // Divert an abrupt completion into a finalizer. Schedules a continuation pad
652
+ // (emitted after the finalizer body) that re-issues the completion from the
653
+ // enclosing context, then sets the resume target + pending value and jumps to
654
+ // the finalizer. TRY_END disarms the finalizer's runtime record so an
655
+ // exception raised inside it doesn't loop back to itself.
656
+ _routeThroughFinally(bc, node, entry, action) {
657
+ const padLabel = this._makeLabel("finally_cont");
658
+ let contAction;
659
+ if (action.kind === "return") {
660
+ // Park the return value in the finalizer's payload register so it
661
+ // survives the finalizer body; the pad resumes the return from there.
662
+ if (action.valueReg !== entry.payloadReg) {
663
+ this.emit(bc, [this.OP.MOVE, entry.payloadReg, action.valueReg], node);
664
+ }
665
+ contAction = {
666
+ kind: "return",
667
+ valueReg: entry.payloadReg
668
+ };
669
+ } else {
670
+ contAction = action;
671
+ }
672
+ entry.pads.push({
673
+ label: padLabel,
674
+ emit: () => this._emitUnwind(bc, node, contAction)
675
+ });
676
+ this._emitLoadLabel(bc, entry.contReg, padLabel, node);
677
+ this.emit(bc, [this.OP.TRY_END], node); // disarm this finalizer's record
678
+ this.emit(bc, [this.OP.JUMP, {
679
+ type: "label",
680
+ label: entry.finallyLabel
681
+ }], node);
682
+ }
683
+
684
+ // Main (top-level)
556
685
  _compileMain(body) {
557
686
  const mainCtx = new FnContext(this, null);
558
687
  const savedCtx = this._currentCtx;
@@ -580,7 +709,7 @@ export class Compiler {
580
709
  this._currentCtx = savedCtx;
581
710
  }
582
711
 
583
- // ── Statements ────────────────────────────────────────────────────────────
712
+ // Statements
584
713
  // Wrapper that resets temps after every statement so that short-lived
585
714
  // expression temps don't accumulate across statements.
586
715
  _compileStatement(node, scope, bc) {
@@ -602,6 +731,8 @@ export class Compiler {
602
731
  break;
603
732
  case "FunctionDeclaration":
604
733
  {
734
+ // Already hoisted and emitted at function entry — skip.
735
+ if (node._hoistedDesc) break;
605
736
  const desc = this._compileFunctionDecl(node);
606
737
  const closureReg = this._emitMakeClosure(desc, node, bc);
607
738
  if (scope) {
@@ -629,12 +760,12 @@ export class Compiler {
629
760
  reg = ctx.allocReg();
630
761
  this.emit(bc, [this.OP.LOAD_CONST, reg, b.constantOperand(undefined)], node);
631
762
  }
632
- for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
633
- if (this._loopStack[_ri].type === "try") {
634
- this.emit(bc, [this.OP.TRY_END], node);
635
- }
636
- }
637
- this.emit(bc, [this.OP.RETURN, reg], node);
763
+ // Unwind through enclosing try/finally regions: disarm catch handlers
764
+ // and route through any finalizer before the value is actually returned.
765
+ this._emitUnwind(bc, node, {
766
+ kind: "return",
767
+ valueReg: reg
768
+ });
638
769
  break;
639
770
  }
640
771
  case "ExpressionStatement":
@@ -854,22 +985,19 @@ export class Compiler {
854
985
  if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
855
986
  } else {
856
987
  for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
857
- if (this._loopStack[_bi].type !== "try") {
988
+ const _t = this._loopStack[_bi].type;
989
+ if (_t !== "try" && _t !== "finally") {
858
990
  _bTargetIdx = _bi;
859
991
  break;
860
992
  }
861
993
  }
862
994
  if (_bTargetIdx === -1) throw new Error("break outside loop");
863
995
  }
864
- for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
865
- if (this._loopStack[_bi].type === "try") {
866
- this.emit(bc, [this.OP.TRY_END], node);
867
- }
868
- }
869
- this.emit(bc, [this.OP.JUMP, {
870
- type: "label",
871
- label: this._loopStack[_bTargetIdx].breakLabel
872
- }], node);
996
+ // Disarm catch handlers and route through finalizers on the way out.
997
+ this._emitUnwind(bc, node, {
998
+ kind: "break",
999
+ targetEntry: this._loopStack[_bTargetIdx]
1000
+ });
873
1001
  break;
874
1002
  }
875
1003
  case "ContinueStatement":
@@ -893,15 +1021,11 @@ export class Compiler {
893
1021
  }
894
1022
  if (_cTargetIdx === -1) throw new Error("continue outside loop");
895
1023
  }
896
- for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
897
- if (this._loopStack[_ci].type === "try") {
898
- this.emit(bc, [this.OP.TRY_END], node);
899
- }
900
- }
901
- this.emit(bc, [this.OP.JUMP, {
902
- type: "label",
903
- label: this._loopStack[_cTargetIdx].continueLabel
904
- }], node);
1024
+ // Disarm catch handlers and route through finalizers on the way out.
1025
+ this._emitUnwind(bc, node, {
1026
+ kind: "continue",
1027
+ targetEntry: this._loopStack[_cTargetIdx]
1028
+ });
905
1029
  break;
906
1030
  }
907
1031
  case "SwitchStatement":
@@ -1064,51 +1188,139 @@ export class Compiler {
1064
1188
  }
1065
1189
  case "TryStatement":
1066
1190
  {
1067
- if (node.finalizer) {
1068
- throw new Error("try..finally is not supported");
1191
+ if (!node.handler && !node.finalizer) {
1192
+ throw new Error("try without catch or finally is not supported");
1069
1193
  }
1070
- if (!node.handler) {
1071
- throw new Error("try without catch is not supported");
1194
+
1195
+ // Emits the inner try[/catch] region. When there is a finalizer this is
1196
+ // nested *inside* the finally region (CPython-style desugaring of
1197
+ // try/catch/finally into try { try/catch } finally), so each runtime
1198
+ // handler record is purely a catch or purely a finally — never both.
1199
+ const emitTryCatch = () => {
1200
+ if (!node.handler) {
1201
+ // try { … } finally { … } — no catch, just run the protected body.
1202
+ for (const stmt of node.block.body) {
1203
+ this._compileStatement(stmt, scope, bc);
1204
+ }
1205
+ return;
1206
+ }
1207
+ const catchLabel = this._makeLabel("catch");
1208
+ const afterCatchLabel = this._makeLabel("after_catch");
1209
+
1210
+ // Determine where the caught exception is written.
1211
+ const exceptionReg = node.handler.param?.type === "Identifier" ? scope?._locals.get(node.handler.param.name) ?? ctx.allocReg() // shouldn't normally reach here
1212
+ : node._exceptionSlot;
1213
+ this.emit(bc, [this.OP.TRY_SETUP, {
1214
+ type: "label",
1215
+ label: catchLabel
1216
+ }, exceptionReg], node);
1217
+ this._loopStack.push({
1218
+ type: "try",
1219
+ label: null,
1220
+ breakLabel: "",
1221
+ continueLabel: ""
1222
+ });
1223
+ for (const stmt of node.block.body) {
1224
+ this._compileStatement(stmt, scope, bc);
1225
+ }
1226
+ this._loopStack.pop();
1227
+ this.emit(bc, [this.OP.TRY_END], node);
1228
+ this.emit(bc, [this.OP.JUMP, {
1229
+ type: "label",
1230
+ label: afterCatchLabel
1231
+ }], node);
1232
+
1233
+ // Catch block: exceptionReg already holds the caught value.
1234
+ this.emit(bc, [null, {
1235
+ type: "defineLabel",
1236
+ label: catchLabel
1237
+ }], node);
1238
+
1239
+ // If no param binding, just ignore the exception (it's in the dummy slot).
1240
+ for (const stmt of node.handler.body.body) {
1241
+ this._compileStatement(stmt, scope, bc);
1242
+ }
1243
+ this.emit(bc, [null, {
1244
+ type: "defineLabel",
1245
+ label: afterCatchLabel
1246
+ }], node);
1247
+ };
1248
+ if (!node.finalizer) {
1249
+ emitTryCatch();
1250
+ break;
1072
1251
  }
1073
- const catchLabel = this._makeLabel("catch");
1074
- const afterCatchLabel = this._makeLabel("after_catch");
1075
1252
 
1076
- // Determine where the caught exception is written.
1077
- const exceptionReg = node.handler.param?.type === "Identifier" ? scope?._locals.get(node.handler.param.name) ?? ctx.allocReg() // shouldn't normally reach here
1078
- : node._exceptionSlot;
1079
- this.emit(bc, [this.OP.TRY_SETUP, {
1253
+ // try [catch] finally
1254
+ const finallyLabel = this._makeLabel("finally");
1255
+ const afterFinallyLabel = this._makeLabel("after_finally");
1256
+ const throwPadLabel = this._makeLabel("finally_throw");
1257
+ const contReg = node._finallyContReg;
1258
+ const payloadReg = node._finallyPayloadReg;
1259
+
1260
+ // Arm the finalizer for the whole protected region (try + catch).
1261
+ this.emit(bc, [this.OP.FINALLY_SETUP, {
1080
1262
  type: "label",
1081
- label: catchLabel
1082
- }, exceptionReg], node);
1083
- this._loopStack.push({
1084
- type: "try",
1263
+ label: finallyLabel
1264
+ }, contReg, payloadReg, {
1265
+ type: "label",
1266
+ label: throwPadLabel
1267
+ }], node);
1268
+ const finallyEntry = {
1269
+ type: "finally",
1085
1270
  label: null,
1086
1271
  breakLabel: "",
1087
- continueLabel: ""
1088
- });
1089
- for (const stmt of node.block.body) {
1090
- this._compileStatement(stmt, scope, bc);
1091
- }
1092
- this._loopStack.pop();
1272
+ continueLabel: "",
1273
+ finallyLabel,
1274
+ contReg,
1275
+ payloadReg,
1276
+ pads: []
1277
+ };
1278
+ this._loopStack.push(finallyEntry);
1279
+ emitTryCatch();
1280
+ this._loopStack.pop(); // leaving the protected region
1281
+
1282
+ // Normal completion: disarm the finalizer, set resume = after the whole
1283
+ // statement, then fall into the finalizer body.
1093
1284
  this.emit(bc, [this.OP.TRY_END], node);
1285
+ this._emitLoadLabel(bc, contReg, afterFinallyLabel, node);
1094
1286
  this.emit(bc, [this.OP.JUMP, {
1095
1287
  type: "label",
1096
- label: afterCatchLabel
1288
+ label: finallyLabel
1097
1289
  }], node);
1098
1290
 
1099
- // Catch block: exceptionReg already holds the caught value.
1291
+ // Finalizer body (compiled once). Runs with the enclosing context on
1292
+ // the loop stack, so an abrupt completion inside it routes outward
1293
+ // (overriding any pending completion — correct JS semantics).
1100
1294
  this.emit(bc, [null, {
1101
1295
  type: "defineLabel",
1102
- label: catchLabel
1296
+ label: finallyLabel
1103
1297
  }], node);
1104
-
1105
- // If no param binding, just ignore the exception (it's in the dummy slot).
1106
- for (const stmt of node.handler.body.body) {
1298
+ for (const stmt of node.finalizer.body) {
1107
1299
  this._compileStatement(stmt, scope, bc);
1108
1300
  }
1301
+ // END_FINALLY: resume at whatever the continuation register points to.
1302
+ this.emit(bc, [this.OP.JUMP_REG, contReg], node);
1303
+
1304
+ // Throw pad: re-raise the in-flight exception after the finalizer.
1109
1305
  this.emit(bc, [null, {
1110
1306
  type: "defineLabel",
1111
- label: afterCatchLabel
1307
+ label: throwPadLabel
1308
+ }], node);
1309
+ this.emit(bc, [this.OP.THROW, payloadReg], node);
1310
+
1311
+ // Continuation pads for return/break/continue that crossed this
1312
+ // finalizer (collected during body compilation, emitted now that the
1313
+ // enclosing loop-stack context is restored).
1314
+ for (const pad of finallyEntry.pads) {
1315
+ this.emit(bc, [null, {
1316
+ type: "defineLabel",
1317
+ label: pad.label
1318
+ }], node);
1319
+ pad.emit();
1320
+ }
1321
+ this.emit(bc, [null, {
1322
+ type: "defineLabel",
1323
+ label: afterFinallyLabel
1112
1324
  }], node);
1113
1325
  break;
1114
1326
  }
@@ -1120,7 +1332,65 @@ export class Compiler {
1120
1332
  }
1121
1333
  }
1122
1334
 
1123
- // ── Expressions ───────────────────────────────────────────────────────────
1335
+ // Returns true if any element in an argument/element list is a SpreadElement.
1336
+ _hasSpread(args) {
1337
+ return args.some(a => a != null && a.type === "SpreadElement");
1338
+ }
1339
+
1340
+ // Build a flat argument array at runtime when the call contains spread elements.
1341
+ // Returns a register holding an Array with all arguments flattened.
1342
+ // Strategy: build a prefix array from leading non-spread elements, then
1343
+ // repeatedly call Array.prototype.concat — spread elements are concat'd directly
1344
+ // (concat spreads array args one level), non-spread elements are wrapped in a
1345
+ // single-element array before concat so they aren't spread.
1346
+ _buildSpreadArgs(args, scope, bc, node) {
1347
+ const ctx = this._currentCtx;
1348
+ const firstSpreadIdx = args.findIndex(a => a != null && a.type === "SpreadElement");
1349
+
1350
+ // Build initial array from non-spread prefix (may be empty).
1351
+ const prefix = args.slice(0, firstSpreadIdx);
1352
+ const prefixRegs = prefix.map(a => {
1353
+ if (a === null) {
1354
+ const r = ctx.allocReg();
1355
+ this.emit(bc, [this.OP.LOAD_CONST, r, b.constantOperand(undefined)], node);
1356
+ return r;
1357
+ }
1358
+ return this._compileExpr(a, scope, bc);
1359
+ });
1360
+ let accReg = ctx.allocReg();
1361
+ this.emit(bc, [this.OP.BUILD_ARRAY, accReg, prefix.length, ...prefixRegs], node);
1362
+
1363
+ // Process each remaining arg via Array.prototype.concat.
1364
+ for (let i = firstSpreadIdx; i < args.length; i++) {
1365
+ const arg = args[i];
1366
+ const concatKeyReg = ctx.allocReg();
1367
+ this.emit(bc, [this.OP.LOAD_CONST, concatKeyReg, b.constantOperand("concat")], node);
1368
+ const concatFnReg = ctx.allocReg();
1369
+ this.emit(bc, [this.OP.GET_PROP, concatFnReg, accReg, concatKeyReg], node);
1370
+ let argArrReg;
1371
+ if (arg === null) {
1372
+ // Array hole — treat as undefined wrapped in [undefined]
1373
+ const elemReg = ctx.allocReg();
1374
+ this.emit(bc, [this.OP.LOAD_CONST, elemReg, b.constantOperand(undefined)], node);
1375
+ argArrReg = ctx.allocReg();
1376
+ this.emit(bc, [this.OP.BUILD_ARRAY, argArrReg, 1, elemReg], node);
1377
+ } else if (arg.type === "SpreadElement") {
1378
+ // Spread: concat the iterable directly (concat flattens one level).
1379
+ argArrReg = this._compileExpr(arg.argument, scope, bc);
1380
+ } else {
1381
+ // Non-spread: wrap in [elem] so concat doesn't flatten the value.
1382
+ const elemReg = this._compileExpr(arg, scope, bc);
1383
+ argArrReg = ctx.allocReg();
1384
+ this.emit(bc, [this.OP.BUILD_ARRAY, argArrReg, 1, elemReg], node);
1385
+ }
1386
+ const newAccReg = ctx.allocReg();
1387
+ this.emit(bc, [this.OP.CALL_METHOD, newAccReg, accReg, concatFnReg, 1, argArrReg], node);
1388
+ accReg = newAccReg;
1389
+ }
1390
+ return accReg;
1391
+ }
1392
+
1393
+ // Expressions
1124
1394
  // Returns the virtual RegisterOperand that holds the result.
1125
1395
  // For local variables: returns their RegisterOperand directly (no instruction emitted).
1126
1396
  // For all others: allocates a fresh virtual register, emits the instruction(s),
@@ -1184,16 +1454,37 @@ export class Compiler {
1184
1454
  }
1185
1455
  case "ThisExpression":
1186
1456
  {
1457
+ // `this` is resolved like any ordinary binding. In a non-arrow function
1458
+ // it is a hidden local (materialized by the entry prologue); in an arrow
1459
+ // it climbs to the enclosing function and becomes an upvalue read. Either
1460
+ // way the usage site is a generic register/upvalue access, not a
1461
+ // semantically-revealing LOAD_THIS.
1462
+ const res = this._resolve("this", this._currentCtx);
1463
+ if (res.kind === "local") return res.reg; // register IS the local
1464
+ if (res.kind === "upvalue") {
1465
+ const dst = ctx.allocReg();
1466
+ this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
1467
+ return dst;
1468
+ }
1469
+ // Fallback (no enclosing `this` binding, e.g. a stray top-level use):
1470
+ // read the frame receiver directly.
1187
1471
  const dst = ctx.allocReg();
1188
1472
  this.emit(bc, [this.OP.LOAD_THIS, dst], node);
1189
1473
  return dst;
1190
1474
  }
1191
1475
  case "NewExpression":
1192
1476
  {
1193
- const calleeReg = this._compileExpr(node.callee, scope, bc);
1194
- const argRegs = node.arguments.map(a => this._compileExpr(a, scope, bc));
1477
+ const n = node;
1478
+ ok(n.arguments.length < U16_MAX, `Too many arguments (max ${U16_MAX - 1})`);
1479
+ const calleeReg = this._compileExpr(n.callee, scope, bc);
1195
1480
  const dst = ctx.allocReg();
1196
- this.emit(bc, [this.OP.NEW, dst, calleeReg, node.arguments.length, ...argRegs], node);
1481
+ if (this._hasSpread(n.arguments)) {
1482
+ const argsArrayReg = this._buildSpreadArgs(n.arguments, scope, bc, node);
1483
+ this.emit(bc, [this.OP.NEW, dst, calleeReg, this.SENTINELS.CALL_SPREAD, argsArrayReg], node);
1484
+ } else {
1485
+ const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1486
+ this.emit(bc, [this.OP.NEW, dst, calleeReg, n.arguments.length, ...argRegs], node);
1487
+ }
1197
1488
  return dst;
1198
1489
  }
1199
1490
  case "SequenceExpression":
@@ -1245,17 +1536,32 @@ export class Compiler {
1245
1536
  const n = node;
1246
1537
  const endLabel = this._makeLabel("logical_end");
1247
1538
  const isOr = n.operator === "||";
1248
- if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
1539
+ const isNullish = n.operator === "??";
1540
+ if (!isOr && !isNullish && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
1249
1541
  const lhsReg = this._compileExpr(n.left, scope, bc);
1250
1542
  const reg_result = ctx.allocReg();
1251
1543
  if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
1252
-
1253
- // For ||: if truthy keep LHS, jump past RHS.
1254
- // For &&: if falsy keep LHS, jump past RHS.
1255
- this.emit(bc, [isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE, reg_result, {
1256
- type: "label",
1257
- label: endLabel
1258
- }], node);
1544
+ if (isNullish) {
1545
+ // a ?? b keep LHS unless it is null or undefined, otherwise use RHS.
1546
+ // `reg_result == null` (loose) is true for exactly null and undefined,
1547
+ // which is precisely the set of "nullish" values.
1548
+ const nullReg = ctx.allocReg();
1549
+ this.emit(bc, [this.OP.LOAD_CONST, nullReg, b.constantOperand(null)], node);
1550
+ const isNullishReg = ctx.allocReg();
1551
+ this.emit(bc, [this.OP.LOOSE_EQ, isNullishReg, reg_result, nullReg], node);
1552
+ // Not nullish → keep LHS and skip RHS.
1553
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, isNullishReg, {
1554
+ type: "label",
1555
+ label: endLabel
1556
+ }], node);
1557
+ } else {
1558
+ // For ||: if truthy keep LHS, jump past RHS.
1559
+ // For &&: if falsy keep LHS, jump past RHS.
1560
+ this.emit(bc, [isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE, reg_result, {
1561
+ type: "label",
1562
+ label: endLabel
1563
+ }], node);
1564
+ }
1259
1565
 
1260
1566
  // Compile RHS into reg_result.
1261
1567
  const rhsReg = this._compileExpr(n.right, scope, bc);
@@ -1297,6 +1603,7 @@ export class Compiler {
1297
1603
  "*": this.OP.MUL,
1298
1604
  "/": this.OP.DIV,
1299
1605
  "%": this.OP.MOD,
1606
+ "**": this.OP.EXP,
1300
1607
  "&": this.OP.BAND,
1301
1608
  "|": this.OP.BOR,
1302
1609
  "^": this.OP.BXOR,
@@ -1385,6 +1692,7 @@ export class Compiler {
1385
1692
  "*=": this.OP.MUL,
1386
1693
  "/=": this.OP.DIV,
1387
1694
  "%=": this.OP.MOD,
1695
+ "**=": this.OP.EXP,
1388
1696
  "&=": this.OP.BAND,
1389
1697
  "|=": this.OP.BOR,
1390
1698
  "^=": this.OP.BXOR,
@@ -1457,6 +1765,7 @@ export class Compiler {
1457
1765
  case "CallExpression":
1458
1766
  {
1459
1767
  const n = node;
1768
+ ok(n.arguments.length < U16_MAX, `Too many arguments (max ${U16_MAX - 1})`);
1460
1769
  if (n.callee.type === "MemberExpression") {
1461
1770
  // Method call: receiver.method(args)
1462
1771
  const receiverReg = this._compileExpr(n.callee.object, scope, bc);
@@ -1469,16 +1778,26 @@ export class Compiler {
1469
1778
  }
1470
1779
  const calleeReg = ctx.allocReg();
1471
1780
  this.emit(bc, [this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg], node);
1472
- const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1473
1781
  const dst = ctx.allocReg();
1474
- this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, n.arguments.length, ...argRegs], node);
1782
+ if (this._hasSpread(n.arguments)) {
1783
+ const argsArrayReg = this._buildSpreadArgs(n.arguments, scope, bc, node);
1784
+ this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, this.SENTINELS.CALL_SPREAD, argsArrayReg], node);
1785
+ } else {
1786
+ const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1787
+ this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, n.arguments.length, ...argRegs], node);
1788
+ }
1475
1789
  return dst;
1476
1790
  } else {
1477
1791
  // Plain call: fn(args)
1478
1792
  const calleeReg = this._compileExpr(n.callee, scope, bc);
1479
- const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1480
1793
  const dst = ctx.allocReg();
1481
- this.emit(bc, [this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs], node);
1794
+ if (this._hasSpread(n.arguments)) {
1795
+ const argsArrayReg = this._buildSpreadArgs(n.arguments, scope, bc, node);
1796
+ this.emit(bc, [this.OP.CALL, dst, calleeReg, this.SENTINELS.CALL_SPREAD, argsArrayReg], node);
1797
+ } else {
1798
+ const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1799
+ this.emit(bc, [this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs], node);
1800
+ }
1482
1801
  return dst;
1483
1802
  }
1484
1803
  }
@@ -1553,6 +1872,14 @@ export class Compiler {
1553
1872
  const desc = this._compileFunctionDecl(node);
1554
1873
  return this._emitMakeClosure(desc, node, bc);
1555
1874
  }
1875
+ case "ArrowFunctionExpression":
1876
+ {
1877
+ // Arrows compile through the same path as any other function. They differ
1878
+ // only in that they bind no `this`/`arguments` (handled in
1879
+ // _compileFunctionDecl), so the resulting closure is indistinguishable.
1880
+ const desc = this._compileFunctionDecl(node);
1881
+ return this._emitMakeClosure(desc, node, bc);
1882
+ }
1556
1883
  case "MemberExpression":
1557
1884
  {
1558
1885
  const n = node;
@@ -1571,6 +1898,9 @@ export class Compiler {
1571
1898
  case "ArrayExpression":
1572
1899
  {
1573
1900
  const n = node;
1901
+ if (this._hasSpread(n.elements)) {
1902
+ return this._buildSpreadArgs(n.elements, scope, bc, node);
1903
+ }
1574
1904
  const elemRegs = n.elements.map(el => {
1575
1905
  if (el === null) {
1576
1906
  const r = ctx.allocReg();
@@ -1586,45 +1916,87 @@ export class Compiler {
1586
1916
  case "ObjectExpression":
1587
1917
  {
1588
1918
  const n = node;
1589
- const regularProps = [];
1590
- const accessorProps = [];
1591
- for (const prop of n.properties) {
1592
- if (prop.type === "SpreadElement") throw new Error("Object spread not supported");
1593
- if (prop.type === "ObjectMethod") {
1594
- if (prop.kind === "get" || prop.kind === "set") {
1595
- if (prop.computed) throw new Error("Computed getter/setter keys are not supported");
1596
- accessorProps.push(prop);
1597
- } else {
1598
- throw new Error("Shorthand method syntax is not supported");
1599
- }
1600
- } else {
1601
- regularProps.push(prop);
1919
+ const hasSpread = n.properties.some(p => p.type === "SpreadElement");
1920
+ const hasComputed = n.properties.some(p => (p.type === "ObjectProperty" || p.type === "ObjectMethod") && p.computed);
1921
+ const hasMethodShorthand = n.properties.some(p => p.type === "ObjectMethod" && p.kind === "method");
1922
+
1923
+ // Fast path: no spread, no computed keys, no method shorthands.
1924
+ // Uses BUILD_OBJECT for data properties then DEFINE_GETTER/SETTER for accessors.
1925
+ if (!hasSpread && !hasComputed && !hasMethodShorthand) {
1926
+ const regularProps = [];
1927
+ const accessorProps = [];
1928
+ for (const prop of n.properties) {
1929
+ if (prop.type === "ObjectMethod") accessorProps.push(prop);else regularProps.push(prop);
1602
1930
  }
1931
+ const pairRegs = [];
1932
+ for (const prop of regularProps) {
1933
+ const key = prop.key;
1934
+ let keyStr;
1935
+ 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}`);
1936
+ const keyReg = ctx.allocReg();
1937
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1938
+ const valReg = this._compileExpr(prop.value, scope, bc);
1939
+ pairRegs.push(keyReg, valReg);
1940
+ }
1941
+ const dst = ctx.allocReg();
1942
+ this.emit(bc, [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs], node);
1943
+ for (const prop of accessorProps) {
1944
+ const key = prop.key;
1945
+ let keyStr;
1946
+ 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}`);
1947
+ const keyReg = ctx.allocReg();
1948
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1949
+ const fnReg = this._emitMakeClosure(this._compileFunctionDecl(prop), prop, bc);
1950
+ this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
1951
+ }
1952
+ return dst;
1603
1953
  }
1604
1954
 
1605
- // Build flat [key, val, key, val, …] register list.
1606
- const pairRegs = [];
1607
- for (const prop of regularProps) {
1608
- let keyStr;
1609
- const key = prop.key;
1610
- 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}`);
1611
- const keyReg = ctx.allocReg();
1612
- this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1613
- const valReg = this._compileExpr(prop.value, scope, bc);
1614
- pairRegs.push(keyReg, valReg);
1615
- }
1955
+ // General path: handles spread elements, computed keys, and method shorthands.
1956
+ // Builds an empty object then sets each property in source order.
1616
1957
  const dst = ctx.allocReg();
1617
- this.emit(bc, [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs], node);
1618
-
1619
- // Define accessors on the object now sitting in `dst`.
1620
- for (const prop of accessorProps) {
1621
- const key = prop.key;
1622
- let keyStr;
1623
- 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}`);
1624
- const keyReg = ctx.allocReg();
1625
- this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1626
- const fnReg = this._emitMakeClosure(this._compileFunctionDecl(prop), prop, bc);
1627
- this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
1958
+ this.emit(bc, [this.OP.BUILD_OBJECT, dst, 0], node);
1959
+ for (const prop of n.properties) {
1960
+ if (prop.type === "SpreadElement") {
1961
+ // {…src} copies own enumerable properties via Object.assign(dst, src).
1962
+ const objGlobalReg = ctx.allocReg();
1963
+ this.emit(bc, [this.OP.LOAD_GLOBAL, objGlobalReg, b.constantOperand("Object")], node);
1964
+ const assignKeyReg = ctx.allocReg();
1965
+ this.emit(bc, [this.OP.LOAD_CONST, assignKeyReg, b.constantOperand("assign")], node);
1966
+ const assignFnReg = ctx.allocReg();
1967
+ this.emit(bc, [this.OP.GET_PROP, assignFnReg, objGlobalReg, assignKeyReg], node);
1968
+ const spreadValReg = this._compileExpr(prop.argument, scope, bc);
1969
+ const _assignResultReg = ctx.allocReg();
1970
+ this.emit(bc, [this.OP.CALL_METHOD, _assignResultReg, objGlobalReg, assignFnReg, 2, dst, spreadValReg], node);
1971
+ } else {
1972
+ const p = prop;
1973
+
1974
+ // Resolve key: computed → evaluate expression; static → load constant.
1975
+ let keyReg;
1976
+ if (p.computed) {
1977
+ keyReg = this._compileExpr(p.key, scope, bc);
1978
+ } else {
1979
+ const key = p.key;
1980
+ let keyStr;
1981
+ 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}`);
1982
+ keyReg = ctx.allocReg();
1983
+ this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1984
+ }
1985
+ if (p.type === "ObjectMethod") {
1986
+ const fnReg = this._emitMakeClosure(this._compileFunctionDecl(p), p, bc);
1987
+ if (p.kind === "get") {
1988
+ this.emit(bc, [this.OP.DEFINE_GETTER, dst, keyReg, fnReg], node);
1989
+ } else if (p.kind === "set") {
1990
+ this.emit(bc, [this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
1991
+ } else {
1992
+ // method shorthand: {foo() {}} ≡ {foo: function() {}}
1993
+ this.emit(bc, [this.OP.SET_PROP, dst, keyReg, fnReg], node);
1994
+ }
1995
+ } else {
1996
+ const valReg = this._compileExpr(p.value, scope, bc);
1997
+ this.emit(bc, [this.OP.SET_PROP, dst, keyReg, valReg], node);
1998
+ }
1999
+ }
1628
2000
  }
1629
2001
  return dst;
1630
2002
  }
@@ -1636,7 +2008,7 @@ export class Compiler {
1636
2008
  }
1637
2009
  }
1638
2010
 
1639
- // ── Serializer ────────────────────────────────────────────────────────────────
2011
+ // Serializer
1640
2012
  class Serializer {
1641
2013
  constructor(compiler) {
1642
2014
  this.compiler = compiler;
@@ -1844,6 +2216,8 @@ class Serializer {
1844
2216
  sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
1845
2217
  const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
1846
2218
  sections.push(`var OP = ${generate(object).code};`);
2219
+ const sentinelsObject = t.objectExpression(Object.entries(compiler.SENTINELS).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
2220
+ sections.push(`var SENTINELS = ${generate(sentinelsObject).code};`);
1847
2221
  initBody.push(this._serializeConstants(constants));
1848
2222
  sections = [...initBody, ...sections];
1849
2223
  sections.push(VM_RUNTIME);
@@ -1908,7 +2282,8 @@ export async function compileAndSerialize(sourceCode, options) {
1908
2282
  timings[name] = elapsedMs;
1909
2283
  compiler.profileData.transforms[name] = {
1910
2284
  transformTime: elapsedMs,
1911
- bytecodeSize: bytecode.length
2285
+ bytecodeSize: bytecode.length,
2286
+ flatBytecodeSize: bytecode.flat().length
1912
2287
  };
1913
2288
  compiler.log(`Bytecode pass ${name} completed in ${Math.floor(elapsedMs)}ms`);
1914
2289
  return passResult;
@@ -1948,7 +2323,7 @@ export async function compileAndSerialize(sourceCode, options) {
1948
2323
  const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
1949
2324
 
1950
2325
  // for (const key of Object.keys(timings)) {
1951
- // console.log(` ${key}: ${timings[key]}ms`);
2326
+ // console.log(` ${key}: ${timings[key]}ms`);
1952
2327
  // }
1953
2328
 
1954
2329
  // This part was purposefully pulled out Serializer as OP_NAME's get resolved during buildRuntime