js-confuser-vm 0.1.0 → 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 (63) hide show
  1. package/README.md +281 -147
  2. package/dist/build-runtime.js +41 -15
  3. package/dist/compiler.js +714 -265
  4. package/dist/disassembler.js +367 -0
  5. package/dist/index.js +7 -2
  6. package/dist/runtime.js +160 -119
  7. package/dist/template.js +163 -42
  8. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  9. package/dist/transforms/bytecode/concealConstants.js +2 -2
  10. package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
  11. package/dist/transforms/bytecode/dispatcher.js +15 -111
  12. package/dist/transforms/bytecode/macroOpcodes.js +2 -2
  13. package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
  14. package/dist/transforms/bytecode/resolveRegisters.js +23 -4
  15. package/dist/transforms/bytecode/selfModifying.js +88 -21
  16. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  17. package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
  18. package/dist/transforms/bytecode/stringConcealing.js +288 -0
  19. package/dist/transforms/runtime/classObfuscation.js +43 -0
  20. package/dist/transforms/runtime/handlerTable.js +91 -0
  21. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  22. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  23. package/dist/types.js +1 -1
  24. package/dist/utils/ast-utils.js +75 -0
  25. package/dist/utils/op-utils.js +1 -2
  26. package/dist/utils/pass-utils.js +100 -0
  27. package/dist/utils/profile-utils.js +3 -0
  28. package/package.json +8 -1
  29. package/.gitmodules +0 -4
  30. package/.prettierignore +0 -1
  31. package/CHANGELOG.md +0 -335
  32. package/babel-plugin-inline-runtime.cjs +0 -34
  33. package/babel.config.json +0 -23
  34. package/index.ts +0 -38
  35. package/jest-strip-types.js +0 -10
  36. package/jest.config.js +0 -52
  37. package/src/build-runtime.ts +0 -78
  38. package/src/compiler.ts +0 -2593
  39. package/src/index.ts +0 -14
  40. package/src/minify.ts +0 -21
  41. package/src/options.ts +0 -18
  42. package/src/runtime.ts +0 -923
  43. package/src/template.ts +0 -141
  44. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
  45. package/src/transforms/bytecode/concealConstants.ts +0 -52
  46. package/src/transforms/bytecode/dispatcher.ts +0 -398
  47. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  48. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  49. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  50. package/src/transforms/bytecode/resolveRegisters.ts +0 -221
  51. package/src/transforms/bytecode/selfModifying.ts +0 -121
  52. package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
  53. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  54. package/src/transforms/runtime/internalVariables.ts +0 -270
  55. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  56. package/src/transforms/runtime/microOpcodes.ts +0 -93
  57. package/src/transforms/runtime/minify.ts +0 -1
  58. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  59. package/src/transforms/runtime/specializedOpcodes.ts +0 -156
  60. package/src/types.ts +0 -93
  61. package/src/utils/op-utils.ts +0 -48
  62. package/src/utils/random-utils.ts +0 -31
  63. package/tsconfig.json +0 -12
package/dist/compiler.js CHANGED
@@ -5,40 +5,43 @@ import traverseImport from "@babel/traverse";
5
5
  import { generate } from "@babel/generator";
6
6
  import { stripTypeScriptTypes } from "module";
7
7
  import { ok } from "assert";
8
- import { obfuscateRuntime } from "./build-runtime.js";
8
+ import { buildRuntime } from "./build-runtime.js";
9
9
  import { DEFAULT_OPTIONS } from "./options.js";
10
10
  import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
11
11
  import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.js";
12
- import { resolveConstants } from "./transforms/bytecode/resolveContants.js";
12
+ import { resolveConstants } from "./transforms/bytecode/resolveConstants.js";
13
13
  import { selfModifying } from "./transforms/bytecode/selfModifying.js";
14
14
  import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
15
- import { microOpcodes } from "./transforms/bytecode/microOpcodes.js";
16
15
  import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
17
16
  import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
18
17
  import { getRandomInt } from "./utils/random-utils.js";
19
- import { U16_MAX } from "./utils/op-utils.js";
18
+ import { U16_MAX, U32_MAX } from "./utils/op-utils.js";
20
19
  import { concealConstants } from "./transforms/bytecode/concealConstants.js";
21
20
  import { dispatcher } from "./transforms/bytecode/dispatcher.js";
21
+ import { controlFlowFlattening } from "./transforms/bytecode/controlFlowFlattening.js";
22
+ import { stringConcealing } from "./transforms/bytecode/stringConcealing.js";
23
+ import { now } from "./utils/profile-utils.js";
24
+ import { walkHoistScope } from "./utils/ast-utils.js";
22
25
  const traverse = traverseImport.default || traverseImport;
23
- const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue \u2014 Lua/CPython style.\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\n// _absSlot is the absolute index in VM._regs (frame._base + local slot).\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\n// Frame \u2014 analogous to Lua CallInfo / CPython PyFrameObject.\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // \u2500\u2500 Flat register file (Lua-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // All frames share a single array. Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n this._internals = {};\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var ctor = regs[base + this._operand()];\n if (typeof ctor === \"function\") {\n regs[base + dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n regs[base + dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n for (var i = 0; i < args.length && i < c.fn.regCount; i++) {\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: target PC is read from a register rather than a\n // bytecode immediate. Used by the jumpDispatcher pass so that static\n // analysis cannot determine the jump destination without tracking the\n // register value (which contains an encoded PC resolved at runtime).\n frame._pc = regs[base + this._operand()];\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n this._regs[handledFrame._base + h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals.window = window;\n for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
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";
24
27
  export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
25
28
  export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
26
29
 
27
- // ── Opcodes ──────────────────────────────────────────────────────────────────
30
+ // Opcodes
28
31
  // Register-based encoding. Operand convention (x86 / CPython style):
