js-confuser-vm 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -147
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +714 -265
- package/dist/disassembler.js +367 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +160 -119
- package/dist/template.js +163 -42
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
- package/dist/transforms/bytecode/dispatcher.js +15 -111
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
- package/dist/transforms/bytecode/resolveRegisters.js +23 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
- package/dist/transforms/bytecode/stringConcealing.js +288 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +75 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/package.json +8 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -335
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/index.ts +0 -38
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -52
- package/src/build-runtime.ts +0 -78
- package/src/compiler.ts +0 -2593
- package/src/index.ts +0 -14
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -18
- package/src/runtime.ts +0 -923
- package/src/template.ts +0 -141
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/dispatcher.ts +0 -398
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -221
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -156
- package/src/types.ts +0 -93
- package/src/utils/op-utils.ts +0 -48
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
package/dist/compiler.js
CHANGED
|
@@ -5,40 +5,43 @@ import traverseImport from "@babel/traverse";
|
|
|
5
5
|
import { generate } from "@babel/generator";
|
|
6
6
|
import { stripTypeScriptTypes } from "module";
|
|
7
7
|
import { ok } from "assert";
|
|
8
|
-
import {
|
|
8
|
+
import { buildRuntime } from "./build-runtime.js";
|
|
9
9
|
import { DEFAULT_OPTIONS } from "./options.js";
|
|
10
10
|
import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
|
|
11
11
|
import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.js";
|
|
12
|
-
import { resolveConstants } from "./transforms/bytecode/
|
|
12
|
+
import { resolveConstants } from "./transforms/bytecode/resolveConstants.js";
|
|
13
13
|
import { selfModifying } from "./transforms/bytecode/selfModifying.js";
|
|
14
14
|
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
|
|
15
|
-
import { microOpcodes } from "./transforms/bytecode/microOpcodes.js";
|
|
16
15
|
import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
|
|
17
16
|
import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
|
|
18
17
|
import { getRandomInt } from "./utils/random-utils.js";
|
|
19
|
-
import { U16_MAX } from "./utils/op-utils.js";
|
|
18
|
+
import { U16_MAX, U32_MAX } from "./utils/op-utils.js";
|
|
20
19
|
import { concealConstants } from "./transforms/bytecode/concealConstants.js";
|
|
21
20
|
import { dispatcher } from "./transforms/bytecode/dispatcher.js";
|
|
21
|
+
import { controlFlowFlattening } from "./transforms/bytecode/controlFlowFlattening.js";
|
|
22
|
+
import { stringConcealing } from "./transforms/bytecode/stringConcealing.js";
|
|
23
|
+
import { now } from "./utils/profile-utils.js";
|
|
24
|
+
import { walkHoistScope } from "./utils/ast-utils.js";
|
|
22
25
|
const traverse = traverseImport.default || traverseImport;
|
|
23
|
-
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue \u2014 Lua/CPython style.\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\n// _absSlot is the absolute index in VM._regs (frame._base + local slot).\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\n// Frame \u2014 analogous to Lua CallInfo / CPython PyFrameObject.\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // \u2500\u2500 Flat register file (Lua-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // All frames share a single array. Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n this._internals = {};\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var ctor = regs[base + this._operand()];\n if (typeof ctor === \"function\") {\n regs[base + dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n regs[base + dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n for (var i = 0; i < args.length && i < c.fn.regCount; i++) {\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: target PC is read from a register rather than a\n // bytecode immediate. Used by the jumpDispatcher pass so that static\n // analysis cannot determine the jump destination without tracking the\n // register value (which contains an encoded PC resolved at runtime).\n frame._pc = regs[base + this._operand()];\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n this._regs[handledFrame._base + h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals.window = window;\n for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
|
|
26
|
+
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\nconst SENTINELS = { CALL_SPREAD: 0 };\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction base64ToBytes(s) {\n return typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n}\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b = base64ToBytes(s);\n // Each slot is a u32 stored as 4 little-endian bytes.\n var r = new Uint32Array(b.length / 4);\n for (var i = 0; i < r.length; i++)\n r[i] =\n (b[i * 4] |\n (b[i * 4 + 1] << 8) |\n (b[i * 4 + 2] << 16) |\n (b[i * 4 + 3] << 24)) >>>\n 0;\n return r;\n}\n\n// Closure map\n// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls.\n// A WeakMap is used over a Symbol to prevent leaking information to debuggers\nvar CLOSURE_MAP = new WeakMap();\n\n// Upvalue (Lua style)\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\n// Frame (Lua CallInfo / CPython PyFrameObject)\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // Flat register array (Lua-style)\n // Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx: pool index (first operand of the constant pair emitted by resolveConstants).\n// key: conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b = base64ToBytes(v);\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`);\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // globals[globalName] = regs[src]\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n // regs[dst] = delete regs[obj][regs[key]]\n // The delete operator returns true if successful which is most cases\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // Arithmetic (dst, src1, src2)\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.EXP: {\n // Math.pow instead of `**`\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = Math.pow(a, regs[base + this._operand()]);\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // Comparison (dst, src1, src2)\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n // regs[dst] = regs[obj] instanceof regs[ctor]\n // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n regs[base + dst] = obj instanceof regs[base + this._operand()];\n break;\n }\n\n // Unary (dst, src)\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consumes argument (intended)\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // regs[dst] = typeof window[name]\n // Never throws ReferenceError, instead returns undefined for undeclared variables\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // Control flow\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // Calls\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg)\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args;\n if (argc === SENTINELS.CALL_SPREAD) {\n args = regs[base + this._operand()];\n } else {\n args = new Array(argc);\n for (var i = 0; i < argc; i++)\n args[i] = regs[base + this._operand()];\n }\n\n var closure = callee && CLOSURE_MAP.get(callee);\n if (closure) {\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg)\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args;\n if (argc === SENTINELS.CALL_SPREAD) {\n args = regs[base + this._operand()];\n } else {\n args = new Array(argc);\n for (var i = 0; i < argc; i++)\n args[i] = regs[base + this._operand()];\n }\n\n var closure = callee && CLOSURE_MAP.get(callee);\n if (closure) {\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg)\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args;\n if (argc === SENTINELS.CALL_SPREAD) {\n args = regs[base + this._operand()];\n } else {\n args = new Array(argc);\n for (var i = 0; i < argc; i++)\n args[i] = regs[base + this._operand()];\n }\n\n var closure = callee && CLOSURE_MAP.get(callee);\n if (closure) {\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n // Zero out callee's register window to limit exposing runtime values\n var hi = frame._base + frame.closure.fn.regCount;\n for (var i = frame._base ; i < hi; i++)\n this._regs[i] = undefined;\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // Closures\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n var hasRest = this._operand(); // 1 if last param is a rest element\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n hasRest: hasRest,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n if (c.fn.hasRest) {\n var restSlot = c.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n sub._regs[i] = i < args.length ? args[i] : undefined;\n sub._regs[restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < c.fn.regCount; i++)\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n CLOSURE_MAP.set(shell, closure);\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // Collections\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // Object methods (getters / setters)\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the top handler record\n // (works for both catch and finally regions; they share the stack).\n frame._handlerStack.pop();\n break;\n }\n\n case OP.FINALLY_SETUP: {\n // finallyPc, contReg, payloadReg, throwPad\n // Arm a finalizer for the current region. Unlike a catch record this\n // carries no exceptionReg; instead the continuation register (contReg)\n // receives the resume PC and payloadReg carries the in-flight value.\n frame._handlerStack.push({\n finallyPc: this._operand(),\n contReg: this._operand(),\n payloadReg: this._operand(),\n throwPad: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n // Self-modifying bytecode\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: allows VM to jump based on runtime values.\n frame._pc = regs[base + this._operand()];\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding\n // Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // if there's no handler, propagate back to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the protected region.\n this._frameStack.length = h.frameStackDepth;\n var hBase = handledFrame._base;\n if (h.exceptionReg !== undefined) {\n // catch region \u2014 deliver the exception to the catch binding and run it.\n this._regs[hBase + h.exceptionReg] = err;\n handledFrame._pc = h.handlerPc;\n } else {\n // finally region: run the finalizer with the exception pending, then\n // resume at its throw pad (which re-raises and continues unwinding).\n this._regs[hBase + h.contReg] = h.throwPad;\n this._regs[hBase + h.payloadReg] = err;\n handledFrame._pc = h.finallyPc;\n }\n this._regsTop = hBase + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n/* @BOOT */ // <- This comment can't be removed!\nvar globals = globalThis;\nif (typeof window !== \"undefined\") {\n globals.window = window;\n globals.document = typeof document !== \"undefined\" ? document : undefined;\n}\nif (typeof module !== \"undefined\") {\n globals.module = module;\n globals.exports = typeof exports !== \"undefined\" ? exports : undefined;\n}\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
|
|
24
27
|
export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
25
28
|
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
|
|
26
29
|
|
|
27
|
-
//
|
|
30
|
+
// Opcodes
|
|
28
31
|
// Register-based encoding. Operand convention (x86 / CPython style):
|
|
29
|
-
//
|
|
32
|
+
// destination register first, then source registers, then immediates.
|
|
30
33
|
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
+
// dst – register index that receives the result
|
|
35
|
+
// src – register index holding an input value
|
|
36
|
+
// imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
|
|
34
37
|
//
|
|
35
38
|
// Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
|
|
36
39
|
// Every load: [op, dst, ...]
|
|
37
40
|
// Every store: [op, target, src]
|
|
38
41
|
// Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
|
|
39
|
-
//
|
|
42
|
+
// CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
|
|
40
43
|
export const OP_ORIGINAL = {
|
|
41
|
-
//
|
|
44
|
+
// Loads
|
|
42
45
|
LOAD_CONST: 0,
|
|
43
46
|
// dst, constIdx regs[dst] = constants[constIdx]
|
|
44
47
|
LOAD_INT: 1,
|
|
@@ -52,13 +55,13 @@ export const OP_ORIGINAL = {
|
|
|
52
55
|
MOVE: 5,
|
|
53
56
|
// dst, src regs[dst] = regs[src]
|
|
54
57
|
|
|
55
|
-
//
|
|
58
|
+
// Stores
|
|
56
59
|
STORE_GLOBAL: 6,
|
|
57
60
|
// nameIdx, src globals[constants[nameIdx]] = regs[src]
|
|
58
61
|
STORE_UPVALUE: 7,
|
|
59
62
|
// uvIdx, src upvalues[uvIdx].write(regs[src])
|
|
60
63
|
|
|
61
|
-
//
|
|
64
|
+
// Property access
|
|
62
65
|
GET_PROP: 8,
|
|
63
66
|
// dst, obj, key regs[dst] = regs[obj][regs[key]]
|
|
64
67
|
SET_PROP: 9,
|
|
@@ -66,19 +69,21 @@ export const OP_ORIGINAL = {
|
|
|
66
69
|
DELETE_PROP: 10,
|
|
67
70
|
// dst, obj, key regs[dst] = delete regs[obj][regs[key]]
|
|
68
71
|
|
|
69
|
-
//
|
|
72
|
+
// Arithmetic / bitwise (dst, src1, src2)
|
|
70
73
|
ADD: 11,
|
|
71
74
|
SUB: 12,
|
|
72
75
|
MUL: 13,
|
|
73
76
|
DIV: 14,
|
|
74
77
|
MOD: 15,
|
|
78
|
+
EXP: 60,
|
|
79
|
+
// dst, src1, src2 regs[dst] = regs[src1] ** regs[src2]
|
|
75
80
|
BAND: 16,
|
|
76
81
|
BOR: 17,
|
|
77
82
|
BXOR: 18,
|
|
78
83
|
SHL: 19,
|
|
79
84
|
SHR: 20,
|
|
80
85
|
USHR: 21,
|
|
81
|
-
//
|
|
86
|
+
// Comparison (dst, src1, src2)
|
|
82
87
|
LT: 22,
|
|
83
88
|
GT: 23,
|
|
84
89
|
LTE: 24,
|
|
@@ -89,7 +94,7 @@ export const OP_ORIGINAL = {
|
|
|
89
94
|
LOOSE_NEQ: 29,
|
|
90
95
|
IN: 30,
|
|
91
96
|
INSTANCEOF: 31,
|
|
92
|
-
//
|
|
97
|
+
// Unary (dst, src)
|
|
93
98
|
UNARY_NEG: 32,
|
|
94
99
|
UNARY_POS: 33,
|
|
95
100
|
UNARY_NOT: 34,
|
|
@@ -101,7 +106,7 @@ export const OP_ORIGINAL = {
|
|
|
101
106
|
TYPEOF_SAFE: 38,
|
|
102
107
|
// dst, nameConstIdx – safe typeof for potentially-undeclared globals
|
|
103
108
|
|
|
104
|
-
//
|
|
109
|
+
// Control flow
|
|
105
110
|
JUMP: 39,
|
|
106
111
|
// target
|
|
107
112
|
JUMP_IF_FALSE: 40,
|
|
@@ -109,7 +114,7 @@ export const OP_ORIGINAL = {
|
|
|
109
114
|
JUMP_IF_TRUE: 41,
|
|
110
115
|
// src, target if regs[src] then pc = target (|| short-circuit)
|
|
111
116
|
|
|
112
|
-
//
|
|
117
|
+
// Calls & constructors
|
|
113
118
|
CALL: 42,
|
|
114
119
|
// dst, callee, argc, [argRegs…]
|
|
115
120
|
CALL_METHOD: 43,
|
|
@@ -121,48 +126,59 @@ export const OP_ORIGINAL = {
|
|
|
121
126
|
THROW: 46,
|
|
122
127
|
// src
|
|
123
128
|
|
|
124
|
-
//
|
|
129
|
+
// Closures
|
|
125
130
|
// dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
126
131
|
MAKE_CLOSURE: 47,
|
|
127
|
-
//
|
|
132
|
+
// Collections
|
|
128
133
|
BUILD_ARRAY: 48,
|
|
129
134
|
// dst, count, [elemRegs…]
|
|
130
135
|
BUILD_OBJECT: 49,
|
|
131
136
|
// dst, pairCount, [keyReg, valReg, …]
|
|
132
137
|
|
|
133
|
-
//
|
|
138
|
+
// Property definitions (getters / setters)
|
|
134
139
|
DEFINE_GETTER: 50,
|
|
135
140
|
// obj, key, fn
|
|
136
141
|
DEFINE_SETTER: 51,
|
|
137
142
|
// obj, key, fn
|
|
138
143
|
|
|
139
|
-
//
|
|
144
|
+
// For-in iteration
|
|
140
145
|
FOR_IN_SETUP: 52,
|
|
141
146
|
// dst, src dst = { _keys: enumKeys(src), i: 0 }
|
|
142
147
|
FOR_IN_NEXT: 53,
|
|
143
148
|
// dst, iter, exitTarget
|
|
144
149
|
|
|
145
|
-
//
|
|
150
|
+
// Exception handling
|
|
146
151
|
TRY_SETUP: 54,
|
|
147
152
|
// handlerPc, exceptionReg
|
|
148
153
|
TRY_END: 55,
|
|
149
|
-
//
|
|
154
|
+
// Self-modifying bytecode
|
|
150
155
|
PATCH: 56,
|
|
151
156
|
// destPc, sliceStart, sliceEnd
|
|
152
157
|
|
|
153
|
-
//
|
|
158
|
+
// Debug
|
|
154
159
|
DEBUGGER: 57,
|
|
155
|
-
//
|
|
156
|
-
// Used by
|
|
160
|
+
// Indirect jump (register-addressed)
|
|
161
|
+
// Used by Dispatcher pass. The target PC is read from a register
|
|
157
162
|
// rather than encoded as a bytecode immediate, so static analysis cannot
|
|
158
163
|
// determine the destination without tracking register values at runtime.
|
|
159
|
-
JUMP_REG: 58
|
|
164
|
+
JUMP_REG: 58,
|
|
165
|
+
// src — frame._pc = regs[src]
|
|
166
|
+
|
|
167
|
+
// Exception handling (finally)
|
|
168
|
+
// Arms a finalizer for the current region. Operands:
|
|
169
|
+
// finallyPc, contReg, payloadReg, throwPad
|
|
170
|
+
// The finalizer runs on every exit path (normal, return, break/continue,
|
|
171
|
+
// throw). contReg holds the PC to resume at once the finalizer completes;
|
|
172
|
+
// the finalizer ends with JUMP_REG contReg. payloadReg carries the pending
|
|
173
|
+
// value (return value or in-flight exception). throwPad is the PC the
|
|
174
|
+
// runtime resumes at when an exception is pending (re-raises after finally).
|
|
175
|
+
FINALLY_SETUP: 59
|
|
160
176
|
};
|
|
161
177
|
|
|
162
|
-
//
|
|
178
|
+
// Scope
|
|
163
179
|
// Maps variable names to virtual RegisterOperands.
|
|
164
180
|
// Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
|
|
165
|
-
// resolveRegisters() assigns concrete slot indices before
|
|
181
|
+
// resolveRegisters() assigns concrete slot indices before serialization.
|
|
166
182
|
class Scope {
|
|
167
183
|
constructor(parent = null) {
|
|
168
184
|
this.parent = parent;
|
|
@@ -188,15 +204,15 @@ class Scope {
|
|
|
188
204
|
}
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
//
|
|
207
|
+
// FnContext
|
|
192
208
|
// Compiler-side state for the function currently being compiled.
|
|
193
209
|
// Distinct from the runtime Frame — this is compile-time only.
|
|
194
210
|
//
|
|
195
211
|
// Virtual-register model (Lua/LLVM style):
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
212
|
+
// Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
|
|
213
|
+
// unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
|
|
214
|
+
// liveness-aware slot assignment and sets desc.regCount at the end of the
|
|
215
|
+
// pipeline, just like resolveLabels() fills in jump targets.
|
|
200
216
|
class FnContext {
|
|
201
217
|
// index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
|
|
202
218
|
|
|
@@ -218,11 +234,11 @@ class FnContext {
|
|
|
218
234
|
return b.registerOperand(this._nextId++, this._fnId);
|
|
219
235
|
}
|
|
220
236
|
|
|
221
|
-
/**
|
|
222
|
-
* Allocate a short-lived temporary register (pool "temp::").
|
|
223
|
-
* resolveRegisters() will reuse its concrete slot once its live range ends.
|
|
224
|
-
* Do NOT use for named locals or upvalue-captured variables — use _newReg()
|
|
225
|
-
* via scope.define() for those, so they stay in the stable "local::" pool.
|
|
237
|
+
/**
|
|
238
|
+
* Allocate a short-lived temporary register (pool "temp::").
|
|
239
|
+
* resolveRegisters() will reuse its concrete slot once its live range ends.
|
|
240
|
+
* Do NOT use for named locals or upvalue-captured variables — use _newReg()
|
|
241
|
+
* via scope.define() for those, so they stay in the stable "local::" pool.
|
|
226
242
|
*/
|
|
227
243
|
allocReg() {
|
|
228
244
|
return b.registerOperand(this._nextId++, this._fnId, {
|
|
@@ -230,17 +246,17 @@ class FnContext {
|
|
|
230
246
|
});
|
|
231
247
|
}
|
|
232
248
|
|
|
233
|
-
/**
|
|
234
|
-
* Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
|
|
235
|
-
*
|
|
236
|
-
* NOTE: This is extraneous for any programmatically generated IR.
|
|
237
|
-
* resolveRegisters() already computes lastUse as the last instruction index
|
|
238
|
-
* where the register appears as a real operand — which is always the tightest
|
|
239
|
-
* correct bound when you stop emitting a register after its last logical use.
|
|
240
|
-
* freeReg is only needed in the rare case where a register has a late syntactic
|
|
241
|
-
* appearance that does NOT represent its true logical death (e.g. a dummy read
|
|
242
|
-
* emitted for side-effects long after the value is logically dead). No current
|
|
243
|
-
* pass in this codebase uses it; it is kept as an extension point only.
|
|
249
|
+
/**
|
|
250
|
+
* Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
|
|
251
|
+
*
|
|
252
|
+
* NOTE: This is extraneous for any programmatically generated IR.
|
|
253
|
+
* resolveRegisters() already computes lastUse as the last instruction index
|
|
254
|
+
* where the register appears as a real operand — which is always the tightest
|
|
255
|
+
* correct bound when you stop emitting a register after its last logical use.
|
|
256
|
+
* freeReg is only needed in the rare case where a register has a late syntactic
|
|
257
|
+
* appearance that does NOT represent its true logical death (e.g. a dummy read
|
|
258
|
+
* emitted for side-effects long after the value is logically dead). No current
|
|
259
|
+
* pass in this codebase uses it; it is kept as an extension point only.
|
|
244
260
|
*/
|
|
245
261
|
freeReg(bc, reg) {
|
|
246
262
|
bc.push([null, b.freeRegOperand(reg)]);
|
|
@@ -260,12 +276,13 @@ class FnContext {
|
|
|
260
276
|
return idx;
|
|
261
277
|
}
|
|
262
278
|
}
|
|
263
|
-
//
|
|
279
|
+
// Compiler
|
|
264
280
|
export class Compiler {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
log(...messages) {
|
|
282
|
+
if (this.options.verbose) {
|
|
283
|
+
console.log(...messages);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
269
286
|
_cloneRegisterOperand(operand) {
|
|
270
287
|
if (!operand || typeof operand !== "object") return operand;
|
|
271
288
|
if (operand.type !== "register") return operand;
|
|
@@ -294,10 +311,6 @@ export class Compiler {
|
|
|
294
311
|
this.MICRO_OPS = {};
|
|
295
312
|
this.SPECIALIZED_OPS = {};
|
|
296
313
|
this.ALIASED_OPS = {};
|
|
297
|
-
this._internals = {
|
|
298
|
-
globally: new Map(),
|
|
299
|
-
opcodes: new Map()
|
|
300
|
-
};
|
|
301
314
|
this.OP = {
|
|
302
315
|
...OP_ORIGINAL
|
|
303
316
|
};
|
|
@@ -312,8 +325,16 @@ export class Compiler {
|
|
|
312
325
|
this.OP[key] = val;
|
|
313
326
|
}
|
|
314
327
|
}
|
|
328
|
+
|
|
329
|
+
// SENTINELS: magic values placed in argc slots to signal special call modes.
|
|
330
|
+
// Default to U16_MAX (safely above any valid arg count).
|
|
331
|
+
// When randomizeOpcodes is on, pick a random value in [U16_MAX, U32_MAX] so
|
|
332
|
+
// each obfuscated output looks different to a static analyser.
|
|
333
|
+
this.SENTINELS = {
|
|
334
|
+
CALL_SPREAD: this.options.randomizeOpcodes ? getRandomInt(U16_MAX, U32_MAX) : U16_MAX
|
|
335
|
+
};
|
|
315
336
|
this.OP_NAME = Object.fromEntries(Object.entries(this.OP).map(([k, v]) => [v, k]));
|
|
316
|
-
this.JUMP_OPS = new Set([this.OP.JUMP, this.OP.JUMP_IF_FALSE, this.OP.JUMP_IF_TRUE, this.OP.FOR_IN_NEXT, this.OP.TRY_SETUP]);
|
|
337
|
+
this.JUMP_OPS = new Set([this.OP.JUMP, this.OP.JUMP_IF_FALSE, this.OP.JUMP_IF_TRUE, this.OP.FOR_IN_NEXT, this.OP.TRY_SETUP, this.OP.FINALLY_SETUP]);
|
|
317
338
|
}
|
|
318
339
|
_makeLabel(hint = "") {
|
|
319
340
|
return `${hint || "L"}_${this._labelCount++}`;
|
|
@@ -344,12 +365,11 @@ export class Compiler {
|
|
|
344
365
|
};
|
|
345
366
|
}
|
|
346
367
|
|
|
347
|
-
// ── Variable hoisting ──────────────────────────────────────────────────────
|
|
348
368
|
// Pre-scan a statement list and reserve virtual registers for every var
|
|
349
369
|
// declaration, function declaration, for-in iterator, and try-catch binding.
|
|
350
370
|
// Must be called before any emit so that locals are allocated before temps.
|
|
351
371
|
_hoistVars(stmts, scope, ctx) {
|
|
352
|
-
|
|
372
|
+
walkHoistScope(stmts, stmt => {
|
|
353
373
|
switch (stmt.type) {
|
|
354
374
|
case "VariableDeclaration":
|
|
355
375
|
for (const decl of stmt.declarations) {
|
|
@@ -359,90 +379,77 @@ export class Compiler {
|
|
|
359
379
|
case "FunctionDeclaration":
|
|
360
380
|
if (stmt.id) scope.define(stmt.id.name, ctx);
|
|
361
381
|
break;
|
|
362
|
-
case "BlockStatement":
|
|
363
|
-
this._hoistVars(stmt.body, scope, ctx);
|
|
364
|
-
break;
|
|
365
|
-
case "IfStatement":
|
|
366
|
-
{
|
|
367
|
-
const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
|
|
368
|
-
this._hoistVars(cons, scope, ctx);
|
|
369
|
-
if (stmt.alternate) {
|
|
370
|
-
const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
|
|
371
|
-
this._hoistVars(alt, scope, ctx);
|
|
372
|
-
}
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
case "WhileStatement":
|
|
376
|
-
case "DoWhileStatement":
|
|
377
|
-
{
|
|
378
|
-
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
379
|
-
this._hoistVars(body, scope, ctx);
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
case "ForStatement":
|
|
383
|
-
{
|
|
384
|
-
if (stmt.init?.type === "VariableDeclaration") {
|
|
385
|
-
for (const decl of stmt.init.declarations) {
|
|
386
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
390
|
-
this._hoistVars(body, scope, ctx);
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
382
|
case "ForInStatement":
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
stmt._iterSlot = ctx._newReg();
|
|
397
|
-
if (stmt.left.type === "VariableDeclaration") {
|
|
398
|
-
for (const decl of stmt.left.declarations) {
|
|
399
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
403
|
-
this._hoistVars(body, scope, ctx);
|
|
404
|
-
break;
|
|
405
|
-
}
|
|
406
|
-
case "SwitchStatement":
|
|
407
|
-
for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
|
|
383
|
+
// Reserve a hidden virtual register for the iterator object.
|
|
384
|
+
stmt._iterSlot = ctx._newReg();
|
|
408
385
|
break;
|
|
409
386
|
case "TryStatement":
|
|
410
|
-
this._hoistVars(stmt.block.body, scope, ctx);
|
|
411
387
|
if (stmt.handler) {
|
|
412
388
|
if (stmt.handler.param?.type === "Identifier") {
|
|
413
|
-
// Catch parameter IS the exception register.
|
|
414
389
|
scope.define(stmt.handler.param.name, ctx);
|
|
415
390
|
} else {
|
|
416
|
-
// No catch binding – reserve a dummy
|
|
391
|
+
// No catch binding – reserve a dummy register for the exception value.
|
|
417
392
|
stmt._exceptionSlot = ctx._newReg();
|
|
418
393
|
}
|
|
419
|
-
this._hoistVars(stmt.handler.body.body, scope, ctx);
|
|
420
394
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
395
|
+
if (stmt.finalizer) {
|
|
396
|
+
// Two stable locals survive the finalizer: contReg (resume PC) and
|
|
397
|
+
// payloadReg (pending return value / in-flight exception).
|
|
398
|
+
stmt._finallyContReg = ctx._newReg();
|
|
399
|
+
stmt._finallyPayloadReg = ctx._newReg();
|
|
400
|
+
}
|
|
424
401
|
break;
|
|
425
402
|
}
|
|
426
|
-
}
|
|
403
|
+
});
|
|
427
404
|
}
|
|
428
405
|
|
|
429
|
-
//
|
|
406
|
+
// Collect all FunctionDeclaration nodes reachable in the current function
|
|
407
|
+
// scope (does not cross into nested function bodies).
|
|
408
|
+
_collectHoistedFunctions(stmts) {
|
|
409
|
+
const result = [];
|
|
410
|
+
walkHoistScope(stmts, stmt => {
|
|
411
|
+
if (stmt.type === "FunctionDeclaration") result.push(stmt);
|
|
412
|
+
});
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
profileData = {
|
|
416
|
+
transforms: {}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Entry point
|
|
430
420
|
compile(source) {
|
|
421
|
+
let startedAt = now();
|
|
431
422
|
const ast = parse(source, {
|
|
432
423
|
sourceType: "script",
|
|
433
424
|
allowReturnOutsideFunction: true
|
|
434
425
|
});
|
|
426
|
+
this.profileData.parseTime = now() - startedAt;
|
|
435
427
|
return this.compileAST(ast);
|
|
436
428
|
}
|
|
437
429
|
compileAST(ast) {
|
|
430
|
+
let startedAt = now();
|
|
438
431
|
this._compileMain(ast.program.body);
|
|
432
|
+
this.profileData.compileTime = now() - startedAt;
|
|
439
433
|
return this.bytecode;
|
|
440
434
|
}
|
|
441
435
|
|
|
442
|
-
//
|
|
436
|
+
// Function compilation
|
|
443
437
|
_compileFunctionDecl(node) {
|
|
438
|
+
const isArrow = node.type === "ArrowFunctionExpression";
|
|
444
439
|
ok(!node.generator, "Generator functions are not supported");
|
|
445
440
|
ok(!node.async, "Async functions are not supported");
|
|
441
|
+
|
|
442
|
+
// Arrow functions do NOT bind their own `this` or `arguments`; both are
|
|
443
|
+
// inherited lexically from the nearest enclosing non-arrow function. We
|
|
444
|
+
// model this with the ordinary upvalue machinery: a non-arrow function
|
|
445
|
+
// materializes its receiver into a hidden `this` local (see the prologue
|
|
446
|
+
// below) and an arrow that references `this`/`arguments` simply resolves the
|
|
447
|
+
// name up the scope chain, capturing it as an upvalue. The result is that an
|
|
448
|
+
// arrow's MAKE_CLOSURE is byte-for-byte indistinguishable from any other
|
|
449
|
+
// nested closure — there is no "arrow" marker anywhere in the bytecode.
|
|
450
|
+
// `node.body` may be an Expression (concise body: `x => x + 1`) rather than
|
|
451
|
+
// a BlockStatement.
|
|
452
|
+
const isBlockBody = node.body.type === "BlockStatement";
|
|
446
453
|
var fnIdx = this.fnDescriptors.length;
|
|
447
454
|
const entryLabel = this._makeLabel(`fn_${fnIdx}`);
|
|
448
455
|
var desc = {};
|
|
@@ -454,17 +461,54 @@ export class Compiler {
|
|
|
454
461
|
this._loopStack = [];
|
|
455
462
|
|
|
456
463
|
// 1. Define parameters as virtual registers (occupy the first IDs in order).
|
|
464
|
+
let hasRest = false;
|
|
457
465
|
for (const param of node.params) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
466
|
+
if (param.type === "RestElement") {
|
|
467
|
+
ok(param.argument.type === "Identifier", "Rest element must be a simple identifier");
|
|
468
|
+
hasRest = true;
|
|
469
|
+
ctx.scope.define(param.argument.name, ctx);
|
|
470
|
+
} else {
|
|
471
|
+
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
472
|
+
ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
|
|
473
|
+
ctx.scope.define(identifier.name, ctx);
|
|
474
|
+
}
|
|
461
475
|
}
|
|
462
476
|
|
|
463
|
-
// 2. Reserve the `arguments` virtual register (immediately after params)
|
|
464
|
-
|
|
477
|
+
// 2. Reserve the `arguments` virtual register (immediately after params)
|
|
478
|
+
// and a hidden `this` register (immediately after that). Order matters: the
|
|
479
|
+
// runtime writes the args array into slot `paramCount`, so `arguments` must
|
|
480
|
+
// keep that slot and `this` follows it. Arrow functions bind neither — a
|
|
481
|
+
// reference climbs the scope chain to the enclosing function's register.
|
|
482
|
+
if (!isArrow) {
|
|
483
|
+
ctx.scope.define("arguments", ctx);
|
|
484
|
+
const thisReg = ctx.scope.define("this", ctx);
|
|
485
|
+
// Prologue: materialize the receiver (frame.thisVal) into the hidden
|
|
486
|
+
// `this` local so it reads like any other register and can be captured as
|
|
487
|
+
// an upvalue by nested arrows. This is the only place LOAD_THIS is now
|
|
488
|
+
// emitted, so `this` usage sites become generic register reads.
|
|
489
|
+
this.emit(ctx.bc, [this.OP.LOAD_THIS, thisReg], node);
|
|
490
|
+
}
|
|
465
491
|
|
|
466
492
|
// 3. Hoist all var declarations so locals are allocated before any temps.
|
|
467
|
-
|
|
493
|
+
// Concise-body arrows have an expression body with no statements to hoist.
|
|
494
|
+
if (isBlockBody) {
|
|
495
|
+
this._hoistVars(node.body.body, ctx.scope, ctx);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 4. Hoist function declarations: compile and emit MAKE_CLOSURE at function
|
|
499
|
+
// entry so they are available before any code in the body runs.
|
|
500
|
+
if (isBlockBody) {
|
|
501
|
+
const hoistedFnDecls = this._collectHoistedFunctions(node.body.body);
|
|
502
|
+
for (const fnDecl of hoistedFnDecls) {
|
|
503
|
+
const fnDesc = this._compileFunctionDecl(fnDecl);
|
|
504
|
+
fnDecl._hoistedDesc = fnDesc;
|
|
505
|
+
const closureReg = this._emitMakeClosure(fnDesc, fnDecl, ctx.bc);
|
|
506
|
+
const slot = ctx.scope._locals.get(fnDecl.id.name);
|
|
507
|
+
if (closureReg !== slot) {
|
|
508
|
+
this.emit(ctx.bc, [this.OP.MOVE, slot, closureReg], fnDecl);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
468
512
|
|
|
469
513
|
// 5. Emit default-value guards.
|
|
470
514
|
for (const param of node.params) {
|
|
@@ -494,14 +538,20 @@ export class Compiler {
|
|
|
494
538
|
}
|
|
495
539
|
|
|
496
540
|
// 6. Compile body.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
541
|
+
if (!isBlockBody) {
|
|
542
|
+
// Concise-body arrow: `(...) => expr` is equivalent to `{ return expr }`.
|
|
543
|
+
const reg = this._compileExpr(node.body, ctx.scope, ctx.bc);
|
|
544
|
+
this.emit(ctx.bc, [this.OP.RETURN, reg], node);
|
|
545
|
+
} else {
|
|
546
|
+
for (const stmt of node.body.body) {
|
|
547
|
+
this._compileStatement(stmt, ctx.scope, ctx.bc);
|
|
548
|
+
}
|
|
500
549
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
550
|
+
// Implicit return undefined at end of function.
|
|
551
|
+
const reg_undef = ctx.allocReg();
|
|
552
|
+
this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], node);
|
|
553
|
+
this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
|
|
554
|
+
}
|
|
505
555
|
this._currentCtx = savedCtx;
|
|
506
556
|
this._loopStack = savedLoopStack;
|
|
507
557
|
node._fnIdx = fnIdx;
|
|
@@ -510,6 +560,14 @@ export class Compiler {
|
|
|
510
560
|
desc.bytecode = ctx.bc;
|
|
511
561
|
desc._fnIdx = fnIdx;
|
|
512
562
|
desc.paramCount = node.params.length;
|
|
563
|
+
// Leading locals whose slots are fixed by position and written by the
|
|
564
|
+
// runtime at call time: the params (slots 0..paramCount-1), plus — for
|
|
565
|
+
// non-arrow functions — `arguments` (slot paramCount) and the hidden `this`
|
|
566
|
+
// (slot paramCount+1). These MUST get an identity slot mapping even when
|
|
567
|
+
// unused, otherwise a later local/upvalue capture slides into a param slot
|
|
568
|
+
// and the runtime's fixed-slot writes corrupt it. See resolveRegisters().
|
|
569
|
+
desc.reservedRegisters = node.params.length + (isArrow ? 0 : 2);
|
|
570
|
+
desc.hasRest = hasRest;
|
|
513
571
|
// regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
|
|
514
572
|
desc.upvalues = ctx.upvalues.slice();
|
|
515
573
|
desc.ctx = ctx;
|
|
@@ -532,11 +590,98 @@ export class Compiler {
|
|
|
532
590
|
label: desc.entryLabel
|
|
533
591
|
}, desc.paramCount, b.fnRegCountOperand(desc._fnIdx),
|
|
534
592
|
// resolved by resolveRegisters()
|
|
535
|
-
desc.upvalues.length,
|
|
593
|
+
desc.upvalues.length, desc.hasRest ? 1 : 0,
|
|
594
|
+
// 1 = last param is a rest element
|
|
595
|
+
...uvOperands], node);
|
|
536
596
|
return dst;
|
|
537
597
|
}
|
|
538
598
|
|
|
539
|
-
//
|
|
599
|
+
// Load a label's resolved PC into a register (resolveLabels fills the value).
|
|
600
|
+
// Used to seed a finalizer's continuation register with a resume target.
|
|
601
|
+
_emitLoadLabel(bc, reg, label, node) {
|
|
602
|
+
this.emit(bc, [this.OP.LOAD_INT, reg, {
|
|
603
|
+
type: "label",
|
|
604
|
+
label
|
|
605
|
+
}], node);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Abrupt-completion unwinding
|
|
609
|
+
// Emits the bytecode that carries an abrupt completion (return / break /
|
|
610
|
+
// continue) out through every enclosing handler on the loop stack:
|
|
611
|
+
// • a "try" (catch-only) region is disarmed with TRY_END and we keep going;
|
|
612
|
+
// • a "finally" region is *routed through* — control is sent to the
|
|
613
|
+
// finalizer first and the remainder of the unwind resumes from the
|
|
614
|
+
// finalizer's continuation pad (see _routeThroughFinally);
|
|
615
|
+
// • loop/switch/block frames are skipped until we reach the break/continue
|
|
616
|
+
// target, where we emit the final JUMP.
|
|
617
|
+
// For a return with no enclosing finalizer this degenerates to the original
|
|
618
|
+
// "TRY_END per crossed try, then RETURN" behavior.
|
|
619
|
+
//
|
|
620
|
+
// action:
|
|
621
|
+
// { kind: "return", valueReg }
|
|
622
|
+
// { kind: "break" | "continue", targetEntry } (targetEntry is the loop-
|
|
623
|
+
// stack record to jump to; identified by object identity so it stays
|
|
624
|
+
// valid even when re-walked from a finalizer pad after the stack shrank)
|
|
625
|
+
_emitUnwind(bc, node, action) {
|
|
626
|
+
const stack = this._loopStack;
|
|
627
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
628
|
+
const entry = stack[i];
|
|
629
|
+
if (action.kind !== "return" && entry === action.targetEntry) {
|
|
630
|
+
const label = action.kind === "break" ? entry.breakLabel : entry.continueLabel;
|
|
631
|
+
this.emit(bc, [this.OP.JUMP, {
|
|
632
|
+
type: "label",
|
|
633
|
+
label
|
|
634
|
+
}], node);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (entry.type === "finally") {
|
|
638
|
+
this._routeThroughFinally(bc, node, entry, action);
|
|
639
|
+
return; // the finalizer's pad continues the unwind
|
|
640
|
+
}
|
|
641
|
+
if (entry.type === "try") {
|
|
642
|
+
this.emit(bc, [this.OP.TRY_END], node);
|
|
643
|
+
}
|
|
644
|
+
// loop / switch / block frames that aren't the target are just skipped.
|
|
645
|
+
}
|
|
646
|
+
if (action.kind === "return") {
|
|
647
|
+
this.emit(bc, [this.OP.RETURN, action.valueReg], node);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Divert an abrupt completion into a finalizer. Schedules a continuation pad
|
|
652
|
+
// (emitted after the finalizer body) that re-issues the completion from the
|
|
653
|
+
// enclosing context, then sets the resume target + pending value and jumps to
|
|
654
|
+
// the finalizer. TRY_END disarms the finalizer's runtime record so an
|
|
655
|
+
// exception raised inside it doesn't loop back to itself.
|
|
656
|
+
_routeThroughFinally(bc, node, entry, action) {
|
|
657
|
+
const padLabel = this._makeLabel("finally_cont");
|
|
658
|
+
let contAction;
|
|
659
|
+
if (action.kind === "return") {
|
|
660
|
+
// Park the return value in the finalizer's payload register so it
|
|
661
|
+
// survives the finalizer body; the pad resumes the return from there.
|
|
662
|
+
if (action.valueReg !== entry.payloadReg) {
|
|
663
|
+
this.emit(bc, [this.OP.MOVE, entry.payloadReg, action.valueReg], node);
|
|
664
|
+
}
|
|
665
|
+
contAction = {
|
|
666
|
+
kind: "return",
|
|
667
|
+
valueReg: entry.payloadReg
|
|
668
|
+
};
|
|
669
|
+
} else {
|
|
670
|
+
contAction = action;
|
|
671
|
+
}
|
|
672
|
+
entry.pads.push({
|
|
673
|
+
label: padLabel,
|
|
674
|
+
emit: () => this._emitUnwind(bc, node, contAction)
|
|
675
|
+
});
|
|
676
|
+
this._emitLoadLabel(bc, entry.contReg, padLabel, node);
|
|
677
|
+
this.emit(bc, [this.OP.TRY_END], node); // disarm this finalizer's record
|
|
678
|
+
this.emit(bc, [this.OP.JUMP, {
|
|
679
|
+
type: "label",
|
|
680
|
+
label: entry.finallyLabel
|
|
681
|
+
}], node);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Main (top-level)
|
|
540
685
|
_compileMain(body) {
|
|
541
686
|
const mainCtx = new FnContext(this, null);
|
|
542
687
|
const savedCtx = this._currentCtx;
|
|
@@ -564,7 +709,7 @@ export class Compiler {
|
|
|
564
709
|
this._currentCtx = savedCtx;
|
|
565
710
|
}
|
|
566
711
|
|
|
567
|
-
//
|
|
712
|
+
// Statements
|
|
568
713
|
// Wrapper that resets temps after every statement so that short-lived
|
|
569
714
|
// expression temps don't accumulate across statements.
|
|
570
715
|
_compileStatement(node, scope, bc) {
|
|
@@ -586,6 +731,8 @@ export class Compiler {
|
|
|
586
731
|
break;
|
|
587
732
|
case "FunctionDeclaration":
|
|
588
733
|
{
|
|
734
|
+
// Already hoisted and emitted at function entry — skip.
|
|
735
|
+
if (node._hoistedDesc) break;
|
|
589
736
|
const desc = this._compileFunctionDecl(node);
|
|
590
737
|
const closureReg = this._emitMakeClosure(desc, node, bc);
|
|
591
738
|
if (scope) {
|
|
@@ -613,12 +760,12 @@ export class Compiler {
|
|
|
613
760
|
reg = ctx.allocReg();
|
|
614
761
|
this.emit(bc, [this.OP.LOAD_CONST, reg, b.constantOperand(undefined)], node);
|
|
615
762
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
763
|
+
// Unwind through enclosing try/finally regions: disarm catch handlers
|
|
764
|
+
// and route through any finalizer before the value is actually returned.
|
|
765
|
+
this._emitUnwind(bc, node, {
|
|
766
|
+
kind: "return",
|
|
767
|
+
valueReg: reg
|
|
768
|
+
});
|
|
622
769
|
break;
|
|
623
770
|
}
|
|
624
771
|
case "ExpressionStatement":
|
|
@@ -838,22 +985,19 @@ export class Compiler {
|
|
|
838
985
|
if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
|
|
839
986
|
} else {
|
|
840
987
|
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
841
|
-
|
|
988
|
+
const _t = this._loopStack[_bi].type;
|
|
989
|
+
if (_t !== "try" && _t !== "finally") {
|
|
842
990
|
_bTargetIdx = _bi;
|
|
843
991
|
break;
|
|
844
992
|
}
|
|
845
993
|
}
|
|
846
994
|
if (_bTargetIdx === -1) throw new Error("break outside loop");
|
|
847
995
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
this.emit(bc, [this.OP.JUMP, {
|
|
854
|
-
type: "label",
|
|
855
|
-
label: this._loopStack[_bTargetIdx].breakLabel
|
|
856
|
-
}], node);
|
|
996
|
+
// Disarm catch handlers and route through finalizers on the way out.
|
|
997
|
+
this._emitUnwind(bc, node, {
|
|
998
|
+
kind: "break",
|
|
999
|
+
targetEntry: this._loopStack[_bTargetIdx]
|
|
1000
|
+
});
|
|
857
1001
|
break;
|
|
858
1002
|
}
|
|
859
1003
|
case "ContinueStatement":
|
|
@@ -877,15 +1021,11 @@ export class Compiler {
|
|
|
877
1021
|
}
|
|
878
1022
|
if (_cTargetIdx === -1) throw new Error("continue outside loop");
|
|
879
1023
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
this.emit(bc, [this.OP.JUMP, {
|
|
886
|
-
type: "label",
|
|
887
|
-
label: this._loopStack[_cTargetIdx].continueLabel
|
|
888
|
-
}], node);
|
|
1024
|
+
// Disarm catch handlers and route through finalizers on the way out.
|
|
1025
|
+
this._emitUnwind(bc, node, {
|
|
1026
|
+
kind: "continue",
|
|
1027
|
+
targetEntry: this._loopStack[_cTargetIdx]
|
|
1028
|
+
});
|
|
889
1029
|
break;
|
|
890
1030
|
}
|
|
891
1031
|
case "SwitchStatement":
|
|
@@ -1048,51 +1188,139 @@ export class Compiler {
|
|
|
1048
1188
|
}
|
|
1049
1189
|
case "TryStatement":
|
|
1050
1190
|
{
|
|
1051
|
-
if (node.finalizer) {
|
|
1052
|
-
throw new Error("try
|
|
1191
|
+
if (!node.handler && !node.finalizer) {
|
|
1192
|
+
throw new Error("try without catch or finally is not supported");
|
|
1053
1193
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1194
|
+
|
|
1195
|
+
// Emits the inner try[/catch] region. When there is a finalizer this is
|
|
1196
|
+
// nested *inside* the finally region (CPython-style desugaring of
|
|
1197
|
+
// try/catch/finally into try { try/catch } finally), so each runtime
|
|
1198
|
+
// handler record is purely a catch or purely a finally — never both.
|
|
1199
|
+
const emitTryCatch = () => {
|
|
1200
|
+
if (!node.handler) {
|
|
1201
|
+
// try { … } finally { … } — no catch, just run the protected body.
|
|
1202
|
+
for (const stmt of node.block.body) {
|
|
1203
|
+
this._compileStatement(stmt, scope, bc);
|
|
1204
|
+
}
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const catchLabel = this._makeLabel("catch");
|
|
1208
|
+
const afterCatchLabel = this._makeLabel("after_catch");
|
|
1209
|
+
|
|
1210
|
+
// Determine where the caught exception is written.
|
|
1211
|
+
const exceptionReg = node.handler.param?.type === "Identifier" ? scope?._locals.get(node.handler.param.name) ?? ctx.allocReg() // shouldn't normally reach here
|
|
1212
|
+
: node._exceptionSlot;
|
|
1213
|
+
this.emit(bc, [this.OP.TRY_SETUP, {
|
|
1214
|
+
type: "label",
|
|
1215
|
+
label: catchLabel
|
|
1216
|
+
}, exceptionReg], node);
|
|
1217
|
+
this._loopStack.push({
|
|
1218
|
+
type: "try",
|
|
1219
|
+
label: null,
|
|
1220
|
+
breakLabel: "",
|
|
1221
|
+
continueLabel: ""
|
|
1222
|
+
});
|
|
1223
|
+
for (const stmt of node.block.body) {
|
|
1224
|
+
this._compileStatement(stmt, scope, bc);
|
|
1225
|
+
}
|
|
1226
|
+
this._loopStack.pop();
|
|
1227
|
+
this.emit(bc, [this.OP.TRY_END], node);
|
|
1228
|
+
this.emit(bc, [this.OP.JUMP, {
|
|
1229
|
+
type: "label",
|
|
1230
|
+
label: afterCatchLabel
|
|
1231
|
+
}], node);
|
|
1232
|
+
|
|
1233
|
+
// Catch block: exceptionReg already holds the caught value.
|
|
1234
|
+
this.emit(bc, [null, {
|
|
1235
|
+
type: "defineLabel",
|
|
1236
|
+
label: catchLabel
|
|
1237
|
+
}], node);
|
|
1238
|
+
|
|
1239
|
+
// If no param binding, just ignore the exception (it's in the dummy slot).
|
|
1240
|
+
for (const stmt of node.handler.body.body) {
|
|
1241
|
+
this._compileStatement(stmt, scope, bc);
|
|
1242
|
+
}
|
|
1243
|
+
this.emit(bc, [null, {
|
|
1244
|
+
type: "defineLabel",
|
|
1245
|
+
label: afterCatchLabel
|
|
1246
|
+
}], node);
|
|
1247
|
+
};
|
|
1248
|
+
if (!node.finalizer) {
|
|
1249
|
+
emitTryCatch();
|
|
1250
|
+
break;
|
|
1056
1251
|
}
|
|
1057
|
-
const catchLabel = this._makeLabel("catch");
|
|
1058
|
-
const afterCatchLabel = this._makeLabel("after_catch");
|
|
1059
1252
|
|
|
1060
|
-
//
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
this.
|
|
1253
|
+
// try [catch] finally
|
|
1254
|
+
const finallyLabel = this._makeLabel("finally");
|
|
1255
|
+
const afterFinallyLabel = this._makeLabel("after_finally");
|
|
1256
|
+
const throwPadLabel = this._makeLabel("finally_throw");
|
|
1257
|
+
const contReg = node._finallyContReg;
|
|
1258
|
+
const payloadReg = node._finallyPayloadReg;
|
|
1259
|
+
|
|
1260
|
+
// Arm the finalizer for the whole protected region (try + catch).
|
|
1261
|
+
this.emit(bc, [this.OP.FINALLY_SETUP, {
|
|
1064
1262
|
type: "label",
|
|
1065
|
-
label:
|
|
1066
|
-
},
|
|
1067
|
-
|
|
1068
|
-
|
|
1263
|
+
label: finallyLabel
|
|
1264
|
+
}, contReg, payloadReg, {
|
|
1265
|
+
type: "label",
|
|
1266
|
+
label: throwPadLabel
|
|
1267
|
+
}], node);
|
|
1268
|
+
const finallyEntry = {
|
|
1269
|
+
type: "finally",
|
|
1069
1270
|
label: null,
|
|
1070
1271
|
breakLabel: "",
|
|
1071
|
-
continueLabel: ""
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1272
|
+
continueLabel: "",
|
|
1273
|
+
finallyLabel,
|
|
1274
|
+
contReg,
|
|
1275
|
+
payloadReg,
|
|
1276
|
+
pads: []
|
|
1277
|
+
};
|
|
1278
|
+
this._loopStack.push(finallyEntry);
|
|
1279
|
+
emitTryCatch();
|
|
1280
|
+
this._loopStack.pop(); // leaving the protected region
|
|
1281
|
+
|
|
1282
|
+
// Normal completion: disarm the finalizer, set resume = after the whole
|
|
1283
|
+
// statement, then fall into the finalizer body.
|
|
1077
1284
|
this.emit(bc, [this.OP.TRY_END], node);
|
|
1285
|
+
this._emitLoadLabel(bc, contReg, afterFinallyLabel, node);
|
|
1078
1286
|
this.emit(bc, [this.OP.JUMP, {
|
|
1079
1287
|
type: "label",
|
|
1080
|
-
label:
|
|
1288
|
+
label: finallyLabel
|
|
1081
1289
|
}], node);
|
|
1082
1290
|
|
|
1083
|
-
//
|
|
1291
|
+
// Finalizer body (compiled once). Runs with the enclosing context on
|
|
1292
|
+
// the loop stack, so an abrupt completion inside it routes outward
|
|
1293
|
+
// (overriding any pending completion — correct JS semantics).
|
|
1084
1294
|
this.emit(bc, [null, {
|
|
1085
1295
|
type: "defineLabel",
|
|
1086
|
-
label:
|
|
1296
|
+
label: finallyLabel
|
|
1087
1297
|
}], node);
|
|
1088
|
-
|
|
1089
|
-
// If no param binding, just ignore the exception (it's in the dummy slot).
|
|
1090
|
-
for (const stmt of node.handler.body.body) {
|
|
1298
|
+
for (const stmt of node.finalizer.body) {
|
|
1091
1299
|
this._compileStatement(stmt, scope, bc);
|
|
1092
1300
|
}
|
|
1301
|
+
// END_FINALLY: resume at whatever the continuation register points to.
|
|
1302
|
+
this.emit(bc, [this.OP.JUMP_REG, contReg], node);
|
|
1303
|
+
|
|
1304
|
+
// Throw pad: re-raise the in-flight exception after the finalizer.
|
|
1305
|
+
this.emit(bc, [null, {
|
|
1306
|
+
type: "defineLabel",
|
|
1307
|
+
label: throwPadLabel
|
|
1308
|
+
}], node);
|
|
1309
|
+
this.emit(bc, [this.OP.THROW, payloadReg], node);
|
|
1310
|
+
|
|
1311
|
+
// Continuation pads for return/break/continue that crossed this
|
|
1312
|
+
// finalizer (collected during body compilation, emitted now that the
|
|
1313
|
+
// enclosing loop-stack context is restored).
|
|
1314
|
+
for (const pad of finallyEntry.pads) {
|
|
1315
|
+
this.emit(bc, [null, {
|
|
1316
|
+
type: "defineLabel",
|
|
1317
|
+
label: pad.label
|
|
1318
|
+
}], node);
|
|
1319
|
+
pad.emit();
|
|
1320
|
+
}
|
|
1093
1321
|
this.emit(bc, [null, {
|
|
1094
1322
|
type: "defineLabel",
|
|
1095
|
-
label:
|
|
1323
|
+
label: afterFinallyLabel
|
|
1096
1324
|
}], node);
|
|
1097
1325
|
break;
|
|
1098
1326
|
}
|
|
@@ -1104,7 +1332,65 @@ export class Compiler {
|
|
|
1104
1332
|
}
|
|
1105
1333
|
}
|
|
1106
1334
|
|
|
1107
|
-
//
|
|
1335
|
+
// Returns true if any element in an argument/element list is a SpreadElement.
|
|
1336
|
+
_hasSpread(args) {
|
|
1337
|
+
return args.some(a => a != null && a.type === "SpreadElement");
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Build a flat argument array at runtime when the call contains spread elements.
|
|
1341
|
+
// Returns a register holding an Array with all arguments flattened.
|
|
1342
|
+
// Strategy: build a prefix array from leading non-spread elements, then
|
|
1343
|
+
// repeatedly call Array.prototype.concat — spread elements are concat'd directly
|
|
1344
|
+
// (concat spreads array args one level), non-spread elements are wrapped in a
|
|
1345
|
+
// single-element array before concat so they aren't spread.
|
|
1346
|
+
_buildSpreadArgs(args, scope, bc, node) {
|
|
1347
|
+
const ctx = this._currentCtx;
|
|
1348
|
+
const firstSpreadIdx = args.findIndex(a => a != null && a.type === "SpreadElement");
|
|
1349
|
+
|
|
1350
|
+
// Build initial array from non-spread prefix (may be empty).
|
|
1351
|
+
const prefix = args.slice(0, firstSpreadIdx);
|
|
1352
|
+
const prefixRegs = prefix.map(a => {
|
|
1353
|
+
if (a === null) {
|
|
1354
|
+
const r = ctx.allocReg();
|
|
1355
|
+
this.emit(bc, [this.OP.LOAD_CONST, r, b.constantOperand(undefined)], node);
|
|
1356
|
+
return r;
|
|
1357
|
+
}
|
|
1358
|
+
return this._compileExpr(a, scope, bc);
|
|
1359
|
+
});
|
|
1360
|
+
let accReg = ctx.allocReg();
|
|
1361
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, accReg, prefix.length, ...prefixRegs], node);
|
|
1362
|
+
|
|
1363
|
+
// Process each remaining arg via Array.prototype.concat.
|
|
1364
|
+
for (let i = firstSpreadIdx; i < args.length; i++) {
|
|
1365
|
+
const arg = args[i];
|
|
1366
|
+
const concatKeyReg = ctx.allocReg();
|
|
1367
|
+
this.emit(bc, [this.OP.LOAD_CONST, concatKeyReg, b.constantOperand("concat")], node);
|
|
1368
|
+
const concatFnReg = ctx.allocReg();
|
|
1369
|
+
this.emit(bc, [this.OP.GET_PROP, concatFnReg, accReg, concatKeyReg], node);
|
|
1370
|
+
let argArrReg;
|
|
1371
|
+
if (arg === null) {
|
|
1372
|
+
// Array hole — treat as undefined wrapped in [undefined]
|
|
1373
|
+
const elemReg = ctx.allocReg();
|
|
1374
|
+
this.emit(bc, [this.OP.LOAD_CONST, elemReg, b.constantOperand(undefined)], node);
|
|
1375
|
+
argArrReg = ctx.allocReg();
|
|
1376
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, argArrReg, 1, elemReg], node);
|
|
1377
|
+
} else if (arg.type === "SpreadElement") {
|
|
1378
|
+
// Spread: concat the iterable directly (concat flattens one level).
|
|
1379
|
+
argArrReg = this._compileExpr(arg.argument, scope, bc);
|
|
1380
|
+
} else {
|
|
1381
|
+
// Non-spread: wrap in [elem] so concat doesn't flatten the value.
|
|
1382
|
+
const elemReg = this._compileExpr(arg, scope, bc);
|
|
1383
|
+
argArrReg = ctx.allocReg();
|
|
1384
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, argArrReg, 1, elemReg], node);
|
|
1385
|
+
}
|
|
1386
|
+
const newAccReg = ctx.allocReg();
|
|
1387
|
+
this.emit(bc, [this.OP.CALL_METHOD, newAccReg, accReg, concatFnReg, 1, argArrReg], node);
|
|
1388
|
+
accReg = newAccReg;
|
|
1389
|
+
}
|
|
1390
|
+
return accReg;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Expressions
|
|
1108
1394
|
// Returns the virtual RegisterOperand that holds the result.
|
|
1109
1395
|
// For local variables: returns their RegisterOperand directly (no instruction emitted).
|
|
1110
1396
|
// For all others: allocates a fresh virtual register, emits the instruction(s),
|
|
@@ -1124,6 +1410,19 @@ export class Compiler {
|
|
|
1124
1410
|
|
|
1125
1411
|
return dst;
|
|
1126
1412
|
}
|
|
1413
|
+
|
|
1414
|
+
// _VM_JUMP_("labelName") — emits JUMP with a label operand.
|
|
1415
|
+
// Used by bytecode transforms (e.g. CFF) via Template to express jumps
|
|
1416
|
+
// to labels that exist in the parent compiler's bytecode stream.
|
|
1417
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_JUMP_") {
|
|
1418
|
+
const label = node.arguments[0].value;
|
|
1419
|
+
bc.push([this.OP.JUMP, {
|
|
1420
|
+
type: "label",
|
|
1421
|
+
label
|
|
1422
|
+
}]);
|
|
1423
|
+
// Return a dummy register — caller (ExpressionStatement) discards it.
|
|
1424
|
+
return ctx.allocReg();
|
|
1425
|
+
}
|
|
1127
1426
|
switch (node.type) {
|
|
1128
1427
|
case "NumericLiteral":
|
|
1129
1428
|
case "StringLiteral":
|
|
@@ -1155,16 +1454,37 @@ export class Compiler {
|
|
|
1155
1454
|
}
|
|
1156
1455
|
case "ThisExpression":
|
|
1157
1456
|
{
|
|
1457
|
+
// `this` is resolved like any ordinary binding. In a non-arrow function
|
|
1458
|
+
// it is a hidden local (materialized by the entry prologue); in an arrow
|
|
1459
|
+
// it climbs to the enclosing function and becomes an upvalue read. Either
|
|
1460
|
+
// way the usage site is a generic register/upvalue access, not a
|
|
1461
|
+
// semantically-revealing LOAD_THIS.
|
|
1462
|
+
const res = this._resolve("this", this._currentCtx);
|
|
1463
|
+
if (res.kind === "local") return res.reg; // register IS the local
|
|
1464
|
+
if (res.kind === "upvalue") {
|
|
1465
|
+
const dst = ctx.allocReg();
|
|
1466
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
|
|
1467
|
+
return dst;
|
|
1468
|
+
}
|
|
1469
|
+
// Fallback (no enclosing `this` binding, e.g. a stray top-level use):
|
|
1470
|
+
// read the frame receiver directly.
|
|
1158
1471
|
const dst = ctx.allocReg();
|
|
1159
1472
|
this.emit(bc, [this.OP.LOAD_THIS, dst], node);
|
|
1160
1473
|
return dst;
|
|
1161
1474
|
}
|
|
1162
1475
|
case "NewExpression":
|
|
1163
1476
|
{
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1477
|
+
const n = node;
|
|
1478
|
+
ok(n.arguments.length < U16_MAX, `Too many arguments (max ${U16_MAX - 1})`);
|
|
1479
|
+
const calleeReg = this._compileExpr(n.callee, scope, bc);
|
|
1166
1480
|
const dst = ctx.allocReg();
|
|
1167
|
-
this.
|
|
1481
|
+
if (this._hasSpread(n.arguments)) {
|
|
1482
|
+
const argsArrayReg = this._buildSpreadArgs(n.arguments, scope, bc, node);
|
|
1483
|
+
this.emit(bc, [this.OP.NEW, dst, calleeReg, this.SENTINELS.CALL_SPREAD, argsArrayReg], node);
|
|
1484
|
+
} else {
|
|
1485
|
+
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1486
|
+
this.emit(bc, [this.OP.NEW, dst, calleeReg, n.arguments.length, ...argRegs], node);
|
|
1487
|
+
}
|
|
1168
1488
|
return dst;
|
|
1169
1489
|
}
|
|
1170
1490
|
case "SequenceExpression":
|
|
@@ -1216,17 +1536,32 @@ export class Compiler {
|
|
|
1216
1536
|
const n = node;
|
|
1217
1537
|
const endLabel = this._makeLabel("logical_end");
|
|
1218
1538
|
const isOr = n.operator === "||";
|
|
1219
|
-
|
|
1539
|
+
const isNullish = n.operator === "??";
|
|
1540
|
+
if (!isOr && !isNullish && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
|
|
1220
1541
|
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1221
1542
|
const reg_result = ctx.allocReg();
|
|
1222
1543
|
if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1544
|
+
if (isNullish) {
|
|
1545
|
+
// a ?? b — keep LHS unless it is null or undefined, otherwise use RHS.
|
|
1546
|
+
// `reg_result == null` (loose) is true for exactly null and undefined,
|
|
1547
|
+
// which is precisely the set of "nullish" values.
|
|
1548
|
+
const nullReg = ctx.allocReg();
|
|
1549
|
+
this.emit(bc, [this.OP.LOAD_CONST, nullReg, b.constantOperand(null)], node);
|
|
1550
|
+
const isNullishReg = ctx.allocReg();
|
|
1551
|
+
this.emit(bc, [this.OP.LOOSE_EQ, isNullishReg, reg_result, nullReg], node);
|
|
1552
|
+
// Not nullish → keep LHS and skip RHS.
|
|
1553
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, isNullishReg, {
|
|
1554
|
+
type: "label",
|
|
1555
|
+
label: endLabel
|
|
1556
|
+
}], node);
|
|
1557
|
+
} else {
|
|
1558
|
+
// For ||: if truthy keep LHS, jump past RHS.
|
|
1559
|
+
// For &&: if falsy keep LHS, jump past RHS.
|
|
1560
|
+
this.emit(bc, [isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE, reg_result, {
|
|
1561
|
+
type: "label",
|
|
1562
|
+
label: endLabel
|
|
1563
|
+
}], node);
|
|
1564
|
+
}
|
|
1230
1565
|
|
|
1231
1566
|
// Compile RHS into reg_result.
|
|
1232
1567
|
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
@@ -1268,6 +1603,7 @@ export class Compiler {
|
|
|
1268
1603
|
"*": this.OP.MUL,
|
|
1269
1604
|
"/": this.OP.DIV,
|
|
1270
1605
|
"%": this.OP.MOD,
|
|
1606
|
+
"**": this.OP.EXP,
|
|
1271
1607
|
"&": this.OP.BAND,
|
|
1272
1608
|
"|": this.OP.BOR,
|
|
1273
1609
|
"^": this.OP.BXOR,
|
|
@@ -1356,6 +1692,7 @@ export class Compiler {
|
|
|
1356
1692
|
"*=": this.OP.MUL,
|
|
1357
1693
|
"/=": this.OP.DIV,
|
|
1358
1694
|
"%=": this.OP.MOD,
|
|
1695
|
+
"**=": this.OP.EXP,
|
|
1359
1696
|
"&=": this.OP.BAND,
|
|
1360
1697
|
"|=": this.OP.BOR,
|
|
1361
1698
|
"^=": this.OP.BXOR,
|
|
@@ -1428,6 +1765,7 @@ export class Compiler {
|
|
|
1428
1765
|
case "CallExpression":
|
|
1429
1766
|
{
|
|
1430
1767
|
const n = node;
|
|
1768
|
+
ok(n.arguments.length < U16_MAX, `Too many arguments (max ${U16_MAX - 1})`);
|
|
1431
1769
|
if (n.callee.type === "MemberExpression") {
|
|
1432
1770
|
// Method call: receiver.method(args)
|
|
1433
1771
|
const receiverReg = this._compileExpr(n.callee.object, scope, bc);
|
|
@@ -1440,16 +1778,26 @@ export class Compiler {
|
|
|
1440
1778
|
}
|
|
1441
1779
|
const calleeReg = ctx.allocReg();
|
|
1442
1780
|
this.emit(bc, [this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg], node);
|
|
1443
|
-
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1444
1781
|
const dst = ctx.allocReg();
|
|
1445
|
-
this.
|
|
1782
|
+
if (this._hasSpread(n.arguments)) {
|
|
1783
|
+
const argsArrayReg = this._buildSpreadArgs(n.arguments, scope, bc, node);
|
|
1784
|
+
this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, this.SENTINELS.CALL_SPREAD, argsArrayReg], node);
|
|
1785
|
+
} else {
|
|
1786
|
+
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1787
|
+
this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, n.arguments.length, ...argRegs], node);
|
|
1788
|
+
}
|
|
1446
1789
|
return dst;
|
|
1447
1790
|
} else {
|
|
1448
1791
|
// Plain call: fn(args)
|
|
1449
1792
|
const calleeReg = this._compileExpr(n.callee, scope, bc);
|
|
1450
|
-
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1451
1793
|
const dst = ctx.allocReg();
|
|
1452
|
-
this.
|
|
1794
|
+
if (this._hasSpread(n.arguments)) {
|
|
1795
|
+
const argsArrayReg = this._buildSpreadArgs(n.arguments, scope, bc, node);
|
|
1796
|
+
this.emit(bc, [this.OP.CALL, dst, calleeReg, this.SENTINELS.CALL_SPREAD, argsArrayReg], node);
|
|
1797
|
+
} else {
|
|
1798
|
+
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1799
|
+
this.emit(bc, [this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs], node);
|
|
1800
|
+
}
|
|
1453
1801
|
return dst;
|
|
1454
1802
|
}
|
|
1455
1803
|
}
|
|
@@ -1524,6 +1872,14 @@ export class Compiler {
|
|
|
1524
1872
|
const desc = this._compileFunctionDecl(node);
|
|
1525
1873
|
return this._emitMakeClosure(desc, node, bc);
|
|
1526
1874
|
}
|
|
1875
|
+
case "ArrowFunctionExpression":
|
|
1876
|
+
{
|
|
1877
|
+
// Arrows compile through the same path as any other function. They differ
|
|
1878
|
+
// only in that they bind no `this`/`arguments` (handled in
|
|
1879
|
+
// _compileFunctionDecl), so the resulting closure is indistinguishable.
|
|
1880
|
+
const desc = this._compileFunctionDecl(node);
|
|
1881
|
+
return this._emitMakeClosure(desc, node, bc);
|
|
1882
|
+
}
|
|
1527
1883
|
case "MemberExpression":
|
|
1528
1884
|
{
|
|
1529
1885
|
const n = node;
|
|
@@ -1542,6 +1898,9 @@ export class Compiler {
|
|
|
1542
1898
|
case "ArrayExpression":
|
|
1543
1899
|
{
|
|
1544
1900
|
const n = node;
|
|
1901
|
+
if (this._hasSpread(n.elements)) {
|
|
1902
|
+
return this._buildSpreadArgs(n.elements, scope, bc, node);
|
|
1903
|
+
}
|
|
1545
1904
|
const elemRegs = n.elements.map(el => {
|
|
1546
1905
|
if (el === null) {
|
|
1547
1906
|
const r = ctx.allocReg();
|
|
@@ -1557,45 +1916,87 @@ export class Compiler {
|
|
|
1557
1916
|
case "ObjectExpression":
|
|
1558
1917
|
{
|
|
1559
1918
|
const n = node;
|
|
1560
|
-
const
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1919
|
+
const hasSpread = n.properties.some(p => p.type === "SpreadElement");
|
|
1920
|
+
const hasComputed = n.properties.some(p => (p.type === "ObjectProperty" || p.type === "ObjectMethod") && p.computed);
|
|
1921
|
+
const hasMethodShorthand = n.properties.some(p => p.type === "ObjectMethod" && p.kind === "method");
|
|
1922
|
+
|
|
1923
|
+
// Fast path: no spread, no computed keys, no method shorthands.
|
|
1924
|
+
// Uses BUILD_OBJECT for data properties then DEFINE_GETTER/SETTER for accessors.
|
|
1925
|
+
if (!hasSpread && !hasComputed && !hasMethodShorthand) {
|
|
1926
|
+
const regularProps = [];
|
|
1927
|
+
const accessorProps = [];
|
|
1928
|
+
for (const prop of n.properties) {
|
|
1929
|
+
if (prop.type === "ObjectMethod") accessorProps.push(prop);else regularProps.push(prop);
|
|
1930
|
+
}
|
|
1931
|
+
const pairRegs = [];
|
|
1932
|
+
for (const prop of regularProps) {
|
|
1933
|
+
const key = prop.key;
|
|
1934
|
+
let keyStr;
|
|
1935
|
+
if (key.type === "Identifier") keyStr = key.name;else if (key.type === "StringLiteral" || key.type === "NumericLiteral") keyStr = String(key.value);else throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1936
|
+
const keyReg = ctx.allocReg();
|
|
1937
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
|
|
1938
|
+
const valReg = this._compileExpr(prop.value, scope, bc);
|
|
1939
|
+
pairRegs.push(keyReg, valReg);
|
|
1573
1940
|
}
|
|
1941
|
+
const dst = ctx.allocReg();
|
|
1942
|
+
this.emit(bc, [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs], node);
|
|
1943
|
+
for (const prop of accessorProps) {
|
|
1944
|
+
const key = prop.key;
|
|
1945
|
+
let keyStr;
|
|
1946
|
+
if (key.type === "Identifier") keyStr = key.name;else if (key.type === "StringLiteral" || key.type === "NumericLiteral") keyStr = String(key.value);else throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1947
|
+
const keyReg = ctx.allocReg();
|
|
1948
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
|
|
1949
|
+
const fnReg = this._emitMakeClosure(this._compileFunctionDecl(prop), prop, bc);
|
|
1950
|
+
this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
|
|
1951
|
+
}
|
|
1952
|
+
return dst;
|
|
1574
1953
|
}
|
|
1575
1954
|
|
|
1576
|
-
//
|
|
1577
|
-
|
|
1578
|
-
for (const prop of regularProps) {
|
|
1579
|
-
let keyStr;
|
|
1580
|
-
const key = prop.key;
|
|
1581
|
-
if (key.type === "Identifier") keyStr = key.name;else if (key.type === "StringLiteral" || key.type === "NumericLiteral") keyStr = String(key.value);else throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1582
|
-
const keyReg = ctx.allocReg();
|
|
1583
|
-
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
|
|
1584
|
-
const valReg = this._compileExpr(prop.value, scope, bc);
|
|
1585
|
-
pairRegs.push(keyReg, valReg);
|
|
1586
|
-
}
|
|
1955
|
+
// General path: handles spread elements, computed keys, and method shorthands.
|
|
1956
|
+
// Builds an empty object then sets each property in source order.
|
|
1587
1957
|
const dst = ctx.allocReg();
|
|
1588
|
-
this.emit(bc, [this.OP.BUILD_OBJECT, dst,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1958
|
+
this.emit(bc, [this.OP.BUILD_OBJECT, dst, 0], node);
|
|
1959
|
+
for (const prop of n.properties) {
|
|
1960
|
+
if (prop.type === "SpreadElement") {
|
|
1961
|
+
// {…src} — copies own enumerable properties via Object.assign(dst, src).
|
|
1962
|
+
const objGlobalReg = ctx.allocReg();
|
|
1963
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, objGlobalReg, b.constantOperand("Object")], node);
|
|
1964
|
+
const assignKeyReg = ctx.allocReg();
|
|
1965
|
+
this.emit(bc, [this.OP.LOAD_CONST, assignKeyReg, b.constantOperand("assign")], node);
|
|
1966
|
+
const assignFnReg = ctx.allocReg();
|
|
1967
|
+
this.emit(bc, [this.OP.GET_PROP, assignFnReg, objGlobalReg, assignKeyReg], node);
|
|
1968
|
+
const spreadValReg = this._compileExpr(prop.argument, scope, bc);
|
|
1969
|
+
const _assignResultReg = ctx.allocReg();
|
|
1970
|
+
this.emit(bc, [this.OP.CALL_METHOD, _assignResultReg, objGlobalReg, assignFnReg, 2, dst, spreadValReg], node);
|
|
1971
|
+
} else {
|
|
1972
|
+
const p = prop;
|
|
1973
|
+
|
|
1974
|
+
// Resolve key: computed → evaluate expression; static → load constant.
|
|
1975
|
+
let keyReg;
|
|
1976
|
+
if (p.computed) {
|
|
1977
|
+
keyReg = this._compileExpr(p.key, scope, bc);
|
|
1978
|
+
} else {
|
|
1979
|
+
const key = p.key;
|
|
1980
|
+
let keyStr;
|
|
1981
|
+
if (key.type === "Identifier") keyStr = key.name;else if (key.type === "StringLiteral" || key.type === "NumericLiteral") keyStr = String(key.value);else throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1982
|
+
keyReg = ctx.allocReg();
|
|
1983
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
|
|
1984
|
+
}
|
|
1985
|
+
if (p.type === "ObjectMethod") {
|
|
1986
|
+
const fnReg = this._emitMakeClosure(this._compileFunctionDecl(p), p, bc);
|
|
1987
|
+
if (p.kind === "get") {
|
|
1988
|
+
this.emit(bc, [this.OP.DEFINE_GETTER, dst, keyReg, fnReg], node);
|
|
1989
|
+
} else if (p.kind === "set") {
|
|
1990
|
+
this.emit(bc, [this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
|
|
1991
|
+
} else {
|
|
1992
|
+
// method shorthand: {foo() {}} ≡ {foo: function() {}}
|
|
1993
|
+
this.emit(bc, [this.OP.SET_PROP, dst, keyReg, fnReg], node);
|
|
1994
|
+
}
|
|
1995
|
+
} else {
|
|
1996
|
+
const valReg = this._compileExpr(p.value, scope, bc);
|
|
1997
|
+
this.emit(bc, [this.OP.SET_PROP, dst, keyReg, valReg], node);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1599
2000
|
}
|
|
1600
2001
|
return dst;
|
|
1601
2002
|
}
|
|
@@ -1607,7 +2008,7 @@ export class Compiler {
|
|
|
1607
2008
|
}
|
|
1608
2009
|
}
|
|
1609
2010
|
|
|
1610
|
-
//
|
|
2011
|
+
// Serializer
|
|
1611
2012
|
class Serializer {
|
|
1612
2013
|
constructor(compiler) {
|
|
1613
2014
|
this.compiler = compiler;
|
|
@@ -1774,9 +2175,9 @@ class Serializer {
|
|
|
1774
2175
|
// Validate no opcode or operand exceeds u16 limit
|
|
1775
2176
|
for (const o of resolvedValues) {
|
|
1776
2177
|
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
1777
|
-
ok(o >= 0 && o <=
|
|
2178
|
+
ok(o >= 0 && o <= 0xffffffff, `Operand overflow (max 0xFFFFFFFF u32): ${o}`);
|
|
1778
2179
|
}
|
|
1779
|
-
ok(op >= 0 && op <=
|
|
2180
|
+
ok(op >= 0 && op <= 0xffffffff, `Opcode overflow (max 0xFFFFFFFF u32): ${op}`);
|
|
1780
2181
|
serialized.push(instr);
|
|
1781
2182
|
}
|
|
1782
2183
|
return {
|
|
@@ -1784,10 +2185,12 @@ class Serializer {
|
|
|
1784
2185
|
};
|
|
1785
2186
|
}
|
|
1786
2187
|
_encodeBytecode(flat) {
|
|
1787
|
-
const buf = new Uint8Array(flat.length *
|
|
2188
|
+
const buf = new Uint8Array(flat.length * 4);
|
|
1788
2189
|
flat.forEach((w, i) => {
|
|
1789
|
-
buf[i *
|
|
1790
|
-
buf[i *
|
|
2190
|
+
buf[i * 4] = w & 0xff;
|
|
2191
|
+
buf[i * 4 + 1] = w >>> 8 & 0xff;
|
|
2192
|
+
buf[i * 4 + 2] = w >>> 16 & 0xff;
|
|
2193
|
+
buf[i * 4 + 3] = w >>> 24 & 0xff;
|
|
1791
2194
|
});
|
|
1792
2195
|
return Buffer.from(buf).toString("base64");
|
|
1793
2196
|
}
|
|
@@ -1813,6 +2216,8 @@ class Serializer {
|
|
|
1813
2216
|
sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
|
|
1814
2217
|
const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
|
|
1815
2218
|
sections.push(`var OP = ${generate(object).code};`);
|
|
2219
|
+
const sentinelsObject = t.objectExpression(Object.entries(compiler.SENTINELS).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
|
|
2220
|
+
sections.push(`var SENTINELS = ${generate(sentinelsObject).code};`);
|
|
1816
2221
|
initBody.push(this._serializeConstants(constants));
|
|
1817
2222
|
sections = [...initBody, ...sections];
|
|
1818
2223
|
sections.push(VM_RUNTIME);
|
|
@@ -1820,33 +2225,71 @@ class Serializer {
|
|
|
1820
2225
|
}
|
|
1821
2226
|
}
|
|
1822
2227
|
export async function compileAndSerialize(sourceCode, options) {
|
|
2228
|
+
let obfuscationStartedAt = now();
|
|
1823
2229
|
const compiler = new Compiler(options);
|
|
1824
2230
|
let bytecode = compiler.compile(sourceCode);
|
|
2231
|
+
const passes = [];
|
|
2232
|
+
if (options.stringConcealing) {
|
|
2233
|
+
passes.push({
|
|
2234
|
+
pass: stringConcealing,
|
|
2235
|
+
name: "stringConcealing"
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
1825
2238
|
|
|
1826
|
-
//
|
|
1827
|
-
|
|
1828
|
-
|
|
2239
|
+
// CFF and Dispatcher both run before resolveRegisters and resolveLabels
|
|
2240
|
+
if (options.controlFlowFlattening) {
|
|
2241
|
+
passes.push({
|
|
2242
|
+
pass: controlFlowFlattening,
|
|
2243
|
+
name: "controlFlowFlattening"
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
1829
2246
|
if (options.dispatcher) {
|
|
1830
|
-
|
|
1831
|
-
|
|
2247
|
+
passes.push({
|
|
2248
|
+
pass: dispatcher,
|
|
2249
|
+
name: "dispatcher"
|
|
2250
|
+
});
|
|
1832
2251
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
2252
|
+
passes.push({
|
|
2253
|
+
pass: concealConstants,
|
|
2254
|
+
name: "concealConstants"
|
|
2255
|
+
});
|
|
1835
2256
|
if (options.specializedOpcodes) {
|
|
1836
|
-
passes.push(
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2257
|
+
passes.push({
|
|
2258
|
+
pass: specializedOpcodes,
|
|
2259
|
+
name: "specializedOpcodes"
|
|
2260
|
+
});
|
|
1840
2261
|
}
|
|
1841
2262
|
if (options.macroOpcodes) {
|
|
1842
|
-
passes.push(
|
|
2263
|
+
passes.push({
|
|
2264
|
+
pass: macroOpcodes,
|
|
2265
|
+
name: "macroOpcodes"
|
|
2266
|
+
});
|
|
1843
2267
|
}
|
|
1844
2268
|
if (options.aliasedOpcodes) {
|
|
1845
|
-
passes.push(
|
|
2269
|
+
passes.push({
|
|
2270
|
+
pass: aliasedOpcodes,
|
|
2271
|
+
name: "aliasedOpcodes"
|
|
2272
|
+
});
|
|
1846
2273
|
}
|
|
1847
|
-
|
|
2274
|
+
const timings = {};
|
|
2275
|
+
function runAndTime(pass, name) {
|
|
2276
|
+
const startedAt = now();
|
|
2277
|
+
compiler.log(`Running bytecode pass ${name}...`);
|
|
1848
2278
|
const passResult = pass(bytecode, compiler);
|
|
1849
2279
|
bytecode = passResult.bytecode;
|
|
2280
|
+
const endedAt = now();
|
|
2281
|
+
const elapsedMs = endedAt - startedAt;
|
|
2282
|
+
timings[name] = elapsedMs;
|
|
2283
|
+
compiler.profileData.transforms[name] = {
|
|
2284
|
+
transformTime: elapsedMs,
|
|
2285
|
+
bytecodeSize: bytecode.length,
|
|
2286
|
+
flatBytecodeSize: bytecode.flat().length
|
|
2287
|
+
};
|
|
2288
|
+
compiler.log(`Bytecode pass ${name} completed in ${Math.floor(elapsedMs)}ms`);
|
|
2289
|
+
return passResult;
|
|
2290
|
+
}
|
|
2291
|
+
for (const pass of passes) {
|
|
2292
|
+
runAndTime(pass.pass, pass.name);
|
|
1850
2293
|
}
|
|
1851
2294
|
|
|
1852
2295
|
// Resolve virtual registers to concrete slot indices and set regCount per fn.
|
|
@@ -1854,32 +2297,36 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1854
2297
|
// of the bytecode while leaving RETURN in place, splitting a function's code
|
|
1855
2298
|
// into two non-contiguous regions. Linear-scan liveness then sees incorrect
|
|
1856
2299
|
// firstUse/lastUse for registers that span the gap, causing slot collisions.
|
|
1857
|
-
const regsResult = resolveRegisters
|
|
2300
|
+
const regsResult = runAndTime(resolveRegisters, "resolveRegisters");
|
|
1858
2301
|
bytecode = regsResult.bytecode;
|
|
1859
2302
|
|
|
1860
2303
|
// selfModifying runs after register resolution so concrete slot indices are
|
|
1861
2304
|
// already in place; only label operands remain unresolved at this stage.
|
|
1862
2305
|
if (options.selfModifying) {
|
|
1863
|
-
const smResult = selfModifying
|
|
2306
|
+
const smResult = runAndTime(selfModifying, "selfModifying");
|
|
1864
2307
|
bytecode = smResult.bytecode;
|
|
1865
2308
|
}
|
|
1866
2309
|
|
|
1867
2310
|
// Resolve label references to flat bytecode indices.
|
|
1868
|
-
const labelsResult = resolveLabels
|
|
2311
|
+
const labelsResult = runAndTime(resolveLabels, "resolveLabels");
|
|
1869
2312
|
bytecode = labelsResult.bytecode;
|
|
1870
2313
|
|
|
1871
2314
|
// Set mainStartPc from the first function descriptor (or 0 for top-level start).
|
|
1872
2315
|
compiler.mainStartPc = compiler.mainFn.startPc;
|
|
1873
2316
|
|
|
1874
2317
|
// Resolve constant references to pool indices (+ conceal key operand).
|
|
1875
|
-
const constResult = resolveConstants
|
|
2318
|
+
const constResult = runAndTime(resolveConstants, "resolveConstants");
|
|
1876
2319
|
bytecode = constResult.bytecode;
|
|
1877
2320
|
compiler.constants = constResult.constants;
|
|
1878
2321
|
|
|
1879
2322
|
// Build and obfuscate the runtime.
|
|
1880
2323
|
const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
|
|
1881
2324
|
|
|
1882
|
-
//
|
|
2325
|
+
// for (const key of Object.keys(timings)) {
|
|
2326
|
+
// console.log(` ${key}: ${timings[key]}ms`);
|
|
2327
|
+
// }
|
|
2328
|
+
|
|
2329
|
+
// This part was purposefully pulled out Serializer as OP_NAME's get resolved during buildRuntime
|
|
1883
2330
|
// So for the most useful comments, it's ran absolutely last
|
|
1884
2331
|
// Tests also rely on correct comments so it's required
|
|
1885
2332
|
const generateBytecodeComment = () => {
|
|
@@ -1890,8 +2337,10 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1890
2337
|
}
|
|
1891
2338
|
return lines.join("\n");
|
|
1892
2339
|
};
|
|
1893
|
-
const code = await
|
|
2340
|
+
const code = await buildRuntime(runtimeSource, bytecode, options, compiler, generateBytecodeComment);
|
|
2341
|
+
compiler.profileData.obfuscationTime = now() - obfuscationStartedAt;
|
|
1894
2342
|
return {
|
|
1895
|
-
code
|
|
2343
|
+
code,
|
|
2344
|
+
profileData: compiler.profileData
|
|
1896
2345
|
};
|
|
1897
2346
|
}
|