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