js-confuser-vm 0.0.8 → 0.1.0
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/.gitmodules +4 -0
- package/CHANGELOG.md +102 -2
- package/README.md +95 -1
- package/dist/compiler.js +225 -152
- package/dist/runtime.js +200 -143
- package/dist/template.js +142 -0
- package/dist/transforms/bytecode/dispatcher.js +362 -0
- package/dist/transforms/bytecode/macroOpcodes.js +1 -1
- package/dist/transforms/bytecode/resolveLabels.js +21 -18
- package/dist/transforms/bytecode/resolveRegisters.js +212 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +4 -2
- package/dist/types.js +41 -0
- package/dist/utils/op-utils.js +1 -0
- package/index.ts +1 -0
- package/jest.config.js +5 -0
- package/package.json +10 -2
- package/src/compiler.ts +291 -180
- package/src/options.ts +1 -0
- package/src/runtime.ts +222 -141
- package/src/template.ts +141 -0
- package/src/transforms/bytecode/aliasedOpcodes.ts +1 -1
- package/src/transforms/bytecode/dispatcher.ts +398 -0
- package/src/transforms/bytecode/macroOpcodes.ts +2 -2
- package/src/transforms/bytecode/resolveLabels.ts +31 -27
- package/src/transforms/bytecode/resolveRegisters.ts +221 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +5 -9
- package/src/types.ts +64 -4
- package/src/utils/op-utils.ts +2 -0
- package/dist/transforms/utils/op-utils.js +0 -25
- package/dist/transforms/utils/random-utils.js +0 -27
- package/dist/utilts.js +0 -3
package/dist/compiler.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ok } from "assert";
|
|
|
8
8
|
import { obfuscateRuntime } from "./build-runtime.js";
|
|
9
9
|
import { DEFAULT_OPTIONS } from "./options.js";
|
|
10
10
|
import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
|
|
11
|
+
import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.js";
|
|
11
12
|
import { resolveConstants } from "./transforms/bytecode/resolveContants.js";
|
|
12
13
|
import { selfModifying } from "./transforms/bytecode/selfModifying.js";
|
|
13
14
|
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
|
|
@@ -17,8 +18,9 @@ import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
|
|
|
17
18
|
import { getRandomInt } from "./utils/random-utils.js";
|
|
18
19
|
import { U16_MAX } from "./utils/op-utils.js";
|
|
19
20
|
import { concealConstants } from "./transforms/bytecode/concealConstants.js";
|
|
21
|
+
import { dispatcher } from "./transforms/bytecode/dispatcher.js";
|
|
20
22
|
const traverse = traverseImport.default || traverseImport;
|
|
21
|
-
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue\n// While the outer frame is alive: reads/writes go to frame.regs[slot].\n// After the outer frame returns (closed): reads/writes hit this._value.\nfunction Upvalue(frame, slot) {\n this._frame = frame;\n this._slot = slot;\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._frame.regs[this._slot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._frame.regs[this._slot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._frame.regs[this._slot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg) {\n this.closure = closure;\n this.regs = new Array(closure.fn.regCount).fill(undefined);\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0; // register in parent to write return value\n this._newObj = null; // <- set by NEW so RETURN can see it\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc, // <- where main begins\n };\n this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0);\n this._internals = {};\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Reuse existing open upvalue for this frame+slot if one exists.\n // This is what makes two closures share the same mutable cell.\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (uv._frame === frame && uv._slot === slot) return uv;\n }\n var uv = new Upvalue(frame, slot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN - close every upvalue that was pointing into this frame.\n // After this, closures that captured from the frame read from upvalue.value.\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (uv._frame === frame) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n frame.regs.fill(undefined);\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n frame.regs[dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n frame.regs[dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n frame.regs[dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n frame.regs[dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n frame.regs[dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n frame.regs[dst] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(frame.regs[this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a + frame.regs[this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a - frame.regs[this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a * frame.regs[this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a / frame.regs[this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a % frame.regs[this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a & frame.regs[this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a | frame.regs[this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a ^ frame.regs[this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a << frame.regs[this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >> frame.regs[this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >>> frame.regs[this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a < frame.regs[this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a > frame.regs[this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a <= frame.regs[this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >= frame.regs[this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a === frame.regs[this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a !== frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a == frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a != frame.regs[this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a in frame.regs[this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var ctor = frame.regs[this._operand()];\n if (typeof ctor === \"function\") {\n frame.regs[dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n frame.regs[dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n frame.regs[dst] = -frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n frame.regs[dst] = +frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n frame.regs[dst] = !frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n frame.regs[dst] = ~frame.regs[this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n frame.regs[dst] = typeof frame.regs[this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n frame.regs[dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n frame.regs[dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!frame.regs[src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (frame.regs[src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, this.globals, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = frame.regs[this._operand()];\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, receiver, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var newObj = Object.create(c.prototype || null);\n var f = new Frame(c, frame._pc, frame, newObj, dst);\n f._newObj = newObj;\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n frame.regs[dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = frame.regs[this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n parentFrame.regs[frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw frame.regs[this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n );\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n sub._currentFrame = f;\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n frame.regs[dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = frame.regs[this._operand()];\n frame.regs[dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n o[key] = val;\n }\n frame.regs[dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var getterFn = frame.regs[this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var setterFn = frame.regs[this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n frame.regs[dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = frame.regs[this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n frame.regs[dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n handledFrame.regs[h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals.window = window;\n for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
|
|
23
|
+
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue \u2014 Lua/CPython style.\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\n// _absSlot is the absolute index in VM._regs (frame._base + local slot).\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\n// Frame \u2014 analogous to Lua CallInfo / CPython PyFrameObject.\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // \u2500\u2500 Flat register file (Lua-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // All frames share a single array. Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n this._internals = {};\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var ctor = regs[base + this._operand()];\n if (typeof ctor === \"function\") {\n regs[base + dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n regs[base + dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n for (var i = 0; i < args.length && i < c.fn.regCount; i++) {\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: target PC is read from a register rather than a\n // bytecode immediate. Used by the jumpDispatcher pass so that static\n // analysis cannot determine the jump destination without tracking the\n // register value (which contains an encoded PC resolved at runtime).\n frame._pc = regs[base + this._operand()];\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n this._regs[handledFrame._base + h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals.window = window;\n for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
|
|
22
24
|
export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
23
25
|
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
|
|
24
26
|
|
|
@@ -149,21 +151,26 @@ export const OP_ORIGINAL = {
|
|
|
149
151
|
// destPc, sliceStart, sliceEnd
|
|
150
152
|
|
|
151
153
|
// ── Debug ─────────────────────────────────────────────────────────────────
|
|
152
|
-
DEBUGGER: 57
|
|
154
|
+
DEBUGGER: 57,
|
|
155
|
+
// ── Indirect jump (register-addressed) ───────────────────────────────────
|
|
156
|
+
// Used by the jumpDispatcher pass. The target PC is read from a register
|
|
157
|
+
// rather than encoded as a bytecode immediate, so static analysis cannot
|
|
158
|
+
// determine the destination without tracking register values at runtime.
|
|
159
|
+
JUMP_REG: 58 // src — frame._pc = regs[src]
|
|
153
160
|
};
|
|
154
161
|
|
|
155
162
|
// ── Scope ─────────────────────────────────────────────────────────────────────
|
|
156
|
-
// Maps variable names to
|
|
157
|
-
// Locals are allocated at compile time; zero name lookups at runtime.
|
|
163
|
+
// Maps variable names to virtual RegisterOperands.
|
|
164
|
+
// Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
|
|
165
|
+
// resolveRegisters() assigns concrete slot indices before serialisation.
|
|
158
166
|
class Scope {
|
|
159
167
|
constructor(parent = null) {
|
|
160
168
|
this.parent = parent;
|
|
161
169
|
this._locals = new Map();
|
|
162
|
-
this._next = 0;
|
|
163
170
|
}
|
|
164
|
-
define(name) {
|
|
171
|
+
define(name, ctx) {
|
|
165
172
|
if (!this._locals.has(name)) {
|
|
166
|
-
this._locals.set(name,
|
|
173
|
+
this._locals.set(name, ctx._newReg());
|
|
167
174
|
}
|
|
168
175
|
return this._locals.get(name);
|
|
169
176
|
}
|
|
@@ -171,7 +178,7 @@ class Scope {
|
|
|
171
178
|
if (this._locals.has(name)) {
|
|
172
179
|
return {
|
|
173
180
|
kind: "local",
|
|
174
|
-
|
|
181
|
+
reg: this._locals.get(name)
|
|
175
182
|
};
|
|
176
183
|
}
|
|
177
184
|
if (this.parent) return this.parent.resolve(name);
|
|
@@ -179,38 +186,68 @@ class Scope {
|
|
|
179
186
|
kind: "global"
|
|
180
187
|
};
|
|
181
188
|
}
|
|
182
|
-
get localCount() {
|
|
183
|
-
return this._next;
|
|
184
|
-
}
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
// ── FnContext ─────────────────────────────────────────────────────────────────
|
|
188
192
|
// Compiler-side state for the function currently being compiled.
|
|
189
193
|
// Distinct from the runtime Frame — this is compile-time only.
|
|
194
|
+
//
|
|
195
|
+
// Virtual-register model (Lua/LLVM style):
|
|
196
|
+
// Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
|
|
197
|
+
// unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
|
|
198
|
+
// liveness-aware slot assignment and sets desc.regCount at the end of the
|
|
199
|
+
// pipeline, just like resolveLabels() fills in jump targets.
|
|
190
200
|
class FnContext {
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
// index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
|
|
202
|
+
|
|
203
|
+
// Unique ID for this function — matches the index in compiler.fnDescriptors.
|
|
204
|
+
|
|
205
|
+
// Monotonically increasing counter; each call to _newReg() bumps it.
|
|
206
|
+
_nextId = 0;
|
|
207
|
+
constructor(compiler, parentCtx = null, fnId = 0) {
|
|
196
208
|
this.compiler = compiler;
|
|
197
209
|
this.parentCtx = parentCtx;
|
|
198
210
|
this.scope = new Scope();
|
|
199
211
|
this.bc = [];
|
|
200
212
|
this.upvalues = [];
|
|
213
|
+
this._fnId = fnId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Create a new virtual register owned by this function. */
|
|
217
|
+
_newReg() {
|
|
218
|
+
return b.registerOperand(this._nextId++, this._fnId);
|
|
201
219
|
}
|
|
202
220
|
|
|
203
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Allocate a short-lived temporary register (pool "temp::").
|
|
223
|
+
* resolveRegisters() will reuse its concrete slot once its live range ends.
|
|
224
|
+
* Do NOT use for named locals or upvalue-captured variables — use _newReg()
|
|
225
|
+
* via scope.define() for those, so they stay in the stable "local::" pool.
|
|
226
|
+
*/
|
|
204
227
|
allocReg() {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
228
|
+
return b.registerOperand(this._nextId++, this._fnId, {
|
|
229
|
+
kind: "temp"
|
|
230
|
+
});
|
|
208
231
|
}
|
|
209
232
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
|
|
235
|
+
*
|
|
236
|
+
* NOTE: This is extraneous for any programmatically generated IR.
|
|
237
|
+
* resolveRegisters() already computes lastUse as the last instruction index
|
|
238
|
+
* where the register appears as a real operand — which is always the tightest
|
|
239
|
+
* correct bound when you stop emitting a register after its last logical use.
|
|
240
|
+
* freeReg is only needed in the rare case where a register has a late syntactic
|
|
241
|
+
* appearance that does NOT represent its true logical death (e.g. a dummy read
|
|
242
|
+
* emitted for side-effects long after the value is logically dead). No current
|
|
243
|
+
* pass in this codebase uses it; it is kept as an extension point only.
|
|
244
|
+
*/
|
|
245
|
+
freeReg(bc, reg) {
|
|
246
|
+
bc.push([null, b.freeRegOperand(reg)]);
|
|
213
247
|
}
|
|
248
|
+
|
|
249
|
+
/** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
|
|
250
|
+
resetTemps() {}
|
|
214
251
|
addUpvalue(name, isLocal, index) {
|
|
215
252
|
const existing = this.upvalues.findIndex(u => u.name === name);
|
|
216
253
|
if (existing !== -1) return existing;
|
|
@@ -229,7 +266,15 @@ export class Compiler {
|
|
|
229
266
|
* globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
|
|
230
267
|
* opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
|
|
231
268
|
|
|
269
|
+
_cloneRegisterOperand(operand) {
|
|
270
|
+
if (!operand || typeof operand !== "object") return operand;
|
|
271
|
+
if (operand.type !== "register") return operand;
|
|
272
|
+
return JSON.parse(JSON.stringify(operand));
|
|
273
|
+
}
|
|
232
274
|
emit(bc, instr, node) {
|
|
275
|
+
for (let i = 1; i < instr.length; i++) {
|
|
276
|
+
instr[i] = this._cloneRegisterOperand(instr[i]);
|
|
277
|
+
}
|
|
233
278
|
bc.push(instr);
|
|
234
279
|
instr[SOURCE_NODE_SYM] = node;
|
|
235
280
|
}
|
|
@@ -280,7 +325,7 @@ export class Compiler {
|
|
|
280
325
|
if (ctx.scope._locals.has(name)) {
|
|
281
326
|
return {
|
|
282
327
|
kind: "local",
|
|
283
|
-
|
|
328
|
+
reg: ctx.scope._locals.get(name)
|
|
284
329
|
};
|
|
285
330
|
}
|
|
286
331
|
if (!ctx.parentCtx) return {
|
|
@@ -291,7 +336,7 @@ export class Compiler {
|
|
|
291
336
|
kind: "global"
|
|
292
337
|
};
|
|
293
338
|
const isLocal = parentResult.kind === "local";
|
|
294
|
-
const index = isLocal ? parentResult.
|
|
339
|
+
const index = isLocal ? parentResult.reg : parentResult.index;
|
|
295
340
|
const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
|
|
296
341
|
return {
|
|
297
342
|
kind: "upvalue",
|
|
@@ -300,31 +345,30 @@ export class Compiler {
|
|
|
300
345
|
}
|
|
301
346
|
|
|
302
347
|
// ── Variable hoisting ──────────────────────────────────────────────────────
|
|
303
|
-
// Pre-scan a statement list and reserve
|
|
304
|
-
// function declaration, for-in iterator, and try-catch binding.
|
|
305
|
-
// Must be called before
|
|
306
|
-
|
|
307
|
-
_hoistVars(stmts, scope) {
|
|
348
|
+
// Pre-scan a statement list and reserve virtual registers for every var
|
|
349
|
+
// declaration, function declaration, for-in iterator, and try-catch binding.
|
|
350
|
+
// Must be called before any emit so that locals are allocated before temps.
|
|
351
|
+
_hoistVars(stmts, scope, ctx) {
|
|
308
352
|
for (const stmt of stmts) {
|
|
309
353
|
switch (stmt.type) {
|
|
310
354
|
case "VariableDeclaration":
|
|
311
355
|
for (const decl of stmt.declarations) {
|
|
312
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
356
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
313
357
|
}
|
|
314
358
|
break;
|
|
315
359
|
case "FunctionDeclaration":
|
|
316
|
-
if (stmt.id) scope.define(stmt.id.name);
|
|
360
|
+
if (stmt.id) scope.define(stmt.id.name, ctx);
|
|
317
361
|
break;
|
|
318
362
|
case "BlockStatement":
|
|
319
|
-
this._hoistVars(stmt.body, scope);
|
|
363
|
+
this._hoistVars(stmt.body, scope, ctx);
|
|
320
364
|
break;
|
|
321
365
|
case "IfStatement":
|
|
322
366
|
{
|
|
323
367
|
const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
|
|
324
|
-
this._hoistVars(cons, scope);
|
|
368
|
+
this._hoistVars(cons, scope, ctx);
|
|
325
369
|
if (stmt.alternate) {
|
|
326
370
|
const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
|
|
327
|
-
this._hoistVars(alt, scope);
|
|
371
|
+
this._hoistVars(alt, scope, ctx);
|
|
328
372
|
}
|
|
329
373
|
break;
|
|
330
374
|
}
|
|
@@ -332,51 +376,51 @@ export class Compiler {
|
|
|
332
376
|
case "DoWhileStatement":
|
|
333
377
|
{
|
|
334
378
|
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
335
|
-
this._hoistVars(body, scope);
|
|
379
|
+
this._hoistVars(body, scope, ctx);
|
|
336
380
|
break;
|
|
337
381
|
}
|
|
338
382
|
case "ForStatement":
|
|
339
383
|
{
|
|
340
384
|
if (stmt.init?.type === "VariableDeclaration") {
|
|
341
385
|
for (const decl of stmt.init.declarations) {
|
|
342
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
386
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
343
387
|
}
|
|
344
388
|
}
|
|
345
389
|
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
346
|
-
this._hoistVars(body, scope);
|
|
390
|
+
this._hoistVars(body, scope, ctx);
|
|
347
391
|
break;
|
|
348
392
|
}
|
|
349
393
|
case "ForInStatement":
|
|
350
394
|
{
|
|
351
|
-
// Reserve a hidden register for the iterator object.
|
|
352
|
-
stmt._iterSlot =
|
|
395
|
+
// Reserve a hidden virtual register for the iterator object.
|
|
396
|
+
stmt._iterSlot = ctx._newReg();
|
|
353
397
|
if (stmt.left.type === "VariableDeclaration") {
|
|
354
398
|
for (const decl of stmt.left.declarations) {
|
|
355
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
399
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
356
400
|
}
|
|
357
401
|
}
|
|
358
402
|
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
359
|
-
this._hoistVars(body, scope);
|
|
403
|
+
this._hoistVars(body, scope, ctx);
|
|
360
404
|
break;
|
|
361
405
|
}
|
|
362
406
|
case "SwitchStatement":
|
|
363
|
-
for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
|
|
407
|
+
for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
|
|
364
408
|
break;
|
|
365
409
|
case "TryStatement":
|
|
366
|
-
this._hoistVars(stmt.block.body, scope);
|
|
410
|
+
this._hoistVars(stmt.block.body, scope, ctx);
|
|
367
411
|
if (stmt.handler) {
|
|
368
412
|
if (stmt.handler.param?.type === "Identifier") {
|
|
369
413
|
// Catch parameter IS the exception register.
|
|
370
|
-
scope.define(stmt.handler.param.name);
|
|
414
|
+
scope.define(stmt.handler.param.name, ctx);
|
|
371
415
|
} else {
|
|
372
|
-
// No catch binding – reserve a dummy
|
|
373
|
-
stmt._exceptionSlot =
|
|
416
|
+
// No catch binding – reserve a dummy virtual register for the exception value.
|
|
417
|
+
stmt._exceptionSlot = ctx._newReg();
|
|
374
418
|
}
|
|
375
|
-
this._hoistVars(stmt.handler.body.body, scope);
|
|
419
|
+
this._hoistVars(stmt.handler.body.body, scope, ctx);
|
|
376
420
|
}
|
|
377
421
|
break;
|
|
378
422
|
case "LabeledStatement":
|
|
379
|
-
this._hoistVars([stmt.body], scope);
|
|
423
|
+
this._hoistVars([stmt.body], scope, ctx);
|
|
380
424
|
break;
|
|
381
425
|
}
|
|
382
426
|
}
|
|
@@ -385,7 +429,8 @@ export class Compiler {
|
|
|
385
429
|
// ── Entry point ───────────────────────────────────────────────────────────
|
|
386
430
|
compile(source) {
|
|
387
431
|
const ast = parse(source, {
|
|
388
|
-
sourceType: "script"
|
|
432
|
+
sourceType: "script",
|
|
433
|
+
allowReturnOutsideFunction: true
|
|
389
434
|
});
|
|
390
435
|
return this.compileAST(ast);
|
|
391
436
|
}
|
|
@@ -402,28 +447,24 @@ export class Compiler {
|
|
|
402
447
|
const entryLabel = this._makeLabel(`fn_${fnIdx}`);
|
|
403
448
|
var desc = {};
|
|
404
449
|
this.fnDescriptors.push(desc);
|
|
405
|
-
const ctx = new FnContext(this, this._currentCtx);
|
|
450
|
+
const ctx = new FnContext(this, this._currentCtx, fnIdx);
|
|
406
451
|
const savedCtx = this._currentCtx;
|
|
407
452
|
this._currentCtx = ctx;
|
|
408
453
|
const savedLoopStack = this._loopStack;
|
|
409
454
|
this._loopStack = [];
|
|
410
455
|
|
|
411
|
-
// 1. Define parameters (occupy
|
|
456
|
+
// 1. Define parameters as virtual registers (occupy the first IDs in order).
|
|
412
457
|
for (const param of node.params) {
|
|
413
458
|
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
414
459
|
ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
|
|
415
|
-
ctx.scope.define(identifier.name);
|
|
460
|
+
ctx.scope.define(identifier.name, ctx);
|
|
416
461
|
}
|
|
417
462
|
|
|
418
|
-
// 2. Reserve the `arguments`
|
|
419
|
-
ctx.scope.define("arguments");
|
|
463
|
+
// 2. Reserve the `arguments` virtual register (immediately after params).
|
|
464
|
+
ctx.scope.define("arguments", ctx);
|
|
420
465
|
|
|
421
|
-
// 3. Hoist all var declarations so
|
|
422
|
-
this._hoistVars(node.body.body, ctx.scope);
|
|
423
|
-
|
|
424
|
-
// 4. Temps now start above all locals.
|
|
425
|
-
ctx.regTop = ctx.scope.localCount;
|
|
426
|
-
ctx.maxRegTop = ctx.regTop;
|
|
466
|
+
// 3. Hoist all var declarations so locals are allocated before any temps.
|
|
467
|
+
this._hoistVars(node.body.body, ctx.scope, ctx);
|
|
427
468
|
|
|
428
469
|
// 5. Emit default-value guards.
|
|
429
470
|
for (const param of node.params) {
|
|
@@ -469,25 +510,29 @@ export class Compiler {
|
|
|
469
510
|
desc.bytecode = ctx.bc;
|
|
470
511
|
desc._fnIdx = fnIdx;
|
|
471
512
|
desc.paramCount = node.params.length;
|
|
472
|
-
|
|
513
|
+
// regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
|
|
473
514
|
desc.upvalues = ctx.upvalues.slice();
|
|
515
|
+
desc.ctx = ctx;
|
|
474
516
|
return desc;
|
|
475
517
|
}
|
|
476
518
|
|
|
477
519
|
// Emit MAKE_CLOSURE with all metadata as inline operands.
|
|
478
520
|
// Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
521
|
+
// regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
|
|
479
522
|
_emitMakeClosure(desc, node, bc) {
|
|
480
523
|
const ctx = this._currentCtx;
|
|
481
524
|
const dst = ctx.allocReg();
|
|
482
525
|
const uvOperands = [];
|
|
483
526
|
for (const uv of desc.upvalues) {
|
|
484
527
|
uvOperands.push(uv.isLocal ? 1 : 0);
|
|
485
|
-
uvOperands.push(uv.index);
|
|
528
|
+
uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
|
|
486
529
|
}
|
|
487
530
|
this.emit(bc, [this.OP.MAKE_CLOSURE, dst, {
|
|
488
531
|
type: "label",
|
|
489
532
|
label: desc.entryLabel
|
|
490
|
-
}, desc.paramCount,
|
|
533
|
+
}, desc.paramCount, b.fnRegCountOperand(desc._fnIdx),
|
|
534
|
+
// resolved by resolveRegisters()
|
|
535
|
+
desc.upvalues.length, ...uvOperands], node);
|
|
491
536
|
return dst;
|
|
492
537
|
}
|
|
493
538
|
|
|
@@ -501,7 +546,7 @@ export class Compiler {
|
|
|
501
546
|
async: false,
|
|
502
547
|
generator: false,
|
|
503
548
|
params: [],
|
|
504
|
-
id:
|
|
549
|
+
id: t.identifier("main"),
|
|
505
550
|
body: t.blockStatement([...body])
|
|
506
551
|
});
|
|
507
552
|
for (const descriptor of this.fnDescriptors) {
|
|
@@ -513,7 +558,8 @@ export class Compiler {
|
|
|
513
558
|
this.bytecode.push(instr);
|
|
514
559
|
}
|
|
515
560
|
}
|
|
516
|
-
|
|
561
|
+
|
|
562
|
+
// mainRegCount is set by resolveRegisters() after the pipeline runs.
|
|
517
563
|
this.mainFn = desc;
|
|
518
564
|
this._currentCtx = savedCtx;
|
|
519
565
|
}
|
|
@@ -592,14 +638,8 @@ export class Compiler {
|
|
|
592
638
|
this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
|
|
593
639
|
}
|
|
594
640
|
} else {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// Actually: just emit LOAD_CONST directly into slot.
|
|
598
|
-
// Undo the allocReg – instead emit directly:
|
|
599
|
-
ctx.regTop--; // undo the allocReg above
|
|
600
|
-
const tmp = ctx.allocReg();
|
|
601
|
-
this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
|
|
602
|
-
if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
|
|
641
|
+
// No initializer: var x; → load undefined directly into the local's register.
|
|
642
|
+
this.emit(bc, [this.OP.LOAD_CONST, slot, b.constantOperand(undefined)], node);
|
|
603
643
|
}
|
|
604
644
|
} else {
|
|
605
645
|
if (decl.init) {
|
|
@@ -617,14 +657,11 @@ export class Compiler {
|
|
|
617
657
|
case "IfStatement":
|
|
618
658
|
{
|
|
619
659
|
const elseOrEndLabel = this._makeLabel("if_else");
|
|
620
|
-
const savedTop = ctx.regTop;
|
|
621
660
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
622
661
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
623
662
|
type: "label",
|
|
624
663
|
label: elseOrEndLabel
|
|
625
664
|
}], node);
|
|
626
|
-
ctx.regTop = savedTop; // free test temps
|
|
627
|
-
|
|
628
665
|
const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
|
|
629
666
|
for (const stmt of consequentBody) {
|
|
630
667
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -671,13 +708,11 @@ export class Compiler {
|
|
|
671
708
|
type: "defineLabel",
|
|
672
709
|
label: loopTopLabel
|
|
673
710
|
}], node);
|
|
674
|
-
const savedTop = ctx.regTop;
|
|
675
711
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
676
712
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
677
713
|
type: "label",
|
|
678
714
|
label: exitLabel
|
|
679
715
|
}], node);
|
|
680
|
-
ctx.regTop = savedTop;
|
|
681
716
|
const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
682
717
|
for (const stmt of whileBody) {
|
|
683
718
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -718,13 +753,11 @@ export class Compiler {
|
|
|
718
753
|
type: "defineLabel",
|
|
719
754
|
label: continueLabel
|
|
720
755
|
}], node);
|
|
721
|
-
const savedTop = ctx.regTop;
|
|
722
756
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
723
757
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
724
758
|
type: "label",
|
|
725
759
|
label: exitLabel
|
|
726
760
|
}], node);
|
|
727
|
-
ctx.regTop = savedTop;
|
|
728
761
|
this.emit(bc, [this.OP.JUMP, {
|
|
729
762
|
type: "label",
|
|
730
763
|
label: loopTopLabel
|
|
@@ -762,13 +795,11 @@ export class Compiler {
|
|
|
762
795
|
label: loopTopLabel
|
|
763
796
|
}], node);
|
|
764
797
|
if (node.test) {
|
|
765
|
-
const savedTop = ctx.regTop;
|
|
766
798
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
767
799
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
768
800
|
type: "label",
|
|
769
801
|
label: exitLabel
|
|
770
802
|
}], node);
|
|
771
|
-
ctx.regTop = savedTop;
|
|
772
803
|
}
|
|
773
804
|
const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
774
805
|
for (const stmt of forBody) {
|
|
@@ -880,7 +911,6 @@ export class Compiler {
|
|
|
880
911
|
const cas = cases[i];
|
|
881
912
|
if (cas.test === null) continue;
|
|
882
913
|
const nextCheckLabel = this._makeLabel("sw_next");
|
|
883
|
-
const savedTop = ctx.regTop;
|
|
884
914
|
const caseValReg = this._compileExpr(cas.test, scope, bc);
|
|
885
915
|
const cmpReg = ctx.allocReg();
|
|
886
916
|
this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
|
|
@@ -888,7 +918,6 @@ export class Compiler {
|
|
|
888
918
|
type: "label",
|
|
889
919
|
label: nextCheckLabel
|
|
890
920
|
}], node);
|
|
891
|
-
ctx.regTop = savedTop;
|
|
892
921
|
this.emit(bc, [this.OP.JUMP, {
|
|
893
922
|
type: "label",
|
|
894
923
|
label: caseLabels[i]
|
|
@@ -992,7 +1021,7 @@ export class Compiler {
|
|
|
992
1021
|
} else if (node.left.type === "Identifier") {
|
|
993
1022
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
994
1023
|
if (res.kind === "local") {
|
|
995
|
-
if (keyReg !== res.
|
|
1024
|
+
if (keyReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
|
|
996
1025
|
} else if (res.kind === "upvalue") {
|
|
997
1026
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
|
|
998
1027
|
} else {
|
|
@@ -1076,12 +1105,25 @@ export class Compiler {
|
|
|
1076
1105
|
}
|
|
1077
1106
|
|
|
1078
1107
|
// ── Expressions ───────────────────────────────────────────────────────────
|
|
1079
|
-
// Returns the
|
|
1080
|
-
// For local variables: returns their
|
|
1081
|
-
// For all others: allocates a fresh
|
|
1108
|
+
// Returns the virtual RegisterOperand that holds the result.
|
|
1109
|
+
// For local variables: returns their RegisterOperand directly (no instruction emitted).
|
|
1110
|
+
// For all others: allocates a fresh virtual register, emits the instruction(s),
|
|
1082
1111
|
// and returns the allocated register.
|
|
1083
1112
|
_compileExpr(node, scope, bc) {
|
|
1084
1113
|
const ctx = this._currentCtx;
|
|
1114
|
+
|
|
1115
|
+
// Intrinsic for emitting raw bytecode, useful for emitting register address
|
|
1116
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_") {
|
|
1117
|
+
const argJSONStrng = node.arguments[0].value;
|
|
1118
|
+
console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
|
|
1119
|
+
const arg = JSON.parse(argJSONStrng);
|
|
1120
|
+
console.log("Parsed bytecode:", arg);
|
|
1121
|
+
const dst = ctx.allocReg();
|
|
1122
|
+
let operand = arg[0];
|
|
1123
|
+
this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
|
|
1124
|
+
|
|
1125
|
+
return dst;
|
|
1126
|
+
}
|
|
1085
1127
|
switch (node.type) {
|
|
1086
1128
|
case "NumericLiteral":
|
|
1087
1129
|
case "StringLiteral":
|
|
@@ -1100,7 +1142,7 @@ export class Compiler {
|
|
|
1100
1142
|
case "Identifier":
|
|
1101
1143
|
{
|
|
1102
1144
|
const res = this._resolve(node.name, this._currentCtx);
|
|
1103
|
-
if (res.kind === "local") return res.
|
|
1145
|
+
if (res.kind === "local") return res.reg; // register IS the local
|
|
1104
1146
|
if (res.kind === "upvalue") {
|
|
1105
1147
|
const dst = ctx.allocReg();
|
|
1106
1148
|
this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
|
|
@@ -1129,9 +1171,7 @@ export class Compiler {
|
|
|
1129
1171
|
{
|
|
1130
1172
|
const exprs = node.expressions;
|
|
1131
1173
|
for (let i = 0; i < exprs.length - 1; i++) {
|
|
1132
|
-
|
|
1133
|
-
this._compileExpr(exprs[i], scope, bc);
|
|
1134
|
-
ctx.regTop = savedTop; // discard intermediate result
|
|
1174
|
+
this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
|
|
1135
1175
|
}
|
|
1136
1176
|
return this._compileExpr(exprs[exprs.length - 1], scope, bc);
|
|
1137
1177
|
}
|
|
@@ -1140,17 +1180,13 @@ export class Compiler {
|
|
|
1140
1180
|
const n = node;
|
|
1141
1181
|
const elseLabel = this._makeLabel("ternary_else");
|
|
1142
1182
|
const endLabel = this._makeLabel("ternary_end");
|
|
1143
|
-
|
|
1144
|
-
// Compile test; free its temps after the jump is emitted.
|
|
1145
|
-
const baseTop = ctx.regTop;
|
|
1146
1183
|
const testReg = this._compileExpr(n.test, scope, bc);
|
|
1147
1184
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
1148
1185
|
type: "label",
|
|
1149
1186
|
label: elseLabel
|
|
1150
1187
|
}], node);
|
|
1151
|
-
ctx.regTop = baseTop; // free test temps
|
|
1152
1188
|
|
|
1153
|
-
//
|
|
1189
|
+
// reg_result is a stable virtual register both branches write into.
|
|
1154
1190
|
const reg_result = ctx.allocReg();
|
|
1155
1191
|
|
|
1156
1192
|
// Consequent branch.
|
|
@@ -1161,22 +1197,18 @@ export class Compiler {
|
|
|
1161
1197
|
label: endLabel
|
|
1162
1198
|
}], node);
|
|
1163
1199
|
|
|
1164
|
-
// Alternate branch
|
|
1200
|
+
// Alternate branch — each allocReg() gets a unique virtual ID so no
|
|
1201
|
+
// slot collision is possible; no need to "re-occupy" reg_result.
|
|
1165
1202
|
this.emit(bc, [null, {
|
|
1166
1203
|
type: "defineLabel",
|
|
1167
1204
|
label: elseLabel
|
|
1168
1205
|
}], node);
|
|
1169
|
-
ctx.regTop = baseTop;
|
|
1170
|
-
ctx.allocReg(); // re-occupy reg_result slot
|
|
1171
1206
|
const altReg = this._compileExpr(n.alternate, scope, bc);
|
|
1172
1207
|
if (altReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
|
|
1173
1208
|
this.emit(bc, [null, {
|
|
1174
1209
|
type: "defineLabel",
|
|
1175
1210
|
label: endLabel
|
|
1176
1211
|
}], node);
|
|
1177
|
-
|
|
1178
|
-
// Leave reg_result allocated above baseTop.
|
|
1179
|
-
ctx.regTop = baseTop + 1;
|
|
1180
1212
|
return reg_result;
|
|
1181
1213
|
}
|
|
1182
1214
|
case "LogicalExpression":
|
|
@@ -1185,9 +1217,7 @@ export class Compiler {
|
|
|
1185
1217
|
const endLabel = this._makeLabel("logical_end");
|
|
1186
1218
|
const isOr = n.operator === "||";
|
|
1187
1219
|
if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
|
|
1188
|
-
const baseTop = ctx.regTop;
|
|
1189
1220
|
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1190
|
-
ctx.regTop = baseTop;
|
|
1191
1221
|
const reg_result = ctx.allocReg();
|
|
1192
1222
|
if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
|
|
1193
1223
|
|
|
@@ -1199,15 +1229,12 @@ export class Compiler {
|
|
|
1199
1229
|
}], node);
|
|
1200
1230
|
|
|
1201
1231
|
// Compile RHS into reg_result.
|
|
1202
|
-
ctx.regTop = baseTop;
|
|
1203
|
-
ctx.allocReg(); // re-occupy reg_result
|
|
1204
1232
|
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1205
1233
|
if (rhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
|
|
1206
1234
|
this.emit(bc, [null, {
|
|
1207
1235
|
type: "defineLabel",
|
|
1208
1236
|
label: endLabel
|
|
1209
1237
|
}], node);
|
|
1210
|
-
ctx.regTop = baseTop + 1;
|
|
1211
1238
|
return reg_result;
|
|
1212
1239
|
}
|
|
1213
1240
|
case "TemplateLiteral":
|
|
@@ -1269,7 +1296,8 @@ export class Compiler {
|
|
|
1269
1296
|
|
|
1270
1297
|
// Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
|
|
1271
1298
|
const applyBump = curReg => {
|
|
1272
|
-
const postfixReg = n.prefix ?
|
|
1299
|
+
const postfixReg = n.prefix ? curReg // prefix: postfix copy unused; caller returns newReg instead
|
|
1300
|
+
: (() => {
|
|
1273
1301
|
const r = ctx.allocReg();
|
|
1274
1302
|
this.emit(bc, [this.OP.MOVE, r, curReg], node);
|
|
1275
1303
|
return r;
|
|
@@ -1301,7 +1329,7 @@ export class Compiler {
|
|
|
1301
1329
|
const res = this._resolve(name, this._currentCtx);
|
|
1302
1330
|
let curReg;
|
|
1303
1331
|
if (res.kind === "local") {
|
|
1304
|
-
curReg = res.
|
|
1332
|
+
curReg = res.reg;
|
|
1305
1333
|
} else if (res.kind === "upvalue") {
|
|
1306
1334
|
curReg = ctx.allocReg();
|
|
1307
1335
|
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
@@ -1311,7 +1339,7 @@ export class Compiler {
|
|
|
1311
1339
|
}
|
|
1312
1340
|
const [postfixReg, newReg] = applyBump(curReg);
|
|
1313
1341
|
if (res.kind === "local") {
|
|
1314
|
-
this.emit(bc, [this.OP.MOVE, res.
|
|
1342
|
+
this.emit(bc, [this.OP.MOVE, res.reg, newReg], node);
|
|
1315
1343
|
} else if (res.kind === "upvalue") {
|
|
1316
1344
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, newReg], node);
|
|
1317
1345
|
} else {
|
|
@@ -1369,7 +1397,7 @@ export class Compiler {
|
|
|
1369
1397
|
// Load current value of the variable.
|
|
1370
1398
|
let curReg;
|
|
1371
1399
|
if (res.kind === "local") {
|
|
1372
|
-
curReg = res.
|
|
1400
|
+
curReg = res.reg;
|
|
1373
1401
|
} else if (res.kind === "upvalue") {
|
|
1374
1402
|
curReg = ctx.allocReg();
|
|
1375
1403
|
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
@@ -1386,8 +1414,8 @@ export class Compiler {
|
|
|
1386
1414
|
|
|
1387
1415
|
// Store result and return it.
|
|
1388
1416
|
if (res.kind === "local") {
|
|
1389
|
-
if (rhsReg !== res.
|
|
1390
|
-
return res.
|
|
1417
|
+
if (rhsReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
|
|
1418
|
+
return res.reg;
|
|
1391
1419
|
} else if (res.kind === "upvalue") {
|
|
1392
1420
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
|
|
1393
1421
|
return rhsReg;
|
|
@@ -1608,6 +1636,7 @@ class Serializer {
|
|
|
1608
1636
|
const v = constants[idx];
|
|
1609
1637
|
if (!key) return v;
|
|
1610
1638
|
if (typeof v === "number") return v ^ key;
|
|
1639
|
+
if (typeof v !== "string") return v;
|
|
1611
1640
|
// String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
|
|
1612
1641
|
const bytes = Buffer.from(v, "base64");
|
|
1613
1642
|
let out = "";
|
|
@@ -1617,16 +1646,25 @@ class Serializer {
|
|
|
1617
1646
|
}
|
|
1618
1647
|
return out;
|
|
1619
1648
|
}
|
|
1620
|
-
|
|
1649
|
+
_generateComment(instr) {
|
|
1621
1650
|
const op = instr[0];
|
|
1622
1651
|
const operands = instr.slice(1);
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
1627
|
-
ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
|
|
1652
|
+
if (op === null && operands[0]?.type === "defineLabel") {
|
|
1653
|
+
const label = operands[0].label;
|
|
1654
|
+
return `${label}:`;
|
|
1628
1655
|
}
|
|
1629
|
-
|
|
1656
|
+
const constants = this.compiler.constants;
|
|
1657
|
+
const emittedOperands = operands.filter(operand => operand?.placeholder !== true);
|
|
1658
|
+
const resolvedOperands = emittedOperands.map(o => o?.resolvedValue ?? o);
|
|
1659
|
+
const displayOperands = operands.map((o, i) => {
|
|
1660
|
+
const resolvedValue = resolvedOperands[i];
|
|
1661
|
+
const label = o?.label;
|
|
1662
|
+
let displayOperand = resolvedValue;
|
|
1663
|
+
if (label) {
|
|
1664
|
+
return label;
|
|
1665
|
+
}
|
|
1666
|
+
return displayOperand;
|
|
1667
|
+
});
|
|
1630
1668
|
let name = this.OP_NAME[op];
|
|
1631
1669
|
if (!name || name.includes("{")) {
|
|
1632
1670
|
name = `OP_${op}`;
|
|
@@ -1637,73 +1675,79 @@ class Serializer {
|
|
|
1637
1675
|
}
|
|
1638
1676
|
const sourceNode = instr[SOURCE_NODE_SYM];
|
|
1639
1677
|
const sourceLocation = sourceNode?.loc ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)].filter(Boolean).join("-") : "";
|
|
1640
|
-
if (
|
|
1678
|
+
if (displayOperands.length > 0) {
|
|
1641
1679
|
// Operand[0] is always `dst` for instruction types that produce a value.
|
|
1642
|
-
const dst =
|
|
1680
|
+
const dst = displayOperands[0];
|
|
1643
1681
|
switch (op) {
|
|
1644
1682
|
case this.OP.LOAD_CONST:
|
|
1645
1683
|
{
|
|
1646
1684
|
// resolvedOperands: [dst, constIdx, concealKey]
|
|
1647
|
-
const val = this._decryptConst(constants,
|
|
1685
|
+
const val = this._decryptConst(constants, displayOperands[1], displayOperands[2]);
|
|
1648
1686
|
comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
|
|
1649
1687
|
break;
|
|
1650
1688
|
}
|
|
1689
|
+
case this.OP.LOAD_INT:
|
|
1690
|
+
{
|
|
1691
|
+
// resolvedOperands: [dst, intValue]
|
|
1692
|
+
comment += ` reg[${dst}] = ${displayOperands[1]}`;
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1651
1695
|
case this.OP.LOAD_GLOBAL:
|
|
1652
1696
|
// resolvedOperands: [dst, constIdx, concealKey]
|
|
1653
|
-
comment += ` reg[${dst}] = ${this._decryptConst(constants,
|
|
1697
|
+
comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
|
|
1654
1698
|
break;
|
|
1655
1699
|
case this.OP.STORE_GLOBAL:
|
|
1656
1700
|
// resolvedOperands: [constIdx, concealKey, srcReg]
|
|
1657
|
-
comment += ` ${this._decryptConst(constants,
|
|
1701
|
+
comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
|
|
1658
1702
|
break;
|
|
1659
1703
|
case this.OP.LOAD_UPVALUE:
|
|
1660
|
-
comment += ` reg[${dst}] = upvalue[${
|
|
1704
|
+
comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
|
|
1661
1705
|
break;
|
|
1662
1706
|
case this.OP.STORE_UPVALUE:
|
|
1663
|
-
comment += ` upvalue[${
|
|
1707
|
+
comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
|
|
1664
1708
|
break;
|
|
1665
1709
|
case this.OP.MOVE:
|
|
1666
|
-
comment += ` reg[${dst}] = reg[${
|
|
1710
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
|
|
1667
1711
|
break;
|
|
1668
1712
|
case this.OP.MAKE_CLOSURE:
|
|
1669
|
-
comment += ` reg[${dst}] PC=${
|
|
1713
|
+
comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
|
|
1670
1714
|
break;
|
|
1671
1715
|
case this.OP.CALL:
|
|
1672
|
-
comment += ` reg[${dst}] = reg[${
|
|
1716
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands.slice(3).map(v => `reg[${v}]`).join(", ")})`;
|
|
1673
1717
|
break;
|
|
1674
1718
|
case this.OP.CALL_METHOD:
|
|
1675
|
-
comment += ` reg[${dst}] = reg[${
|
|
1719
|
+
comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
|
|
1676
1720
|
break;
|
|
1677
1721
|
case this.OP.NEW:
|
|
1678
|
-
comment += ` reg[${dst}] = new reg[${
|
|
1722
|
+
comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
|
|
1679
1723
|
break;
|
|
1680
1724
|
case this.OP.RETURN:
|
|
1681
|
-
comment += ` reg[${
|
|
1725
|
+
comment += ` reg[${displayOperands[0]}]`;
|
|
1682
1726
|
break;
|
|
1683
1727
|
case this.OP.BUILD_ARRAY:
|
|
1684
|
-
comment += ` reg[${dst}] = [${
|
|
1728
|
+
comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
|
|
1685
1729
|
break;
|
|
1686
1730
|
case this.OP.BUILD_OBJECT:
|
|
1687
|
-
comment += ` reg[${dst}] = {${
|
|
1731
|
+
comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
|
|
1688
1732
|
break;
|
|
1689
1733
|
case this.OP.GET_PROP:
|
|
1690
|
-
comment += ` reg[${dst}] = reg[${
|
|
1734
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
|
|
1691
1735
|
break;
|
|
1692
1736
|
case this.OP.SET_PROP:
|
|
1693
|
-
comment += ` reg[${
|
|
1737
|
+
comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
|
|
1738
|
+
break;
|
|
1739
|
+
case this.OP.JUMP_REG:
|
|
1740
|
+
comment += ` PC = reg[${displayOperands[0]}]`;
|
|
1694
1741
|
break;
|
|
1695
1742
|
default:
|
|
1696
|
-
comment +=
|
|
1743
|
+
comment += displayOperands.length === 1 ? ` ${displayOperands[0]}` : ` [${displayOperands.join(", ")}]`;
|
|
1697
1744
|
}
|
|
1698
1745
|
}
|
|
1699
1746
|
comment = comment.padEnd(50) + sourceLocation;
|
|
1700
1747
|
const values = [op, ...resolvedOperands];
|
|
1701
1748
|
const instrText = `[${values.join(", ")}]`;
|
|
1702
1749
|
const text = `${(instrText + ",").padEnd(20)} ${comment}`;
|
|
1703
|
-
return
|
|
1704
|
-
text,
|
|
1705
|
-
values
|
|
1706
|
-
};
|
|
1750
|
+
return text;
|
|
1707
1751
|
}
|
|
1708
1752
|
_serializeConstants(constants) {
|
|
1709
1753
|
const lines = ["var CONSTANTS = ["];
|
|
@@ -1716,14 +1760,23 @@ class Serializer {
|
|
|
1716
1760
|
_serializeBytecode(bytecode, compiler) {
|
|
1717
1761
|
const serialized = [];
|
|
1718
1762
|
for (const instr of bytecode) {
|
|
1719
|
-
|
|
1763
|
+
const op = instr[0];
|
|
1764
|
+
const operands = instr.slice(1);
|
|
1765
|
+
if (instr[0] === null) continue; // null opcodes are not emitted
|
|
1766
|
+
|
|
1767
|
+
const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
|
|
1720
1768
|
const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
|
|
1721
1769
|
if (specializedOpInfo) {
|
|
1722
|
-
const operands = instr.slice(1);
|
|
1723
|
-
const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
|
|
1724
1770
|
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
1725
1771
|
compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValues.join("_")}`;
|
|
1726
1772
|
}
|
|
1773
|
+
|
|
1774
|
+
// Validate no opcode or operand exceeds u16 limit
|
|
1775
|
+
for (const o of resolvedValues) {
|
|
1776
|
+
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
1777
|
+
ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
|
|
1778
|
+
}
|
|
1779
|
+
ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
|
|
1727
1780
|
serialized.push(instr);
|
|
1728
1781
|
}
|
|
1729
1782
|
return {
|
|
@@ -1769,6 +1822,14 @@ class Serializer {
|
|
|
1769
1822
|
export async function compileAndSerialize(sourceCode, options) {
|
|
1770
1823
|
const compiler = new Compiler(options);
|
|
1771
1824
|
let bytecode = compiler.compile(sourceCode);
|
|
1825
|
+
|
|
1826
|
+
// jumpDispatcher must run before resolveRegisters so that the new rDisp/rKey
|
|
1827
|
+
// RegisterOperand objects it injects are visible to the liveness analysis.
|
|
1828
|
+
// It must also run before resolveLabels since it emits encodedLabel IR operands.
|
|
1829
|
+
if (options.dispatcher) {
|
|
1830
|
+
const dispatcherResult = dispatcher(bytecode, compiler);
|
|
1831
|
+
bytecode = dispatcherResult.bytecode;
|
|
1832
|
+
}
|
|
1772
1833
|
const passes = [];
|
|
1773
1834
|
passes.push(concealConstants);
|
|
1774
1835
|
if (options.specializedOpcodes) {
|
|
@@ -1780,9 +1841,6 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1780
1841
|
if (options.macroOpcodes) {
|
|
1781
1842
|
passes.push(macroOpcodes);
|
|
1782
1843
|
}
|
|
1783
|
-
if (options.selfModifying) {
|
|
1784
|
-
passes.push(selfModifying);
|
|
1785
|
-
}
|
|
1786
1844
|
if (options.aliasedOpcodes) {
|
|
1787
1845
|
passes.push(aliasedOpcodes);
|
|
1788
1846
|
}
|
|
@@ -1791,6 +1849,21 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1791
1849
|
bytecode = passResult.bytecode;
|
|
1792
1850
|
}
|
|
1793
1851
|
|
|
1852
|
+
// Resolve virtual registers to concrete slot indices and set regCount per fn.
|
|
1853
|
+
// Must run BEFORE selfModifying: that pass moves body instructions to the end
|
|
1854
|
+
// of the bytecode while leaving RETURN in place, splitting a function's code
|
|
1855
|
+
// into two non-contiguous regions. Linear-scan liveness then sees incorrect
|
|
1856
|
+
// firstUse/lastUse for registers that span the gap, causing slot collisions.
|
|
1857
|
+
const regsResult = resolveRegisters(bytecode, compiler);
|
|
1858
|
+
bytecode = regsResult.bytecode;
|
|
1859
|
+
|
|
1860
|
+
// selfModifying runs after register resolution so concrete slot indices are
|
|
1861
|
+
// already in place; only label operands remain unresolved at this stage.
|
|
1862
|
+
if (options.selfModifying) {
|
|
1863
|
+
const smResult = selfModifying(bytecode, compiler);
|
|
1864
|
+
bytecode = smResult.bytecode;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1794
1867
|
// Resolve label references to flat bytecode indices.
|
|
1795
1868
|
const labelsResult = resolveLabels(bytecode, compiler);
|
|
1796
1869
|
bytecode = labelsResult.bytecode;
|
|
@@ -1812,8 +1885,8 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1812
1885
|
const generateBytecodeComment = () => {
|
|
1813
1886
|
var lines = [];
|
|
1814
1887
|
for (const instr of bytecode) {
|
|
1815
|
-
const
|
|
1816
|
-
lines.push("// " +
|
|
1888
|
+
const comment = compiler.serializer._generateComment(instr);
|
|
1889
|
+
lines.push("// " + comment);
|
|
1817
1890
|
}
|
|
1818
1891
|
return lines.join("\n");
|
|
1819
1892
|
};
|