29
- // destination register first, then source registers, then immediates.
32
+ // destination register first, then source registers, then immediates.
30
33
  //
31
- // dst – register index that receives the result
32
- // src – register index holding an input value
33
- // 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 …)
34
37
  //
35
38
  // Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
36
39
  // Every load: [op, dst, ...]
37
40
  // Every store: [op, target, src]
38
41
  // Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
39
- // CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
42
+ // CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
40
43
  export const OP_ORIGINAL = {
41
- // ── Loads ─────────────────────────────────────────────────────────────────
44
+ // Loads
42
45
  LOAD_CONST: 0,
43
46
  // dst, constIdx regs[dst] = constants[constIdx]
44
47
  LOAD_INT: 1,
@@ -52,13 +55,13 @@ export const OP_ORIGINAL = {
52
55
  MOVE: 5,
53
56
  // dst, src regs[dst] = regs[src]
54
57
 
55
- // ── Stores ────────────────────────────────────────────────────────────────
58
+ // Stores
56
59
  STORE_GLOBAL: 6,
57
60
  // nameIdx, src globals[constants[nameIdx]] = regs[src]
58
61
  STORE_UPVALUE: 7,
59
62
  // uvIdx, src upvalues[uvIdx].write(regs[src])
60
63
 
61
- // ── Property access ───────────────────────────────────────────────────────
64
+ // Property access
62
65
  GET_PROP: 8,
63
66
  // dst, obj, key regs[dst] = regs[obj][regs[key]]
64
67
  SET_PROP: 9,
@@ -66,19 +69,21 @@ export const OP_ORIGINAL = {
66
69
  DELETE_PROP: 10,
67
70
  // dst, obj, key regs[dst] = delete regs[obj][regs[key]]
68
71
 
69
- // ── Arithmetic / bitwise (dst, src1, src2) ───────────────────────────────
72
+ // Arithmetic / bitwise (dst, src1, src2)
70
73
  ADD: 11,
71
74
  SUB: 12,
72
75
  MUL: 13,
73
76
  DIV: 14,
74
77
  MOD: 15,
78
+ EXP: 60,
79
+ // dst, src1, src2 regs[dst] = regs[src1] ** regs[src2]
75
80
  BAND: 16,
76
81
  BOR: 17,
77
82
  BXOR: 18,
78
83
  SHL: 19,
79
84
  SHR: 20,
80
85
  USHR: 21,
81
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────────
86
+ // Comparison (dst, src1, src2)
82
87
  LT: 22,
83
88
  GT: 23,
84
89
  LTE: 24,
@@ -89,7 +94,7 @@ export const OP_ORIGINAL = {
89
94
  LOOSE_NEQ: 29,
90
95
  IN: 30,
91
96
  INSTANCEOF: 31,
92
- // ── Unary (dst, src) ─────────────────────────────────────────────────────
97
+ // Unary (dst, src)
93
98
  UNARY_NEG: 32,
94
99
  UNARY_POS: 33,
95
100
  UNARY_NOT: 34,
@@ -101,7 +106,7 @@ export const OP_ORIGINAL = {
101
106
  TYPEOF_SAFE: 38,
102
107
  // dst, nameConstIdx – safe typeof for potentially-undeclared globals
103
108
 
104
- // ── Control flow ──────────────────────────────────────────────────────────
109
+ // Control flow
105
110
  JUMP: 39,
106
111
  // target
107
112
  JUMP_IF_FALSE: 40,
@@ -109,7 +114,7 @@ export const OP_ORIGINAL = {
109
114
  JUMP_IF_TRUE: 41,
110
115
  // src, target if regs[src] then pc = target (|| short-circuit)
111
116
 
112
- // ── Calls & constructors ──────────────────────────────────────────────────
117
+ // Calls & constructors
113
118
  CALL: 42,
114
119
  // dst, callee, argc, [argRegs…]
115
120
  CALL_METHOD: 43,
@@ -121,48 +126,59 @@ export const OP_ORIGINAL = {
121
126
  THROW: 46,
122
127
  // src
123
128
 
124
- // ── Closures ──────────────────────────────────────────────────────────────
129
+ // Closures
125
130
  // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
126
131
  MAKE_CLOSURE: 47,
127
- // ── Collections ───────────────────────────────────────────────────────────
132
+ // Collections
128
133
  BUILD_ARRAY: 48,
129
134
  // dst, count, [elemRegs…]
130
135
  BUILD_OBJECT: 49,
131
136
  // dst, pairCount, [keyReg, valReg, …]
132
137
 
133
- // ── Property definitions (getters / setters) ──────────────────────────────
138
+ // Property definitions (getters / setters)
134
139
  DEFINE_GETTER: 50,
135
140
  // obj, key, fn
136
141
  DEFINE_SETTER: 51,
137
142
  // obj, key, fn
138
143
 
139
- // ── For-in iteration ──────────────────────────────────────────────────────
144
+ // For-in iteration
140
145
  FOR_IN_SETUP: 52,
141
146
  // dst, src dst = { _keys: enumKeys(src), i: 0 }
142
147
  FOR_IN_NEXT: 53,
143
148
  // dst, iter, exitTarget
144
149
 
145
- // ── Exception handling ────────────────────────────────────────────────────
150
+ // Exception handling
146
151
  TRY_SETUP: 54,
147
152
  // handlerPc, exceptionReg
148
153
  TRY_END: 55,
149
- // ── Self-modifying bytecode ───────────────────────────────────────────────
154
+ // Self-modifying bytecode
150
155
  PATCH: 56,
151
156
  // destPc, sliceStart, sliceEnd
152
157
 
153
- // ── Debug ─────────────────────────────────────────────────────────────────
158
+ // Debug
154
159
  DEBUGGER: 57,
155
- // ── Indirect jump (register-addressed) ───────────────────────────────────
156
- // Used by the jumpDispatcher pass. The target PC is read from a register
160
+ // Indirect jump (register-addressed)
161
+ // Used by Dispatcher pass. The target PC is read from a register
157
162
  // rather than encoded as a bytecode immediate, so static analysis cannot
158
163
  // determine the destination without tracking register values at runtime.
159
- 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
160
176
  };
161
177
 
162
- // ── Scope ─────────────────────────────────────────────────────────────────────
178
+ // Scope
163
179
  // Maps variable names to virtual RegisterOperands.
164
180
  // Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
165
- // resolveRegisters() assigns concrete slot indices before serialisation.
181
+ // resolveRegisters() assigns concrete slot indices before serialization.
166
182
  class Scope {
167
183
  constructor(parent = null) {
168
184
  this.parent = parent;
@@ -188,15 +204,15 @@ class Scope {
188
204
  }
189
205
  }
190
206
 
191
- // ── FnContext ─────────────────────────────────────────────────────────────────
207
+ // FnContext
192
208
  // Compiler-side state for the function currently being compiled.
193
209
  // Distinct from the runtime Frame — this is compile-time only.
194
210
  //
195
211
  // Virtual-register model (Lua/LLVM style):
196
- // Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
197
- // unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
198
- // liveness-aware slot assignment and sets desc.regCount at the end of the
199
- // pipeline, just like resolveLabels() fills in jump targets.
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.
200
216
  class FnContext {
201
217
  // index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
202
218
 
@@ -218,11 +234,11 @@ class FnContext {
218
234
  return b.registerOperand(this._nextId++, this._fnId);
219
235
  }
220
236
 
221
- /**
222
- * Allocate a short-lived temporary register (pool "temp::").
223
- * resolveRegisters() will reuse its concrete slot once its live range ends.
224
- * Do NOT use for named locals or upvalue-captured variables — use _newReg()
225
- * via scope.define() for those, so they stay in the stable "local::" pool.
237
+ /**
238
+ * Allocate a short-lived temporary register (pool "temp::").
239
+ * resolveRegisters() will reuse its concrete slot once its live range ends.
240
+ * Do NOT use for named locals or upvalue-captured variables — use _newReg()
241
+ * via scope.define() for those, so they stay in the stable "local::" pool.
226
242
  */
227
243
  allocReg() {
228
244
  return b.registerOperand(this._nextId++, this._fnId, {
@@ -230,17 +246,17 @@ class FnContext {
230
246
  });
231
247
  }
232
248
 
233
- /**
234
- * Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
235
- *
236
- * NOTE: This is extraneous for any programmatically generated IR.
237
- * resolveRegisters() already computes lastUse as the last instruction index
238
- * where the register appears as a real operand — which is always the tightest
239
- * correct bound when you stop emitting a register after its last logical use.
240
- * freeReg is only needed in the rare case where a register has a late syntactic
241
- * appearance that does NOT represent its true logical death (e.g. a dummy read
242
- * emitted for side-effects long after the value is logically dead). No current
243
- * pass in this codebase uses it; it is kept as an extension point only.
249
+ /**
250
+ * Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
251
+ *
252
+ * NOTE: This is extraneous for any programmatically generated IR.
253
+ * resolveRegisters() already computes lastUse as the last instruction index
254
+ * where the register appears as a real operand — which is always the tightest
255
+ * correct bound when you stop emitting a register after its last logical use.
256
+ * freeReg is only needed in the rare case where a register has a late syntactic
257
+ * appearance that does NOT represent its true logical death (e.g. a dummy read
258
+ * emitted for side-effects long after the value is logically dead). No current
259
+ * pass in this codebase uses it; it is kept as an extension point only.
244
260
  */
245
261
  freeReg(bc, reg) {
246
262
  bc.push([null, b.freeRegOperand(reg)]);
@@ -260,12 +276,13 @@ class FnContext {
260
276
  return idx;
261
277
  }
262
278
  }
263
- // ── Compiler ──────────────────────────────────────────────────────────────────
279
+ // Compiler
264
280
  export class Compiler {
265
- /** Internal variable slot registry.
266
- * globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
267
- * opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
268
-
281
+ log(...messages) {
282
+ if (this.options.verbose) {
283
+ console.log(...messages);
284
+ }
285
+ }
269
286
  _cloneRegisterOperand(operand) {
270
287
  if (!operand || typeof operand !== "object") return operand;
271
288
  if (operand.type !== "register") return operand;
@@ -294,10 +311,6 @@ export class Compiler {
294
311
  this.MICRO_OPS = {};
295
312
  this.SPECIALIZED_OPS = {};
296
313
  this.ALIASED_OPS = {};
297
- this._internals = {
298
- globally: new Map(),
299
- opcodes: new Map()
300
- };
301
314
  this.OP = {
302
315
  ...OP_ORIGINAL
303
316
  };
@@ -312,8 +325,16 @@ export class Compiler {
312
325
  this.OP[key] = val;
313
326
  }
314
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
+ };
315
336
  this.OP_NAME = Object.fromEntries(Object.entries(this.OP).map(([k, v]) => [v, k]));
316
- 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]);
317
338
  }
318
339
  _makeLabel(hint = "") {
319
340
  return `${hint || "L"}_${this._labelCount++}`;
@@ -344,12 +365,11 @@ export class Compiler {
344
365
  };
345
366
  }
346
367
 
347
- // ── Variable hoisting ──────────────────────────────────────────────────────
348
368
  // Pre-scan a statement list and reserve virtual registers for every var
349
369
  // declaration, function declaration, for-in iterator, and try-catch binding.
350
370
  // Must be called before any emit so that locals are allocated before temps.
351
371
  _hoistVars(stmts, scope, ctx) {
352
- for (const stmt of stmts) {
372
+ walkHoistScope(stmts, stmt => {
353
373
  switch (stmt.type) {
354
374
  case "VariableDeclaration":
355
375
  for (const decl of stmt.declarations) {
@@ -359,90 +379,77 @@ export class Compiler {
359
379
  case "FunctionDeclaration":
360
380
  if (stmt.id) scope.define(stmt.id.name, ctx);
361
381
  break;
362
- case "BlockStatement":
363
- this._hoistVars(stmt.body, scope, ctx);
364
- break;
365
- case "IfStatement":
366
- {
367
- const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
368
- this._hoistVars(cons, scope, ctx);
369
- if (stmt.alternate) {
370
- const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
371
- this._hoistVars(alt, scope, ctx);
372
- }
373
- break;
374
- }
375
- case "WhileStatement":
376
- case "DoWhileStatement":
377
- {
378
- const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
379
- this._hoistVars(body, scope, ctx);
380
- break;
381
- }
382
- case "ForStatement":
383
- {
384
- if (stmt.init?.type === "VariableDeclaration") {
385
- for (const decl of stmt.init.declarations) {
386
- if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
387
- }
388
- }
389
- const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
390
- this._hoistVars(body, scope, ctx);
391
- break;
392
- }
393
382
  case "ForInStatement":
394
- {
395
- // Reserve a hidden virtual register for the iterator object.
396
- stmt._iterSlot = ctx._newReg();
397
- if (stmt.left.type === "VariableDeclaration") {
398
- for (const decl of stmt.left.declarations) {
399
- if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
400
- }
401
- }
402
- const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
403
- this._hoistVars(body, scope, ctx);
404
- break;
405
- }
406
- case "SwitchStatement":
407
- 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();
408
385
  break;
409
386
  case "TryStatement":
410
- this._hoistVars(stmt.block.body, scope, ctx);
411
387
  if (stmt.handler) {
412
388
  if (stmt.handler.param?.type === "Identifier") {
413
- // Catch parameter IS the exception register.
414
389
  scope.define(stmt.handler.param.name, ctx);
415
390
  } else {
416
- // No catch binding – reserve a dummy virtual register for the exception value.
391
+ // No catch binding – reserve a dummy register for the exception value.
417
392
  stmt._exceptionSlot = ctx._newReg();
418
393
  }
419
- this._hoistVars(stmt.handler.body.body, scope, ctx);
420
394
  }
421
- break;
422
- case "LabeledStatement":
423
- 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
+ }
424
401
  break;
425
402
  }
426
- }
403
+ });
427
404
  }
428
405
 
429
- // ── Entry point ───────────────────────────────────────────────────────────
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;
414
+ }
415
+ profileData = {
416
+ transforms: {}
417
+ };
418
+
419
+ // Entry point
430
420
  compile(source) {
421
+ let startedAt = now();
431
422
  const ast = parse(source, {
432
423
  sourceType: "script",
433
424
  allowReturnOutsideFunction: true
434
425
  });
426
+ this.profileData.parseTime = now() - startedAt;
435
427
  return this.compileAST(ast);
436
428
  }
437
429
  compileAST(ast) {
430
+ let startedAt = now();
438
431
  this._compileMain(ast.program.body);
432
+ this.profileData.compileTime = now() - startedAt;
439
433
  return this.bytecode;
440
434
  }
441
435
 
442
- // ── Function compilation ───────────────────────────────────────────────────
436
+ // Function compilation
443
437
  _compileFunctionDecl(node) {
438
+ const isArrow = node.type === "ArrowFunctionExpression";
444
439
  ok(!node.generator, "Generator functions are not supported");
445
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";
446
453
  var fnIdx = this.fnDescriptors.length;
447
454
  const entryLabel = this._makeLabel(`fn_${fnIdx}`);
448
455
  var desc = {};
@@ -454,17 +461,54 @@ export class Compiler {
454
461
  this._loopStack = [];
455
462
 
456
463
  // 1. Define parameters as virtual registers (occupy the first IDs in order).
464
+ let hasRest = false;
457
465
  for (const param of node.params) {
458
- let identifier = param.type === "AssignmentPattern" ? param.left : param;
459
- ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
460
- ctx.scope.define(identifier.name, ctx);
466
+ if (param.type === "RestElement") {
467
+ ok(param.argument.type === "Identifier", "Rest element must be a simple identifier");
468
+ hasRest = true;
469
+ ctx.scope.define(param.argument.name, ctx);
470
+ } else {
471
+ let identifier = param.type === "AssignmentPattern" ? param.left : param;
472
+ ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
473
+ ctx.scope.define(identifier.name, ctx);
474
+ }
461
475
  }
462
476
 
463
- // 2. Reserve the `arguments` virtual register (immediately after params).
464
- 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
+ }
465
491
 
466
492
  // 3. Hoist all var declarations so locals are allocated before any temps.
467
- 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
+ }
468
512
 
469
513
  // 5. Emit default-value guards.
470
514
  for (const param of node.params) {
@@ -494,14 +538,20 @@ export class Compiler {
494
538
  }
495
539
 
496
540
  // 6. Compile body.
497
- for (const stmt of node.body.body) {
498
- this._compileStatement(stmt, ctx.scope, ctx.bc);
499
- }
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
+ }
500
549
 
501
- // Implicit return undefined at end of function.
502
- const reg_undef = ctx.allocReg();
503
- this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], node);
504
- 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
+ }
505
555
  this._currentCtx = savedCtx;
506
556
  this._loopStack = savedLoopStack;
507
557
  node._fnIdx = fnIdx;
@@ -510,6 +560,14 @@ export class Compiler {
510
560
  desc.bytecode = ctx.bc;
511
561
  desc._fnIdx = fnIdx;
512
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);
570
+ desc.hasRest = hasRest;
513
571
  // regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
514
572
  desc.upvalues = ctx.upvalues.slice();
515
573
  desc.ctx = ctx;
@@ -532,11 +590,98 @@ export class Compiler {
532
590
  label: desc.entryLabel
533
591
  }, desc.paramCount, b.fnRegCountOperand(desc._fnIdx),
534
592
  // resolved by resolveRegisters()
535
- desc.upvalues.length, ...uvOperands], node);
593
+ desc.upvalues.length, desc.hasRest ? 1 : 0,
594
+ // 1 = last param is a rest element
595
+ ...uvOperands], node);
536
596
  return dst;
537
597
  }
538
598
 
539
- // ── 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)
540
685
  _compileMain(body) {
541
686
  const mainCtx = new FnContext(this, null);
542
687
  const savedCtx = this._currentCtx;
@@ -564,7 +709,7 @@ export class Compiler {
564
709
  this._currentCtx = savedCtx;
565
710
  }
566
711
 
567
- // ── Statements ────────────────────────────────────────────────────────────
712
+ // Statements
568
713
  // Wrapper that resets temps after every statement so that short-lived
569
714
  // expression temps don't accumulate across statements.
570
715
  _compileStatement(node, scope, bc) {
@@ -586,6 +731,8 @@ export class Compiler {
586
731
  break;
587
732
  case "FunctionDeclaration":
588
733
  {
734
+ // Already hoisted and emitted at function entry — skip.
735
+ if (node._hoistedDesc) break;
589
736
  const desc = this._compileFunctionDecl(node);
590
737
  const closureReg = this._emitMakeClosure(desc, node, bc);
591
738
  if (scope) {
@@ -613,12 +760,12 @@ export class Compiler {
613
760
  reg = ctx.allocReg();
614
761
  this.emit(bc, [this.OP.LOAD_CONST, reg, b.constantOperand(undefined)], node);
615
762
  }
616
- for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
617
- if (this._loopStack[_ri].type === "try") {
618
- this.emit(bc, [this.OP.TRY_END], node);
619
- }
620
- }
621
- 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
+ });
622
769
  break;
623
770
  }
624
771
  case "ExpressionStatement":
@@ -838,22 +985,19 @@ export class Compiler {
838
985
  if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
839
986
  } else {
840
987
  for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
841
- if (this._loopStack[_bi].type !== "try") {
988
+ const _t = this._loopStack[_bi].type;
989
+ if (_t !== "try" && _t !== "finally") {
842
990
  _bTargetIdx = _bi;
843
991
  break;
844
992
  }
845
993
  }
846
994
  if (_bTargetIdx === -1) throw new Error("break outside loop");
847
995
  }
848
- for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
849
- if (this._loopStack[_bi].type === "try") {
850
- this.emit(bc, [this.OP.TRY_END], node);
851
- }
852
- }
853
- this.emit(bc, [this.OP.JUMP, {
854
- type: "label",
855
- label: this._loopStack[_bTargetIdx].breakLabel
856
- }], 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
+ });
857
1001
  break;
858
1002
  }
859
1003
  case "ContinueStatement":
@@ -877,15 +1021,11 @@ export class Compiler {
877
1021
  }
878
1022
  if (_cTargetIdx === -1) throw new Error("continue outside loop");
879
1023
  }
880
- for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
881
- if (this._loopStack[_ci].type === "try") {
882
- this.emit(bc, [this.OP.TRY_END], node);
883
- }
884
- }
885
- this.emit(bc, [this.OP.JUMP, {
886
- type: "label",
887
- label: this._loopStack[_cTargetIdx].continueLabel
888
- }], 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
+ });
889
1029
  break;
890
1030
  }
891
1031
  case "SwitchStatement":
@@ -1048,51 +1188,139 @@ export class Compiler {
1048
1188
  }
1049
1189
  case "TryStatement":
1050
1190
  {
1051
- if (node.finalizer) {
1052
- 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");
1053
1193
  }
1054
- if (!node.handler) {
1055
- 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;
1056
1251
  }
1057
- const catchLabel = this._makeLabel("catch");
1058
- const afterCatchLabel = this._makeLabel("after_catch");
1059
1252
 
1060
- // Determine where the caught exception is written.
1061
- const exceptionReg = node.handler.param?.type === "Identifier" ? scope?._locals.get(node.handler.param.name) ?? ctx.allocReg() // shouldn't normally reach here
1062
- : node._exceptionSlot;
1063
- 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, {
1064
1262
  type: "label",
1065
- label: catchLabel
1066
- }, exceptionReg], node);
1067
- this._loopStack.push({
1068
- type: "try",
1263
+ label: finallyLabel
1264
+ }, contReg, payloadReg, {
1265
+ type: "label",
1266
+ label: throwPadLabel
1267
+ }], node);
1268
+ const finallyEntry = {
1269
+ type: "finally",
1069
1270
  label: null,
1070
1271
  breakLabel: "",
1071
- continueLabel: ""
1072
- });
1073
- for (const stmt of node.block.body) {
1074
- this._compileStatement(stmt, scope, bc);
1075
- }
1076
- 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.
1077
1284
  this.emit(bc, [this.OP.TRY_END], node);
1285
+ this._emitLoadLabel(bc, contReg, afterFinallyLabel, node);
1078
1286
  this.emit(bc, [this.OP.JUMP, {
1079
1287
  type: "label",
1080
- label: afterCatchLabel
1288
+ label: finallyLabel
1081
1289
  }], node);
1082
1290
 
1083
- // 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).
1084
1294
  this.emit(bc, [null, {
1085
1295
  type: "defineLabel",
1086
- label: catchLabel
1296
+ label: finallyLabel
1087
1297
  }], node);
1088
-
1089
- // If no param binding, just ignore the exception (it's in the dummy slot).
1090
- for (const stmt of node.handler.body.body) {
1298
+ for (const stmt of node.finalizer.body) {
1091
1299
  this._compileStatement(stmt, scope, bc);
1092
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.
1305
+ this.emit(bc, [null, {
1306
+ type: "defineLabel",
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
+ }
1093
1321
  this.emit(bc, [null, {
1094
1322
  type: "defineLabel",
1095
- label: afterCatchLabel
1323
+ label: afterFinallyLabel
1096
1324
  }], node);
1097
1325
  break;
1098
1326
  }
@@ -1104,7 +1332,65 @@ export class Compiler {
1104
1332
  }
1105
1333
  }
1106
1334
 
1107
- // ── 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
1108
1394
  // Returns the virtual RegisterOperand that holds the result.
1109
1395
  // For local variables: returns their RegisterOperand directly (no instruction emitted).
1110
1396
  // For all others: allocates a fresh virtual register, emits the instruction(s),
@@ -1124,6 +1410,19 @@ export class Compiler {
1124
1410
 
1125
1411
  return dst;
1126
1412
  }
1413
+
1414
+ // _VM_JUMP_("labelName") — emits JUMP with a label operand.
1415
+ // Used by bytecode transforms (e.g. CFF) via Template to express jumps
1416
+ // to labels that exist in the parent compiler's bytecode stream.
1417
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_JUMP_") {
1418
+ const label = node.arguments[0].value;
1419
+ bc.push([this.OP.JUMP, {
1420
+ type: "label",
1421
+ label
1422
+ }]);
1423
+ // Return a dummy register — caller (ExpressionStatement) discards it.
1424
+ return ctx.allocReg();
1425
+ }
1127
1426
  switch (node.type) {
1128
1427
  case "NumericLiteral":
1129
1428
  case "StringLiteral":
@@ -1155,16 +1454,37 @@ export class Compiler {
1155
1454
  }
1156
1455
  case "ThisExpression":
1157
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.
1158
1471
  const dst = ctx.allocReg();
1159
1472
  this.emit(bc, [this.OP.LOAD_THIS, dst], node);
1160
1473
  return dst;
1161
1474
  }
1162
1475
  case "NewExpression":
1163
1476
  {
1164
- const calleeReg = this._compileExpr(node.callee, scope, bc);
1165
- 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);
1166
1480
  const dst = ctx.allocReg();
1167
- 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
+ }
1168
1488
  return dst;
1169
1489
  }
1170
1490
  case "SequenceExpression":
@@ -1216,17 +1536,32 @@ export class Compiler {
1216
1536
  const n = node;
1217
1537
  const endLabel = this._makeLabel("logical_end");
1218
1538
  const isOr = n.operator === "||";
1219
- 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}`);
1220
1541
  const lhsReg = this._compileExpr(n.left, scope, bc);
1221
1542
  const reg_result = ctx.allocReg();
1222
1543
  if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
1223
-
1224
- // For ||: if truthy keep LHS, jump past RHS.
1225
- // For &&: if falsy keep LHS, jump past RHS.
1226
- this.emit(bc, [isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE, reg_result, {
1227
- type: "label",
1228
- label: endLabel
1229
- }], 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
+ }
1230
1565
 
1231
1566
  // Compile RHS into reg_result.
1232
1567
  const rhsReg = this._compileExpr(n.right, scope, bc);
@@ -1268,6 +1603,7 @@ export class Compiler {
1268
1603
  "*": this.OP.MUL,
1269
1604
  "/": this.OP.DIV,
1270
1605
  "%": this.OP.MOD,
1606
+ "**": this.OP.EXP,
1271
1607
  "&": this.OP.BAND,
1272
1608
  "|": this.OP.BOR,
1273
1609
  "^": this.OP.BXOR,
@@ -1356,6 +1692,7 @@ export class Compiler {
1356
1692
  "*=": this.OP.MUL,
1357
1693
  "/=": this.OP.DIV,
1358
1694
  "%=": this.OP.MOD,
1695
+ "**=": this.OP.EXP,
1359
1696
  "&=": this.OP.BAND,
1360
1697
  "|=": this.OP.BOR,
1361
1698
  "^=": this.OP.BXOR,
@@ -1428,6 +1765,7 @@ export class Compiler {
1428
1765
  case "CallExpression":
1429
1766
  {
1430
1767
  const n = node;
1768
+ ok(n.arguments.length < U16_MAX, `Too many arguments (max ${U16_MAX - 1})`);
1431
1769
  if (n.callee.type === "MemberExpression") {
1432
1770
  // Method call: receiver.method(args)
1433
1771
  const receiverReg = this._compileExpr(n.callee.object, scope, bc);
@@ -1440,16 +1778,26 @@ export class Compiler {
1440
1778
  }
1441
1779
  const calleeReg = ctx.allocReg();
1442
1780
  this.emit(bc, [this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg], node);
1443
- const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1444
1781
  const dst = ctx.allocReg();
1445
- 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
+ }
1446
1789
  return dst;
1447
1790
  } else {
1448
1791
  // Plain call: fn(args)
1449
1792
  const calleeReg = this._compileExpr(n.callee, scope, bc);
1450
- const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
1451
1793
  const dst = ctx.allocReg();
1452
- 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
+ }
1453
1801
  return dst;
1454
1802
  }
1455
1803
  }
@@ -1524,6 +1872,14 @@ export class Compiler {
1524
1872
  const desc = this._compileFunctionDecl(node);
1525
1873
  return this._emitMakeClosure(desc, node, bc);
1526
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
+ }
1527
1883
  case "MemberExpression":
1528
1884
  {
1529
1885
  const n = node;
@@ -1542,6 +1898,9 @@ export class Compiler {
1542
1898
  case "ArrayExpression":
1543
1899
  {
1544
1900
  const n = node;
1901
+ if (this._hasSpread(n.elements)) {
1902
+ return this._buildSpreadArgs(n.elements, scope, bc, node);
1903
+ }
1545
1904
  const elemRegs = n.elements.map(el => {
1546
1905
  if (el === null) {
1547
1906
  const r = ctx.allocReg();
@@ -1557,45 +1916,87 @@ export class Compiler {
1557
1916
  case "ObjectExpression":
1558
1917
  {
1559
1918
  const n = node;
1560
- const regularProps = [];
1561
- const accessorProps = [];
1562
- for (const prop of n.properties) {
1563
- if (prop.type === "SpreadElement") throw new Error("Object spread not supported");
1564
- if (prop.type === "ObjectMethod") {
1565
- if (prop.kind === "get" || prop.kind === "set") {
1566
- if (prop.computed) throw new Error("Computed getter/setter keys are not supported");
1567
- accessorProps.push(prop);
1568
- } else {
1569
- throw new Error("Shorthand method syntax is not supported");
1570
- }
1571
- } else {
1572
- 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);
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);
1573
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;
1574
1953
  }
1575
1954
 
1576
- // Build flat [key, val, key, val, …] register list.
1577
- const pairRegs = [];
1578
- for (const prop of regularProps) {
1579
- let keyStr;
1580
- const key = prop.key;
1581
- 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}`);
1582
- const keyReg = ctx.allocReg();
1583
- this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1584
- const valReg = this._compileExpr(prop.value, scope, bc);
1585
- pairRegs.push(keyReg, valReg);
1586
- }
1955
+ // General path: handles spread elements, computed keys, and method shorthands.
1956
+ // Builds an empty object then sets each property in source order.
1587
1957
  const dst = ctx.allocReg();
1588
- this.emit(bc, [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs], node);
1589
-
1590
- // Define accessors on the object now sitting in `dst`.
1591
- for (const prop of accessorProps) {
1592
- const key = prop.key;
1593
- let keyStr;
1594
- 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}`);
1595
- const keyReg = ctx.allocReg();
1596
- this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
1597
- const fnReg = this._emitMakeClosure(this._compileFunctionDecl(prop), prop, bc);
1598
- 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
+ }
1599
2000
  }
1600
2001
  return dst;
1601
2002
  }
@@ -1607,7 +2008,7 @@ export class Compiler {
1607
2008
  }
1608
2009
  }
1609
2010
 
1610
- // ── Serializer ────────────────────────────────────────────────────────────────
2011
+ // Serializer
1611
2012
  class Serializer {
1612
2013
  constructor(compiler) {
1613
2014
  this.compiler = compiler;
@@ -1774,9 +2175,9 @@ class Serializer {
1774
2175
  // Validate no opcode or operand exceeds u16 limit
1775
2176
  for (const o of resolvedValues) {
1776
2177
  ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
1777
- ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
2178
+ ok(o >= 0 && o <= 0xffffffff, `Operand overflow (max 0xFFFFFFFF u32): ${o}`);
1778
2179
  }
1779
- ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
2180
+ ok(op >= 0 && op <= 0xffffffff, `Opcode overflow (max 0xFFFFFFFF u32): ${op}`);
1780
2181
  serialized.push(instr);
1781
2182
  }
1782
2183
  return {
@@ -1784,10 +2185,12 @@ class Serializer {
1784
2185
  };
1785
2186
  }
1786
2187
  _encodeBytecode(flat) {
1787
- const buf = new Uint8Array(flat.length * 2);
2188
+ const buf = new Uint8Array(flat.length * 4);
1788
2189
  flat.forEach((w, i) => {
1789
- buf[i * 2] = w & 0xff;
1790
- buf[i * 2 + 1] = w >>> 8 & 0xff;
2190
+ buf[i * 4] = w & 0xff;
2191
+ buf[i * 4 + 1] = w >>> 8 & 0xff;
2192
+ buf[i * 4 + 2] = w >>> 16 & 0xff;
2193
+ buf[i * 4 + 3] = w >>> 24 & 0xff;
1791
2194
  });
1792
2195
  return Buffer.from(buf).toString("base64");
1793
2196
  }
@@ -1813,6 +2216,8 @@ class Serializer {
1813
2216
  sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
1814
2217
  const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
1815
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};`);
1816
2221
  initBody.push(this._serializeConstants(constants));
1817
2222
  sections = [...initBody, ...sections];
1818
2223
  sections.push(VM_RUNTIME);
@@ -1820,33 +2225,71 @@ class Serializer {
1820
2225
  }
1821
2226
  }
1822
2227
  export async function compileAndSerialize(sourceCode, options) {
2228
+ let obfuscationStartedAt = now();
1823
2229
  const compiler = new Compiler(options);
1824
2230
  let bytecode = compiler.compile(sourceCode);
2231
+ const passes = [];
2232
+ if (options.stringConcealing) {
2233
+ passes.push({
2234
+ pass: stringConcealing,
2235
+ name: "stringConcealing"
2236
+ });
2237
+ }
1825
2238
 
1826
- // jumpDispatcher must run before resolveRegisters so that the new rDisp/rKey
1827
- // RegisterOperand objects it injects are visible to the liveness analysis.
1828
- // It must also run before resolveLabels since it emits encodedLabel IR operands.
2239
+ // CFF and Dispatcher both run before resolveRegisters and resolveLabels
2240
+ if (options.controlFlowFlattening) {
2241
+ passes.push({
2242
+ pass: controlFlowFlattening,
2243
+ name: "controlFlowFlattening"
2244
+ });
2245
+ }
1829
2246
  if (options.dispatcher) {
1830
- const dispatcherResult = dispatcher(bytecode, compiler);
1831
- bytecode = dispatcherResult.bytecode;
2247
+ passes.push({
2248
+ pass: dispatcher,
2249
+ name: "dispatcher"
2250
+ });
1832
2251
  }
1833
- const passes = [];
1834
- passes.push(concealConstants);
2252
+ passes.push({
2253
+ pass: concealConstants,
2254
+ name: "concealConstants"
2255
+ });
1835
2256
  if (options.specializedOpcodes) {
1836
- passes.push(specializedOpcodes);
1837
- }
1838
- if (options.microOpcodes) {
1839
- passes.push(microOpcodes);
2257
+ passes.push({
2258
+ pass: specializedOpcodes,
2259
+ name: "specializedOpcodes"
2260
+ });
1840
2261
  }
1841
2262
  if (options.macroOpcodes) {
1842
- passes.push(macroOpcodes);
2263
+ passes.push({
2264
+ pass: macroOpcodes,
2265
+ name: "macroOpcodes"
2266
+ });
1843
2267
  }
1844
2268
  if (options.aliasedOpcodes) {
1845
- passes.push(aliasedOpcodes);
2269
+ passes.push({
2270
+ pass: aliasedOpcodes,
2271
+ name: "aliasedOpcodes"
2272
+ });
1846
2273
  }
1847
- for (const pass of passes) {
2274
+ const timings = {};
2275
+ function runAndTime(pass, name) {
2276
+ const startedAt = now();
2277
+ compiler.log(`Running bytecode pass ${name}...`);
1848
2278
  const passResult = pass(bytecode, compiler);
1849
2279
  bytecode = passResult.bytecode;
2280
+ const endedAt = now();
2281
+ const elapsedMs = endedAt - startedAt;
2282
+ timings[name] = elapsedMs;
2283
+ compiler.profileData.transforms[name] = {
2284
+ transformTime: elapsedMs,
2285
+ bytecodeSize: bytecode.length,
2286
+ flatBytecodeSize: bytecode.flat().length
2287
+ };
2288
+ compiler.log(`Bytecode pass ${name} completed in ${Math.floor(elapsedMs)}ms`);
2289
+ return passResult;
2290
+ }
2291
+ for (const pass of passes) {
2292
+ runAndTime(pass.pass, pass.name);
1850
2293
  }
1851
2294
 
1852
2295
  // Resolve virtual registers to concrete slot indices and set regCount per fn.
@@ -1854,32 +2297,36 @@ export async function compileAndSerialize(sourceCode, options) {
1854
2297
  // of the bytecode while leaving RETURN in place, splitting a function's code
1855
2298
  // into two non-contiguous regions. Linear-scan liveness then sees incorrect
1856
2299
  // firstUse/lastUse for registers that span the gap, causing slot collisions.
1857
- const regsResult = resolveRegisters(bytecode, compiler);
2300
+ const regsResult = runAndTime(resolveRegisters, "resolveRegisters");
1858
2301
  bytecode = regsResult.bytecode;
1859
2302
 
1860
2303
  // selfModifying runs after register resolution so concrete slot indices are
1861
2304
  // already in place; only label operands remain unresolved at this stage.
1862
2305
  if (options.selfModifying) {
1863
- const smResult = selfModifying(bytecode, compiler);
2306
+ const smResult = runAndTime(selfModifying, "selfModifying");
1864
2307
  bytecode = smResult.bytecode;
1865
2308
  }
1866
2309
 
1867
2310
  // Resolve label references to flat bytecode indices.
1868
- const labelsResult = resolveLabels(bytecode, compiler);
2311
+ const labelsResult = runAndTime(resolveLabels, "resolveLabels");
1869
2312
  bytecode = labelsResult.bytecode;
1870
2313
 
1871
2314
  // Set mainStartPc from the first function descriptor (or 0 for top-level start).
1872
2315
  compiler.mainStartPc = compiler.mainFn.startPc;
1873
2316
 
1874
2317
  // Resolve constant references to pool indices (+ conceal key operand).
1875
- const constResult = resolveConstants(bytecode, compiler);
2318
+ const constResult = runAndTime(resolveConstants, "resolveConstants");
1876
2319
  bytecode = constResult.bytecode;
1877
2320
  compiler.constants = constResult.constants;
1878
2321
 
1879
2322
  // Build and obfuscate the runtime.
1880
2323
  const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
1881
2324
 
1882
- // This part was purposefully pulled out Serializer as OP_NAME's get resolved during obfuscateRuntime
2325
+ // for (const key of Object.keys(timings)) {
2326
+ // console.log(` ${key}: ${timings[key]}ms`);
2327
+ // }
2328
+
2329
+ // This part was purposefully pulled out Serializer as OP_NAME's get resolved during buildRuntime
1883
2330
  // So for the most useful comments, it's ran absolutely last
1884
2331
  // Tests also rely on correct comments so it's required
1885
2332
  const generateBytecodeComment = () => {
@@ -1890,8 +2337,10 @@ export async function compileAndSerialize(sourceCode, options) {
1890
2337
  }
1891
2338
  return lines.join("\n");
1892
2339
  };
1893
- const code = await obfuscateRuntime(runtimeSource, bytecode, options, compiler, generateBytecodeComment);
2340
+ const code = await buildRuntime(runtimeSource, bytecode, options, compiler, generateBytecodeComment);
2341
+ compiler.profileData.obfuscationTime = now() - obfuscationStartedAt;
1894
2342
  return {
1895
- code
2343
+ code,
2344
+ profileData: compiler.profileData
1896
2345
  };
1897
2346
  }