js-confuser-vm 0.0.5 → 0.0.6
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/CHANGELOG.md +57 -2
- package/README.MD +186 -107
- package/dist/build-runtime.js +7 -1
- package/dist/compiler.js +801 -785
- package/dist/runtime.js +409 -332
- package/dist/transforms/bytecode/aliasedOpcodes.js +140 -0
- package/dist/transforms/bytecode/concealConstants.js +31 -0
- package/dist/transforms/bytecode/macroOpcodes.js +22 -10
- package/dist/transforms/bytecode/resolveContants.js +73 -10
- package/dist/transforms/bytecode/selfModifying.js +3 -2
- package/dist/transforms/bytecode/specializedOpcodes.js +38 -28
- package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
- package/dist/transforms/runtime/shuffleOpcodes.js +1 -1
- package/dist/transforms/runtime/specializedOpcodes.js +21 -16
- package/dist/utils/op-utils.js +29 -0
- package/dist/utils/random-utils.js +27 -0
- package/index.ts +10 -8
- package/jest.config.js +10 -0
- package/package.json +1 -1
- package/src/build-runtime.ts +7 -1
- package/src/compiler.ts +2395 -2069
- package/src/options.ts +2 -0
- package/src/runtime.ts +838 -771
- package/src/transforms/bytecode/aliasedOpcodes.ts +158 -0
- package/src/transforms/bytecode/concealConstants.ts +52 -0
- package/src/transforms/bytecode/macroOpcodes.ts +32 -15
- package/src/transforms/bytecode/resolveContants.ts +87 -16
- package/src/transforms/bytecode/selfModifying.ts +3 -3
- package/src/transforms/bytecode/specializedOpcodes.ts +58 -29
- package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
- package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
- package/src/transforms/runtime/specializedOpcodes.ts +39 -24
- package/src/{transforms/utils → utils}/op-utils.ts +7 -0
- /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
package/dist/compiler.js
CHANGED
|
@@ -12,158 +12,152 @@ import { selfModifying } from "./transforms/bytecode/selfModifying.js";
|
|
|
12
12
|
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
|
|
13
13
|
import * as b from "./types.js";
|
|
14
14
|
import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
|
|
16
|
+
import { getRandomInt } from "./utils/random-utils.js";
|
|
17
|
+
import { U16_MAX } from "./utils/op-utils.js";
|
|
18
|
+
import { concealConstants } from "./transforms/bytecode/concealConstants.js";
|
|
17
19
|
const traverse = traverseImport.default || traverseImport;
|
|
18
|
-
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\r\nconst BYTECODE = [];\r\nconst MAIN_START_PC = 0;\r\nconst CONSTANTS = [];\r\nconst ENCODE_BYTECODE = false;\r\nconst TIMING_CHECKS = false;\r\n// The text above is not included in the compiled output - for type intellisense only\r\n// @START\r\n\r\nfunction decodeBytecode(s) {\r\n if (!ENCODE_BYTECODE) return s;\r\n\r\n var b =\r\n typeof Buffer !== \"undefined\"\r\n ? Buffer.from(s, \"base64\")\r\n : Uint8Array.from(atob(s), function (c) {\r\n return c.charCodeAt(0);\r\n });\r\n // Each slot is a u16 stored as 2 little-endian bytes.\r\n var r = new Uint16Array(b.length / 2);\r\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\r\n return r;\r\n}\r\n\r\n// Closure symbol\r\n// Used to tag shell functions so the VM can fast-path back to the\r\n// inner Closure instead of going through a sub-VM on internal calls.\r\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\r\n\r\n// Upvalue\r\n// While the outer frame is alive: reads/writes go to frame.locals[slot].\r\n// After the outer frame returns (closed): reads/writes hit this.value.\r\nfunction Upvalue(frame, slot) {\r\n this._frame = frame;\r\n this._slot = slot;\r\n this._closed = false;\r\n this._value = undefined;\r\n}\r\nUpvalue.prototype._read = function () {\r\n return this._closed ? this._value : this._frame.locals[this._slot];\r\n};\r\nUpvalue.prototype._write = function (v) {\r\n if (this._closed) this._value = v;\r\n else this._frame.locals[this._slot] = v;\r\n};\r\nUpvalue.prototype._close = function () {\r\n this._value = this._frame.locals[this._slot];\r\n this._closed = true;\r\n};\r\n\r\n// Closure & Frame\r\nfunction Closure(fn) {\r\n this.fn = fn;\r\n this.upvalues = [];\r\n this.prototype = {}; // <- default prototype object for \\`new\\`\r\n}\r\n\r\nfunction Frame(closure, returnPc, parent, thisVal ) {\r\n this.closure = closure;\r\n this.locals = new Array(closure.fn.localCount).fill(undefined);\r\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\r\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\r\n this._parent = parent;\r\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\r\n this._newObj = null; // <- set by NEW so RETURN can see it\r\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\r\n}\r\n\r\n// VM\r\nfunction VM(bytecode, mainStartPc, constants, globals) {\r\n this.bytecode = bytecode;\r\n this.constants = constants;\r\n this.globals = globals;\r\n this._stack = [];\r\n this._frameStack = [];\r\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\r\n\r\n var mainFn = {\r\n paramCount: 0,\r\n localCount: 0,\r\n startPc: mainStartPc, // <- where main begins\r\n };\r\n this._currentFrame = new Frame(new Closure(mainFn), null, null);\r\n}\r\n\r\nVM.prototype._push = function (v) {\r\n this._stack.push(v);\r\n};\r\nVM.prototype._pop = function () {\r\n return this._stack.pop();\r\n};\r\nVM.prototype.peek = function () {\r\n return this._stack[this._stack.length - 1];\r\n};\r\n\r\n// Consume the next slot from the flat bytecode stream and advance the PC.\r\n// Called by opcode handlers to read each of their operands in order.\r\nVM.prototype._operand = function () {\r\n return this.bytecode[this._currentFrame._pc++];\r\n};\r\n\r\nVM.prototype.captureUpvalue = function (frame, slot) {\r\n // Reuse existing open upvalue for this frame+slot if one exists.\r\n // This is what makes two closures share the same mutable cell.\r\n for (var i = 0; i < this._openUpvalues.length; i++) {\r\n var uv = this._openUpvalues[i];\r\n if (uv._frame === frame && uv._slot === slot) return uv;\r\n }\r\n var uv = new Upvalue(frame, slot);\r\n this._openUpvalues.push(uv);\r\n return uv;\r\n};\r\n\r\nVM.prototype._closeUpvaluesFor = function (frame) {\r\n // Called on RETURN - close every upvalue that was pointing into this frame.\r\n // After this, closures that captured from the frame read from upvalue.value.\r\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\r\n if (uv._frame === frame) {\r\n uv._close();\r\n return false;\r\n }\r\n return true;\r\n });\r\n};\r\n\r\nVM.prototype.run = function () {\r\n var now = () => {\r\n return performance.now();\r\n };\r\n\r\n var lastTime = now();\r\n\r\n while (true) {\r\n var frame = this._currentFrame;\r\n var bc = this.bytecode;\r\n if (frame._pc >= bc.length) break;\r\n\r\n var op = this.bytecode[frame._pc++];\r\n\r\n // console.log(frame._pc - 1, op);\r\n\r\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\r\n if (TIMING_CHECKS) {\r\n var currentTime = now();\r\n var isTamper = currentTime - lastTime > 1000;\r\n lastTime = currentTime;\r\n if (isTamper) {\r\n // Poison the bytecode\r\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\r\n // Break the current state\r\n op = OP.POP;\r\n this._stack = [];\r\n }\r\n }\r\n\r\n try {\r\n /* @SWITCH */\r\n switch (op) {\r\n case OP.LOAD_CONST:\r\n this._push(this.constants[this._operand()]);\r\n break;\r\n\r\n case OP.LOAD_INT:\r\n this._push(this._operand());\r\n break;\r\n\r\n case OP.LOAD_LOCAL:\r\n this._push(frame.locals[this._operand()]);\r\n break;\r\n\r\n case OP.STORE_LOCAL:\r\n frame.locals[this._operand()] = this._pop();\r\n break;\r\n\r\n case OP.LOAD_GLOBAL:\r\n this._push(this.globals[this.constants[this._operand()]]);\r\n break;\r\n\r\n case OP.STORE_GLOBAL:\r\n this.globals[this.constants[this._operand()]] = this._pop();\r\n break;\r\n\r\n case OP.GET_PROP: {\r\n // Stack: [..., obj, key] -> [..., obj, obj[key]]\r\n // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver\r\n var key = this._pop();\r\n var obj = this.peek();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n\r\n case OP.ADD: {\r\n var b = this._pop();\r\n this._push(this._pop() + b);\r\n break;\r\n }\r\n case OP.SUB: {\r\n var b = this._pop();\r\n this._push(this._pop() - b);\r\n break;\r\n }\r\n case OP.MUL: {\r\n var b = this._pop();\r\n this._push(this._pop() * b);\r\n break;\r\n }\r\n case OP.DIV: {\r\n var b = this._pop();\r\n this._push(this._pop() / b);\r\n break;\r\n }\r\n case OP.MOD: {\r\n var b = this._pop();\r\n this._push(this._pop() % b);\r\n break;\r\n }\r\n case OP.BAND: {\r\n var b = this._pop();\r\n this._push(this._pop() & b);\r\n break;\r\n }\r\n case OP.BOR: {\r\n var b = this._pop();\r\n this._push(this._pop() | b);\r\n break;\r\n }\r\n case OP.BXOR: {\r\n var b = this._pop();\r\n this._push(this._pop() ^ b);\r\n break;\r\n }\r\n case OP.SHL: {\r\n var b = this._pop();\r\n this._push(this._pop() << b);\r\n break;\r\n }\r\n case OP.SHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >> b);\r\n break;\r\n }\r\n case OP.USHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >>> b);\r\n break;\r\n }\r\n\r\n case OP.LT: {\r\n var b = this._pop();\r\n this._push(this._pop() < b);\r\n break;\r\n }\r\n case OP.GT: {\r\n var b = this._pop();\r\n this._push(this._pop() > b);\r\n break;\r\n }\r\n case OP.EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() === b);\r\n break;\r\n }\r\n\r\n case OP.LTE: {\r\n var b = this._pop();\r\n this._push(this._pop() <= b);\r\n break;\r\n }\r\n case OP.GTE: {\r\n var b = this._pop();\r\n this._push(this._pop() >= b);\r\n break;\r\n }\r\n case OP.NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() !== b);\r\n break;\r\n }\r\n case OP.LOOSE_EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() == b);\r\n break;\r\n }\r\n case OP.LOOSE_NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() != b);\r\n break;\r\n }\r\n\r\n case OP.IN: {\r\n var b = this._pop();\r\n this._push(this._pop() in b);\r\n break;\r\n }\r\n\r\n case OP.INSTANCEOF: {\r\n var ctor = this._pop();\r\n var obj = this._pop();\r\n if (typeof ctor === \"function\") {\r\n // Native constructor (e.g. Array, Date) - native instanceof is fine\r\n this._push(obj instanceof ctor);\r\n } else {\r\n // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.\r\n // Walk obj's prototype chain looking for identity with ctor.prototype.\r\n var proto = ctor.prototype; // the .prototype property on the Closure\r\n var target = Object.getPrototypeOf(obj);\r\n var result = false;\r\n while (target !== null) {\r\n if (target === proto) {\r\n result = true;\r\n break;\r\n }\r\n target = Object.getPrototypeOf(target);\r\n }\r\n this._push(result);\r\n }\r\n break;\r\n }\r\n\r\n case OP.UNARY_NEG:\r\n this._push(-this._pop());\r\n break;\r\n case OP.UNARY_POS:\r\n this._push(this._pop());\r\n break;\r\n case OP.UNARY_NOT:\r\n this._push(!this._pop());\r\n break;\r\n case OP.UNARY_BITNOT:\r\n this._push(~this._pop());\r\n break;\r\n case OP.TYPEOF:\r\n this._push(typeof this._pop());\r\n break;\r\n case OP.VOID:\r\n this._pop();\r\n this._push(undefined);\r\n break;\r\n\r\n case OP.TYPEOF_SAFE: {\r\n // operand is a const index holding the variable name string.\r\n // Mimics JS semantics: typeof undeclaredVar === \"undefined\" (no throw).\r\n var name = this._pop(); // LOAD_CONST pushed the name - consume it\r\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\r\n ? this.globals[name]\r\n : undefined;\r\n this._push(typeof val);\r\n break;\r\n }\r\n\r\n case OP.JUMP:\r\n frame._pc = this._operand();\r\n break;\r\n\r\n case OP.JUMP_IF_FALSE: {\r\n var target = this._operand();\r\n if (!this._pop()) frame._pc = target;\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_TRUE_OR_POP: {\r\n // || semantics: if truthy, we're done - leave value, jump over RHS.\r\n // If falsy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_FALSE_OR_POP: {\r\n // && semantics: if falsy, we're done - leave value, jump over RHS.\r\n // If truthy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (!this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.MAKE_CLOSURE: {\r\n // Inline operands: startPc, paramCount, localCount, uvCount,\r\n // [isLocal_0, idx_0, isLocal_1, idx_1, ...]\r\n var startPc = this._operand();\r\n var paramCount = this._operand();\r\n var localCount = this._operand();\r\n var uvCount = this._operand();\r\n\r\n var uvDescs = new Array(uvCount);\r\n for (var i = 0; i < uvCount; i++) {\r\n var isLocalRaw = this._operand();\r\n var uvIndex = this._operand();\r\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\r\n }\r\n\r\n var fn = {\r\n paramCount: paramCount,\r\n localCount: localCount,\r\n startPc: startPc,\r\n upvalueDescriptors: uvDescs,\r\n };\r\n\r\n var closure = new Closure(fn);\r\n for (var i = 0; i < uvDescs.length; i++) {\r\n var uvd = uvDescs[i];\r\n if (uvd.isLocal) {\r\n // Capture directly from current frame's local slot\r\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\r\n } else {\r\n // Relay - take upvalue from the enclosing closure's list\r\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\r\n }\r\n }\r\n // Wrap in a native callable shell so host code (array methods,\r\n // test assertions, setTimeout, etc.) can invoke VM closures.\r\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\r\n var self = this;\r\n var shell = (function (c) {\r\n return function () {\r\n var args = Array.prototype.slice.call(arguments);\r\n var sub = new VM(self.bytecode, 0, self.constants, self.globals);\r\n // Sloppy-mode: null/undefined thisArg \u2192 global object\r\n var f = new Frame(\r\n c,\r\n null,\r\n null,\r\n this == null ? self.globals : this,\r\n );\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n sub._currentFrame = f;\r\n return sub.run();\r\n };\r\n })(closure);\r\n shell[CLOSURE_SYM] = closure;\r\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\r\n this._push(shell);\r\n break;\r\n }\r\n\r\n case OP.LOAD_UPVALUE:\r\n this._push(frame.closure.upvalues[this._operand()]._read());\r\n break;\r\n\r\n case OP.STORE_UPVALUE:\r\n frame.closure.upvalues[this._operand()]._write(this._pop());\r\n break;\r\n\r\n case OP.BUILD_ARRAY: {\r\n var elems = this._stack.splice(this._stack.length - this._operand());\r\n this._push(elems);\r\n break;\r\n }\r\n\r\n case OP.BUILD_OBJECT: {\r\n // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)\r\n // Pop all pairs and build the object.\r\n var pairs = this._stack.splice(\r\n this._stack.length - this._operand() * 2,\r\n );\r\n var o = {};\r\n for (var i = 0; i < pairs.length; i += 2) {\r\n o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd\r\n }\r\n this._push(o);\r\n break;\r\n }\r\n case OP.SET_PROP: {\r\n // Stack: [..., obj, key, val]\r\n // Leaves val on stack - assignment is an expression in JS.\r\n var val = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n // Reflect.set performs [[Set]] without throwing on failure,\r\n // correctly simulating sloppy-mode assignment from a strict-mode host\r\n // (output.js is an ES module). This also properly invokes inherited\r\n // or prototype-chain setter functions.\r\n Reflect.set(obj, key, val);\r\n this._push(val); // assignment expression evaluates to the assigned value\r\n break;\r\n }\r\n case OP.GET_PROP_COMPUTED: {\r\n // Stack: [..., obj, key] - key is a runtime value (nums[i])\r\n // Mirrors GET_PROP but pops the key that was pushed dynamically.\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n case OP.DELETE_PROP: {\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(delete obj[key]);\r\n break;\r\n }\r\n\r\n case OP.CALL: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM, no sub-VM overhead\r\n var c = callee[CLOSURE_SYM];\r\n // Sloppy-mode: plain function call \u2192 global object as this\r\n var f = new Frame(c, frame._pc, frame, this.globals);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native function\r\n this._push(callee.apply(null, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.CALL_METHOD: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n var receiver = this._pop(); // left on stack by GET_PROP\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM with receiver as this\r\n var c = callee[CLOSURE_SYM];\r\n var f = new Frame(c, frame._pc, frame, receiver);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native method\r\n this._push(callee.apply(receiver, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.LOAD_THIS:\r\n this._push(frame.thisVal);\r\n break;\r\n\r\n case OP.NEW: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure constructor - prototype is unified via shell.prototype = closure.prototype\r\n var c = callee[CLOSURE_SYM];\r\n var newObj = Object.create(c.prototype || null);\r\n var f = new Frame(c, frame._pc, frame, newObj);\r\n f._newObj = newObj;\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native constructor (e.g. new Error(), new Date()).\r\n // Reflect.construct is required - Object.create+apply does NOT set\r\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\r\n this._push(Reflect.construct(callee, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.RETURN: {\r\n var retVal = this._pop();\r\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\r\n if (this._frameStack.length === 0) return retVal;\r\n\r\n // new-call rule: primitive return -> discard, use the constructed object instead\r\n if (frame._newObj !== null) {\r\n if (typeof retVal !== \"object\" || retVal === null)\r\n retVal = frame._newObj;\r\n }\r\n\r\n this._currentFrame = this._frameStack.pop();\r\n this._push(retVal);\r\n break;\r\n }\r\n\r\n case OP.POP:\r\n this._pop();\r\n break;\r\n\r\n case OP.DUP:\r\n this._push(this.peek());\r\n break;\r\n\r\n case OP.THROW:\r\n throw this._pop();\r\n\r\n case OP.FOR_IN_SETUP: {\r\n // Pop the object; build an ordered list of all enumerable own+inherited\r\n // string keys by walking the prototype chain manually.\r\n // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,\r\n // so we never rely on Object.keys() and we handle inheritance correctly.\r\n var obj = this._pop();\r\n var keys = [];\r\n if (obj !== null && obj !== undefined) {\r\n var seen = Object.create(null);\r\n var cur = Object(obj); // box primitives\r\n while (cur !== null) {\r\n var ownNames = Object.getOwnPropertyNames(cur);\r\n for (var i = 0; i < ownNames.length; i++) {\r\n var k = ownNames[i];\r\n if (!(k in seen)) {\r\n seen[k] = true;\r\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\r\n if (propDesc && propDesc.enumerable) {\r\n keys.push(k);\r\n }\r\n }\r\n }\r\n cur = Object.getPrototypeOf(cur);\r\n }\r\n }\r\n this._push({ _keys: keys, i: 0 });\r\n break;\r\n }\r\n\r\n case OP.FOR_IN_NEXT: {\r\n // Operand = jump target for the done case. Must be read before the\r\n // conditional so the PC stays correctly aligned either way.\r\n var target = this._operand();\r\n var iter = this._pop();\r\n if (iter.i >= iter._keys.length) {\r\n frame._pc = target;\r\n } else {\r\n this._push(iter._keys[iter.i++]);\r\n }\r\n break;\r\n }\r\n\r\n case OP.PATCH: {\r\n // Inline operands: destPc, sliceStart, sliceEnd\r\n // Copies bytecode[sliceStart..sliceEnd) flat u16 slots to destPc.\r\n var destPc = this._operand();\r\n var sliceStart = this._operand();\r\n var sliceEnd = this._operand();\r\n\r\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\r\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\r\n }\r\n break;\r\n }\r\n\r\n case OP.TRY_SETUP: {\r\n // Push an exception handler record onto the current frame.\r\n // Saves: catch PC (operand), current stack depth, current frame-stack depth.\r\n // If an exception is thrown before TRY_END fires, the VM jumps here.\r\n frame._handlerStack.push({\r\n handlerPc: this._operand(),\r\n stackDepth: this._stack.length,\r\n frameStackDepth: this._frameStack.length,\r\n });\r\n break;\r\n }\r\n\r\n case OP.TRY_END: {\r\n // Normal exit from a try block \u2014 disarm the exception handler.\r\n frame._handlerStack.pop();\r\n break;\r\n }\r\n\r\n case OP.DEFINE_GETTER: {\r\n // Stack: [..., obj, key, getterFn]\r\n // Pops all three; defines an enumerable, configurable getter on obj.\r\n // If a setter was already defined for this key, it is preserved.\r\n var getterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var getDesc = {\r\n get: getterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.set === \"function\") {\r\n getDesc.set = existingDesc.set;\r\n }\r\n Object.defineProperty(obj, key, getDesc);\r\n break;\r\n }\r\n\r\n case OP.DEFINE_SETTER: {\r\n // Stack: [..., obj, key, setterFn]\r\n // Pops all three; defines an enumerable, configurable setter on obj.\r\n // If a getter was already defined for this key, it is preserved.\r\n var setterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var setDesc = {\r\n set: setterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.get === \"function\") {\r\n setDesc.get = existingDesc.get;\r\n }\r\n Object.defineProperty(obj, key, setDesc);\r\n break;\r\n }\r\n\r\n case OP.DEBUGGER: {\r\n debugger;\r\n break;\r\n }\r\n\r\n default:\r\n throw new Error(\r\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\r\n );\r\n }\r\n } catch (err) {\r\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\r\n // Walk from the current frame upward until we find a frame that has an open\r\n // exception handler (TRY_SETUP without a matching TRY_END).\r\n // For every frame we abandon along the way, close its captured upvalues.\r\n var handledFrame = null;\r\n var searchFrame = this._currentFrame;\r\n while (true) {\r\n if (searchFrame._handlerStack.length > 0) {\r\n handledFrame = searchFrame;\r\n break;\r\n }\r\n // No handler in this frame \u2014 abandon it and walk up.\r\n this._closeUpvaluesFor(searchFrame);\r\n if (this._frameStack.length === 0) break;\r\n searchFrame = this._frameStack.pop();\r\n this._currentFrame = searchFrame;\r\n }\r\n\r\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\r\n\r\n var h = handledFrame._handlerStack.pop();\r\n // Restore the VM value stack to the depth recorded at TRY_SETUP time,\r\n // then push the caught exception so the catch binding can store it.\r\n this._stack.length = h.stackDepth;\r\n this._push(err);\r\n // Discard any call-frames that were pushed inside the try body\r\n // (functions called from within the try block that are still live).\r\n this._frameStack.length = h.frameStackDepth;\r\n // Jump to the catch block.\r\n handledFrame._pc = h.handlerPc;\r\n this._currentFrame = handledFrame;\r\n }\r\n }\r\n};\r\n\r\n// Boot\r\nvar globals = {}; // global object for globals\r\n\r\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\r\n// with a local `window` variable (e.g. the test harness fake window).\r\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\r\n globals[k] = globalThis[k];\r\n}\r\n// If a window object is in scope (browser or test harness), capture it\r\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\r\nif (typeof window !== \"undefined\") {\r\n globals[\"window\"] = window;\r\n}\r\n\r\n// Transfer common primitives\r\nglobals.undefined = undefined;\r\nglobals.Infinity = Infinity;\r\nglobals.NaN = NaN;\r\n\r\nvar vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, CONSTANTS, globals);\r\nvm.run();\r\n";
|
|
20
|
+
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue\n// While the outer frame is alive: reads/writes go to frame.regs[slot].\n// After the outer frame returns (closed): reads/writes hit this._value.\nfunction Upvalue(frame, slot) {\n this._frame = frame;\n this._slot = slot;\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._frame.regs[this._slot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._frame.regs[this._slot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._frame.regs[this._slot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for `new`\n}\n\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg) {\n this.closure = closure;\n this.regs = new Array(closure.fn.regCount).fill(undefined);\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0; // register in parent to write return value\n this._newObj = null; // <- set by NEW so RETURN can see it\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc, // <- where main begins\n };\n this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0);\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Reuse existing open upvalue for this frame+slot if one exists.\n // This is what makes two closures share the same mutable cell.\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (uv._frame === frame && uv._slot === slot) return uv;\n }\n var uv = new Upvalue(frame, slot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN - close every upvalue that was pointing into this frame.\n // After this, closures that captured from the frame read from upvalue.value.\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (uv._frame === frame) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n frame.regs.fill(undefined);\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n frame.regs[dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n frame.regs[dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n frame.regs[dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n frame.regs[dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n frame.regs[dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n frame.regs[dst] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(frame.regs[this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a + frame.regs[this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a - frame.regs[this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a * frame.regs[this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a / frame.regs[this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a % frame.regs[this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a & frame.regs[this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a | frame.regs[this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a ^ frame.regs[this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a << frame.regs[this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >> frame.regs[this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >>> frame.regs[this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a < frame.regs[this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a > frame.regs[this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a <= frame.regs[this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >= frame.regs[this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a === frame.regs[this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a !== frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a == frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a != frame.regs[this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a in frame.regs[this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var ctor = frame.regs[this._operand()];\n if (typeof ctor === \"function\") {\n frame.regs[dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n frame.regs[dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n frame.regs[dst] = -frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n frame.regs[dst] = +frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n frame.regs[dst] = !frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n frame.regs[dst] = ~frame.regs[this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n frame.regs[dst] = typeof frame.regs[this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n frame.regs[dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n frame.regs[dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!frame.regs[src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (frame.regs[src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, this.globals, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = frame.regs[this._operand()];\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, receiver, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var newObj = Object.create(c.prototype || null);\n var f = new Frame(c, frame._pc, frame, newObj, dst);\n f._newObj = newObj;\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n frame.regs[dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = frame.regs[this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n parentFrame.regs[frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw frame.regs[this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n );\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n sub._currentFrame = f;\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n frame.regs[dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = frame.regs[this._operand()];\n frame.regs[dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n o[key] = val;\n }\n frame.regs[dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var getterFn = frame.regs[this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var setterFn = frame.regs[this._operand()];\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n frame.regs[dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = frame.regs[this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n frame.regs[dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n handledFrame.regs[h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals[\"window\"] = window;\n}\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";
|
|
19
21
|
export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
20
|
-
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
|
|
21
|
-
|
|
22
|
-
// Opcodes
|
|
22
|
+
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
|
|
23
|
+
|
|
24
|
+
// ── Opcodes ──────────────────────────────────────────────────────────────────
|
|
25
|
+
// Register-based encoding. Operand convention (x86 / CPython style):
|
|
26
|
+
// destination register first, then source registers, then immediates.
|
|
27
|
+
//
|
|
28
|
+
// dst – register index that receives the result
|
|
29
|
+
// src – register index holding an input value
|
|
30
|
+
// imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
|
|
31
|
+
//
|
|
32
|
+
// Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
|
|
33
|
+
// Every load: [op, dst, ...]
|
|
34
|
+
// Every store: [op, target, src]
|
|
35
|
+
// Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
|
|
36
|
+
// CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
|
|
23
37
|
export const OP_ORIGINAL = {
|
|
38
|
+
// ── Loads ─────────────────────────────────────────────────────────────────
|
|
24
39
|
LOAD_CONST: 0,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
FOR_IN_NEXT: 55,
|
|
133
|
-
// operand=exit_pc; pop iter; if done->jump; else push next key
|
|
134
|
-
|
|
135
|
-
// Self-modifying bytecode
|
|
40
|
+
// dst, constIdx regs[dst] = constants[constIdx]
|
|
41
|
+
LOAD_INT: 1,
|
|
42
|
+
// dst, imm regs[dst] = imm (raw u16 literal)
|
|
43
|
+
LOAD_GLOBAL: 2,
|
|
44
|
+
// dst, nameIdx regs[dst] = globals[constants[nameIdx]]
|
|
45
|
+
LOAD_UPVALUE: 3,
|
|
46
|
+
// dst, uvIdx regs[dst] = upvalues[uvIdx].read()
|
|
47
|
+
LOAD_THIS: 4,
|
|
48
|
+
// dst regs[dst] = frame.thisVal
|
|
49
|
+
MOVE: 5,
|
|
50
|
+
// dst, src regs[dst] = regs[src]
|
|
51
|
+
|
|
52
|
+
// ── Stores ────────────────────────────────────────────────────────────────
|
|
53
|
+
STORE_GLOBAL: 6,
|
|
54
|
+
// nameIdx, src globals[constants[nameIdx]] = regs[src]
|
|
55
|
+
STORE_UPVALUE: 7,
|
|
56
|
+
// uvIdx, src upvalues[uvIdx].write(regs[src])
|
|
57
|
+
|
|
58
|
+
// ── Property access ───────────────────────────────────────────────────────
|
|
59
|
+
GET_PROP: 8,
|
|
60
|
+
// dst, obj, key regs[dst] = regs[obj][regs[key]]
|
|
61
|
+
SET_PROP: 9,
|
|
62
|
+
// obj, key, val regs[obj][regs[key]] = regs[val] (result stays in val reg)
|
|
63
|
+
DELETE_PROP: 10,
|
|
64
|
+
// dst, obj, key regs[dst] = delete regs[obj][regs[key]]
|
|
65
|
+
|
|
66
|
+
// ── Arithmetic / bitwise (dst, src1, src2) ───────────────────────────────
|
|
67
|
+
ADD: 11,
|
|
68
|
+
SUB: 12,
|
|
69
|
+
MUL: 13,
|
|
70
|
+
DIV: 14,
|
|
71
|
+
MOD: 15,
|
|
72
|
+
BAND: 16,
|
|
73
|
+
BOR: 17,
|
|
74
|
+
BXOR: 18,
|
|
75
|
+
SHL: 19,
|
|
76
|
+
SHR: 20,
|
|
77
|
+
USHR: 21,
|
|
78
|
+
// ── Comparison (dst, src1, src2) ─────────────────────────────────────────
|
|
79
|
+
LT: 22,
|
|
80
|
+
GT: 23,
|
|
81
|
+
LTE: 24,
|
|
82
|
+
GTE: 25,
|
|
83
|
+
EQ: 26,
|
|
84
|
+
NEQ: 27,
|
|
85
|
+
LOOSE_EQ: 28,
|
|
86
|
+
LOOSE_NEQ: 29,
|
|
87
|
+
IN: 30,
|
|
88
|
+
INSTANCEOF: 31,
|
|
89
|
+
// ── Unary (dst, src) ─────────────────────────────────────────────────────
|
|
90
|
+
UNARY_NEG: 32,
|
|
91
|
+
UNARY_POS: 33,
|
|
92
|
+
UNARY_NOT: 34,
|
|
93
|
+
UNARY_BITNOT: 35,
|
|
94
|
+
TYPEOF: 36,
|
|
95
|
+
// dst, src
|
|
96
|
+
VOID: 37,
|
|
97
|
+
// dst, src – regs[dst] = undefined (src evaluated for side-effects)
|
|
98
|
+
TYPEOF_SAFE: 38,
|
|
99
|
+
// dst, nameConstIdx – safe typeof for potentially-undeclared globals
|
|
100
|
+
|
|
101
|
+
// ── Control flow ──────────────────────────────────────────────────────────
|
|
102
|
+
JUMP: 39,
|
|
103
|
+
// target
|
|
104
|
+
JUMP_IF_FALSE: 40,
|
|
105
|
+
// src, target if !regs[src] then pc = target
|
|
106
|
+
JUMP_IF_TRUE: 41,
|
|
107
|
+
// src, target if regs[src] then pc = target (|| short-circuit)
|
|
108
|
+
|
|
109
|
+
// ── Calls & constructors ──────────────────────────────────────────────────
|
|
110
|
+
CALL: 42,
|
|
111
|
+
// dst, callee, argc, [argRegs…]
|
|
112
|
+
CALL_METHOD: 43,
|
|
113
|
+
// dst, receiver, callee, argc, [argRegs…]
|
|
114
|
+
NEW: 44,
|
|
115
|
+
// dst, callee, argc, [argRegs…]
|
|
116
|
+
RETURN: 45,
|
|
117
|
+
// src
|
|
118
|
+
THROW: 46,
|
|
119
|
+
// src
|
|
120
|
+
|
|
121
|
+
// ── Closures ──────────────────────────────────────────────────────────────
|
|
122
|
+
// dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
123
|
+
MAKE_CLOSURE: 47,
|
|
124
|
+
// ── Collections ───────────────────────────────────────────────────────────
|
|
125
|
+
BUILD_ARRAY: 48,
|
|
126
|
+
// dst, count, [elemRegs…]
|
|
127
|
+
BUILD_OBJECT: 49,
|
|
128
|
+
// dst, pairCount, [keyReg, valReg, …]
|
|
129
|
+
|
|
130
|
+
// ── Property definitions (getters / setters) ──────────────────────────────
|
|
131
|
+
DEFINE_GETTER: 50,
|
|
132
|
+
// obj, key, fn
|
|
133
|
+
DEFINE_SETTER: 51,
|
|
134
|
+
// obj, key, fn
|
|
135
|
+
|
|
136
|
+
// ── For-in iteration ──────────────────────────────────────────────────────
|
|
137
|
+
FOR_IN_SETUP: 52,
|
|
138
|
+
// dst, src dst = { _keys: enumKeys(src), i: 0 }
|
|
139
|
+
FOR_IN_NEXT: 53,
|
|
140
|
+
// dst, iter, exitTarget
|
|
141
|
+
|
|
142
|
+
// ── Exception handling ────────────────────────────────────────────────────
|
|
143
|
+
TRY_SETUP: 54,
|
|
144
|
+
// handlerPc, exceptionReg
|
|
145
|
+
TRY_END: 55,
|
|
146
|
+
// ── Self-modifying bytecode ───────────────────────────────────────────────
|
|
136
147
|
PATCH: 56,
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
// operand = catch_pc; push exception handler onto frame._handlerStack
|
|
142
|
-
TRY_END: 58,
|
|
143
|
-
// pop exception handler (normal exit from try body)
|
|
144
|
-
|
|
145
|
-
// Getter / Setter (ES5 object literal accessor syntax)
|
|
146
|
-
DEFINE_GETTER: 59,
|
|
147
|
-
// pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {get: fn})
|
|
148
|
-
DEFINE_SETTER: 60,
|
|
149
|
-
// pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
|
|
150
|
-
|
|
151
|
-
DEBUGGER: 61,
|
|
152
|
-
// emits a "debugger" statement
|
|
153
|
-
|
|
154
|
-
// Push the raw integer operand directly onto the stack (no constant pool lookup).
|
|
155
|
-
// Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
|
|
156
|
-
// raw PC number that becomes the operand, which is pushed as-is at runtime.
|
|
157
|
-
LOAD_INT: 62
|
|
148
|
+
// destPc, sliceStart, sliceEnd
|
|
149
|
+
|
|
150
|
+
// ── Debug ─────────────────────────────────────────────────────────────────
|
|
151
|
+
DEBUGGER: 57
|
|
158
152
|
};
|
|
159
153
|
|
|
160
|
-
// Scope
|
|
161
|
-
//
|
|
162
|
-
//
|
|
154
|
+
// ── Scope ─────────────────────────────────────────────────────────────────────
|
|
155
|
+
// Maps variable names to register indices (slot numbers).
|
|
156
|
+
// Locals are allocated at compile time; zero name lookups at runtime.
|
|
163
157
|
class Scope {
|
|
164
158
|
constructor(parent = null) {
|
|
165
159
|
this.parent = parent;
|
|
166
|
-
this._locals = new Map();
|
|
160
|
+
this._locals = new Map();
|
|
167
161
|
this._next = 0;
|
|
168
162
|
}
|
|
169
163
|
define(name) {
|
|
@@ -172,8 +166,6 @@ class Scope {
|
|
|
172
166
|
}
|
|
173
167
|
return this._locals.get(name);
|
|
174
168
|
}
|
|
175
|
-
|
|
176
|
-
// Walk up scope chain. If we fall off the top -> global.
|
|
177
169
|
resolve(name) {
|
|
178
170
|
if (this._locals.has(name)) {
|
|
179
171
|
return {
|
|
@@ -191,21 +183,33 @@ class Scope {
|
|
|
191
183
|
}
|
|
192
184
|
}
|
|
193
185
|
|
|
194
|
-
// FnContext
|
|
186
|
+
// ── FnContext ─────────────────────────────────────────────────────────────────
|
|
195
187
|
// Compiler-side state for the function currently being compiled.
|
|
196
|
-
// Distinct from runtime Frame
|
|
188
|
+
// Distinct from the runtime Frame — this is compile-time only.
|
|
197
189
|
class FnContext {
|
|
190
|
+
// Register allocator. Locals occupy regs[0..scope.localCount-1];
|
|
191
|
+
// temps are allocated above that and freed at statement boundaries.
|
|
192
|
+
regTop = 0;
|
|
193
|
+
maxRegTop = 0;
|
|
198
194
|
constructor(compiler, parentCtx = null) {
|
|
199
195
|
this.compiler = compiler;
|
|
200
196
|
this.parentCtx = parentCtx;
|
|
201
197
|
this.scope = new Scope();
|
|
202
198
|
this.bc = [];
|
|
203
|
-
this.upvalues = [];
|
|
199
|
+
this.upvalues = [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Allocate the next free temp register and return its index. */
|
|
203
|
+
allocReg() {
|
|
204
|
+
const r = this.regTop++;
|
|
205
|
+
if (this.regTop > this.maxRegTop) this.maxRegTop = this.regTop;
|
|
206
|
+
return r;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
/** Release all temps above the local region. Called at each statement boundary. */
|
|
210
|
+
resetTemps() {
|
|
211
|
+
this.regTop = this.scope.localCount;
|
|
212
|
+
}
|
|
209
213
|
addUpvalue(name, isLocal, index) {
|
|
210
214
|
const existing = this.upvalues.findIndex(u => u.name === name);
|
|
211
215
|
if (existing !== -1) return existing;
|
|
@@ -213,41 +217,35 @@ class FnContext {
|
|
|
213
217
|
this.upvalues.push({
|
|
214
218
|
name,
|
|
215
219
|
isLocal,
|
|
216
|
-
index
|
|
220
|
+
index
|
|
217
221
|
});
|
|
218
222
|
return idx;
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
|
-
|
|
222
|
-
// Compiler
|
|
225
|
+
// ── Compiler ──────────────────────────────────────────────────────────────────
|
|
223
226
|
export class Compiler {
|
|
224
227
|
emit(bc, instr, node) {
|
|
225
228
|
bc.push(instr);
|
|
226
229
|
instr[SOURCE_NODE_SYM] = node;
|
|
227
230
|
}
|
|
228
|
-
|
|
229
|
-
// DO NOT USE THIS KEY UNLESS YOU ARE "RESOLVE CONSTANTS"
|
|
230
|
-
// CONSTANTS DURING COMPILATION MUST BE USED BY REFERENCE WITH b.constantOperand("myConstantHere")
|
|
231
|
-
|
|
232
231
|
constructor(options = DEFAULT_OPTIONS) {
|
|
233
232
|
this.options = options;
|
|
234
|
-
this.fnDescriptors = [];
|
|
233
|
+
this.fnDescriptors = [];
|
|
235
234
|
this.bytecode = [];
|
|
236
235
|
this.mainStartPc = 0;
|
|
237
|
-
this.
|
|
238
|
-
this.
|
|
236
|
+
this.mainRegCount = 0;
|
|
237
|
+
this._currentCtx = null;
|
|
238
|
+
this._loopStack = [];
|
|
239
239
|
this._pendingLabel = null;
|
|
240
|
-
this._forInCount = 0;
|
|
241
|
-
this._labelCount = 0;
|
|
242
|
-
|
|
240
|
+
this._forInCount = 0;
|
|
241
|
+
this._labelCount = 0;
|
|
243
242
|
this.serializer = new Serializer(this);
|
|
244
243
|
this.MACRO_OPS = {};
|
|
245
244
|
this.SPECIALIZED_OPS = {};
|
|
245
|
+
this.ALIASED_OPS = {};
|
|
246
246
|
this.OP = {
|
|
247
247
|
...OP_ORIGINAL
|
|
248
248
|
};
|
|
249
|
-
|
|
250
|
-
// Construct randomized opcode mapping
|
|
251
249
|
if (this.options.randomizeOpcodes) {
|
|
252
250
|
let usedNumbers = new Set();
|
|
253
251
|
for (const key in this.OP) {
|
|
@@ -259,59 +257,122 @@ export class Compiler {
|
|
|
259
257
|
this.OP[key] = val;
|
|
260
258
|
}
|
|
261
259
|
}
|
|
262
|
-
|
|
263
|
-
// Reverse map for comment generation
|
|
264
260
|
this.OP_NAME = Object.fromEntries(Object.entries(this.OP).map(([k, v]) => [v, k]));
|
|
265
|
-
this.JUMP_OPS = new Set([this.OP.JUMP, this.OP.JUMP_IF_FALSE, this.OP.
|
|
266
|
-
]);
|
|
261
|
+
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]);
|
|
267
262
|
}
|
|
268
|
-
|
|
269
|
-
// Generate a globally unique label string with an optional hint for readability.
|
|
270
263
|
_makeLabel(hint = "") {
|
|
271
|
-
|
|
272
|
-
return `${hint || "L"}_${id}`;
|
|
264
|
+
return `${hint || "L"}_${this._labelCount++}`;
|
|
273
265
|
}
|
|
274
|
-
|
|
275
|
-
// Variable resolution
|
|
276
|
-
// Walks up the FnContext chain. Crossing a context boundary means
|
|
277
|
-
// we're capturing from an outer function - register an upvalue.
|
|
278
266
|
_resolve(name, ctx) {
|
|
279
267
|
if (!ctx) return {
|
|
280
268
|
kind: "global"
|
|
281
269
|
};
|
|
282
|
-
|
|
283
|
-
// 1. Own locals
|
|
284
270
|
if (ctx.scope._locals.has(name)) {
|
|
285
271
|
return {
|
|
286
272
|
kind: "local",
|
|
287
273
|
slot: ctx.scope._locals.get(name)
|
|
288
274
|
};
|
|
289
275
|
}
|
|
290
|
-
|
|
291
|
-
// 2. No parent context -> must be global
|
|
292
276
|
if (!ctx.parentCtx) return {
|
|
293
277
|
kind: "global"
|
|
294
278
|
};
|
|
295
|
-
|
|
296
|
-
// 3. Ask parent -- recurse up the chain
|
|
297
279
|
const parentResult = this._resolve(name, ctx.parentCtx);
|
|
298
280
|
if (parentResult.kind === "global") return {
|
|
299
281
|
kind: "global"
|
|
300
282
|
};
|
|
301
|
-
|
|
302
|
-
// 4. Parent has it (as local or upvalue) -- register an upvalue here.
|
|
303
|
-
// isLocal=true means "take it straight from parent's locals[index]"
|
|
304
|
-
// isLocal=false means "relay parent's upvalue[index]" (multi-level capture)
|
|
305
283
|
const isLocal = parentResult.kind === "local";
|
|
306
284
|
const index = isLocal ? parentResult.slot : parentResult.index;
|
|
307
|
-
const uvIdx = ctx.addUpvalue(name, isLocal, index);
|
|
285
|
+
const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
|
|
308
286
|
return {
|
|
309
287
|
kind: "upvalue",
|
|
310
288
|
index: uvIdx
|
|
311
289
|
};
|
|
312
290
|
}
|
|
313
291
|
|
|
314
|
-
//
|
|
292
|
+
// ── Variable hoisting ──────────────────────────────────────────────────────
|
|
293
|
+
// Pre-scan a statement list and reserve scope slots for every var declaration,
|
|
294
|
+
// function declaration, for-in iterator, and try-catch binding.
|
|
295
|
+
// Must be called before compilation begins so that ctx.regTop can be set
|
|
296
|
+
// safely above ALL locals (including those declared late in the body).
|
|
297
|
+
_hoistVars(stmts, scope) {
|
|
298
|
+
for (const stmt of stmts) {
|
|
299
|
+
switch (stmt.type) {
|
|
300
|
+
case "VariableDeclaration":
|
|
301
|
+
for (const decl of stmt.declarations) {
|
|
302
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case "FunctionDeclaration":
|
|
306
|
+
if (stmt.id) scope.define(stmt.id.name);
|
|
307
|
+
break;
|
|
308
|
+
case "BlockStatement":
|
|
309
|
+
this._hoistVars(stmt.body, scope);
|
|
310
|
+
break;
|
|
311
|
+
case "IfStatement":
|
|
312
|
+
{
|
|
313
|
+
const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
|
|
314
|
+
this._hoistVars(cons, scope);
|
|
315
|
+
if (stmt.alternate) {
|
|
316
|
+
const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
|
|
317
|
+
this._hoistVars(alt, scope);
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "WhileStatement":
|
|
322
|
+
case "DoWhileStatement":
|
|
323
|
+
{
|
|
324
|
+
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
325
|
+
this._hoistVars(body, scope);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case "ForStatement":
|
|
329
|
+
{
|
|
330
|
+
if (stmt.init?.type === "VariableDeclaration") {
|
|
331
|
+
for (const decl of stmt.init.declarations) {
|
|
332
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
336
|
+
this._hoistVars(body, scope);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case "ForInStatement":
|
|
340
|
+
{
|
|
341
|
+
// Reserve a hidden register for the iterator object.
|
|
342
|
+
stmt._iterSlot = scope._next++;
|
|
343
|
+
if (stmt.left.type === "VariableDeclaration") {
|
|
344
|
+
for (const decl of stmt.left.declarations) {
|
|
345
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
349
|
+
this._hoistVars(body, scope);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "SwitchStatement":
|
|
353
|
+
for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
|
|
354
|
+
break;
|
|
355
|
+
case "TryStatement":
|
|
356
|
+
this._hoistVars(stmt.block.body, scope);
|
|
357
|
+
if (stmt.handler) {
|
|
358
|
+
if (stmt.handler.param?.type === "Identifier") {
|
|
359
|
+
// Catch parameter IS the exception register.
|
|
360
|
+
scope.define(stmt.handler.param.name);
|
|
361
|
+
} else {
|
|
362
|
+
// No catch binding – reserve a dummy slot for the exception value.
|
|
363
|
+
stmt._exceptionSlot = scope._next++;
|
|
364
|
+
}
|
|
365
|
+
this._hoistVars(stmt.handler.body.body, scope);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case "LabeledStatement":
|
|
369
|
+
this._hoistVars([stmt.body], scope);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Entry point ───────────────────────────────────────────────────────────
|
|
315
376
|
compile(source) {
|
|
316
377
|
const ast = parse(source, {
|
|
317
378
|
sourceType: "script"
|
|
@@ -319,146 +380,127 @@ export class Compiler {
|
|
|
319
380
|
return this.compileAST(ast);
|
|
320
381
|
}
|
|
321
382
|
compileAST(ast) {
|
|
322
|
-
// Pass 1 - compile every FunctionDeclaration into a descriptor.
|
|
323
|
-
// Traverse finds them regardless of nesting depth.
|
|
324
383
|
traverse(ast, {
|
|
325
384
|
FunctionDeclaration: path => {
|
|
326
|
-
// Only handle top-level functions for this MVP.
|
|
327
|
-
// (Parent is Program node)
|
|
328
385
|
if (path.parent.type !== "Program") return;
|
|
329
386
|
this._compileFunctionDecl(path.node);
|
|
330
|
-
path.skip();
|
|
387
|
+
path.skip();
|
|
331
388
|
}
|
|
332
389
|
});
|
|
333
|
-
|
|
334
|
-
// Pass 2 -- compile top-level statements into BYTECODE.
|
|
335
390
|
this._compileMain(ast.program.body);
|
|
336
391
|
return this.bytecode;
|
|
337
392
|
}
|
|
338
393
|
|
|
339
|
-
// Function
|
|
340
|
-
|
|
394
|
+
// ── Function compilation ───────────────────────────────────────────────────
|
|
341
395
|
_compileFunctionDecl(node) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// distinct _fnIdx. The placeholder object is mutated in-place below once
|
|
345
|
-
// the body and header are ready.
|
|
396
|
+
ok(!node.generator, "Generator functions are not supported");
|
|
397
|
+
ok(!node.async, "Async functions are not supported");
|
|
346
398
|
var fnIdx = this.fnDescriptors.length;
|
|
347
399
|
const entryLabel = this._makeLabel(`fn_${fnIdx}`);
|
|
348
|
-
var desc = {};
|
|
400
|
+
var desc = {};
|
|
349
401
|
this.fnDescriptors.push(desc);
|
|
350
|
-
|
|
351
|
-
// Create a context whose parent is whatever we're currently compiling.
|
|
352
|
-
// This is what lets _resolve cross function boundaries correctly.
|
|
353
402
|
const ctx = new FnContext(this, this._currentCtx);
|
|
354
403
|
const savedCtx = this._currentCtx;
|
|
355
404
|
this._currentCtx = ctx;
|
|
356
|
-
|
|
357
|
-
// Isolate the loop stack so that try/loop entries from the outer scope
|
|
358
|
-
// don't cause spurious TRY_END / extra jumps inside this function body.
|
|
359
405
|
const savedLoopStack = this._loopStack;
|
|
360
406
|
this._loopStack = [];
|
|
361
407
|
|
|
362
|
-
//
|
|
408
|
+
// 1. Define parameters (occupy regs 0..paramCount-1).
|
|
363
409
|
for (const param of node.params) {
|
|
364
410
|
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
365
411
|
ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
|
|
366
412
|
ctx.scope.define(identifier.name);
|
|
367
413
|
}
|
|
368
414
|
|
|
369
|
-
// Reserve the
|
|
370
|
-
// Slot index will always equal paramCount (params are 0..paramCount-1).
|
|
415
|
+
// 2. Reserve the `arguments` slot (reg index = paramCount).
|
|
371
416
|
ctx.scope.define("arguments");
|
|
372
417
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
418
|
+
// 3. Hoist all var declarations so temps start above every local.
|
|
419
|
+
this._hoistVars(node.body.body, ctx.scope);
|
|
420
|
+
|
|
421
|
+
// 4. Temps now start above all locals.
|
|
422
|
+
ctx.regTop = ctx.scope.localCount;
|
|
423
|
+
ctx.maxRegTop = ctx.regTop;
|
|
424
|
+
|
|
425
|
+
// 5. Emit default-value guards.
|
|
376
426
|
for (const param of node.params) {
|
|
377
427
|
if (param.type !== "AssignmentPattern") continue;
|
|
378
428
|
const slot = ctx.scope._locals.get(param.left.name);
|
|
379
429
|
const skipLabel = this._makeLabel("param_skip");
|
|
380
430
|
|
|
381
|
-
// if (param === undefined) param = <default
|
|
382
|
-
|
|
383
|
-
this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], param);
|
|
384
|
-
|
|
385
|
-
this.emit(ctx.bc, [this.OP.
|
|
431
|
+
// if (param === undefined) param = <default>
|
|
432
|
+
const reg_undef = ctx.allocReg();
|
|
433
|
+
this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], param);
|
|
434
|
+
const reg_cmp = ctx.allocReg();
|
|
435
|
+
this.emit(ctx.bc, [this.OP.EQ, reg_cmp, slot, reg_undef], param);
|
|
436
|
+
this.emit(ctx.bc, [this.OP.JUMP_IF_FALSE, reg_cmp, {
|
|
386
437
|
type: "label",
|
|
387
438
|
label: skipLabel
|
|
388
439
|
}], param);
|
|
389
|
-
|
|
390
|
-
this.
|
|
440
|
+
ctx.resetTemps();
|
|
441
|
+
const srcReg = this._compileExpr(param.right, ctx.scope, ctx.bc);
|
|
442
|
+
if (srcReg !== slot) {
|
|
443
|
+
this.emit(ctx.bc, [this.OP.MOVE, slot, srcReg], param);
|
|
444
|
+
}
|
|
445
|
+
ctx.resetTemps();
|
|
391
446
|
this.emit(ctx.bc, [null, {
|
|
392
447
|
type: "defineLabel",
|
|
393
448
|
label: skipLabel
|
|
394
449
|
}], param);
|
|
395
450
|
}
|
|
451
|
+
|
|
452
|
+
// 6. Compile body.
|
|
396
453
|
for (const stmt of node.body.body) {
|
|
397
454
|
this._compileStatement(stmt, ctx.scope, ctx.bc);
|
|
398
455
|
}
|
|
399
456
|
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
this.emit(ctx.bc, [this.OP.
|
|
403
|
-
this.
|
|
457
|
+
// Implicit return undefined at end of function.
|
|
458
|
+
const reg_undef = ctx.allocReg();
|
|
459
|
+
this.emit(ctx.bc, [this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)], node);
|
|
460
|
+
this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
|
|
461
|
+
this._currentCtx = savedCtx;
|
|
404
462
|
this._loopStack = savedLoopStack;
|
|
405
463
|
node._fnIdx = fnIdx;
|
|
406
|
-
|
|
407
|
-
// Fill the placeholder that was reserved at the top of this function.
|
|
408
|
-
// Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
|
|
409
|
-
// as inline operands on the MAKE_CLOSURE instruction via _emitMakeClosure.
|
|
410
464
|
desc.name = node.id?.name || "<anonymous>";
|
|
411
465
|
desc.entryLabel = entryLabel;
|
|
412
466
|
desc.bytecode = ctx.bc;
|
|
413
467
|
desc._fnIdx = fnIdx;
|
|
414
468
|
desc.paramCount = node.params.length;
|
|
415
|
-
desc.
|
|
469
|
+
desc.regCount = ctx.maxRegTop; // total registers needed at runtime
|
|
416
470
|
desc.upvalues = ctx.upvalues.slice();
|
|
417
471
|
return desc;
|
|
418
472
|
}
|
|
419
473
|
|
|
420
|
-
// Emit
|
|
421
|
-
//
|
|
422
|
-
// shuffling needed.
|
|
423
|
-
//
|
|
424
|
-
// Flat operand layout: startPc, paramCount, localCount, uvCount,
|
|
425
|
-
// [isLocal_0, idx_0, isLocal_1, idx_1, ...]
|
|
474
|
+
// Emit MAKE_CLOSURE with all metadata as inline operands.
|
|
475
|
+
// Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
426
476
|
_emitMakeClosure(desc, node, bc) {
|
|
477
|
+
const ctx = this._currentCtx;
|
|
478
|
+
const dst = ctx.allocReg();
|
|
427
479
|
const uvOperands = [];
|
|
428
480
|
for (const uv of desc.upvalues) {
|
|
429
481
|
uvOperands.push(uv.isLocal ? 1 : 0);
|
|
430
482
|
uvOperands.push(uv.index);
|
|
431
483
|
}
|
|
432
|
-
this.emit(bc, [this.OP.MAKE_CLOSURE, {
|
|
484
|
+
this.emit(bc, [this.OP.MAKE_CLOSURE, dst, {
|
|
433
485
|
type: "label",
|
|
434
486
|
label: desc.entryLabel
|
|
435
|
-
}, desc.paramCount, desc.
|
|
487
|
+
}, desc.paramCount, desc.regCount, desc.upvalues.length, ...uvOperands], node);
|
|
488
|
+
return dst;
|
|
436
489
|
}
|
|
437
490
|
|
|
438
|
-
// Main (top-level)
|
|
491
|
+
// ── Main (top-level) ───────────────────────────────────────────────────────
|
|
439
492
|
_compileMain(body) {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Compile everything else in order
|
|
453
|
-
for (const node of body) {
|
|
454
|
-
if (node.type === "FunctionDeclaration") continue;
|
|
455
|
-
this._compileStatement(node, null, bc); // null scope -> global context
|
|
456
|
-
}
|
|
457
|
-
this.emit(bc, [this.OP.RETURN], null); // end program
|
|
458
|
-
|
|
459
|
-
// Append all function bodies. Each function's entryLabel (already generated
|
|
460
|
-
// in _compileFunctionDecl) points directly to the first body instruction;
|
|
461
|
-
// metadata is pushed onto the stack at each call site, not stored inline.
|
|
493
|
+
const mainCtx = new FnContext(this, null);
|
|
494
|
+
const savedCtx = this._currentCtx;
|
|
495
|
+
this._currentCtx = mainCtx;
|
|
496
|
+
var desc = this._compileFunctionDecl({
|
|
497
|
+
type: "FunctionDeclaration",
|
|
498
|
+
async: false,
|
|
499
|
+
generator: false,
|
|
500
|
+
params: [],
|
|
501
|
+
id: null,
|
|
502
|
+
body: t.blockStatement([...body])
|
|
503
|
+
});
|
|
462
504
|
for (const descriptor of this.fnDescriptors) {
|
|
463
505
|
this.bytecode.push([null, {
|
|
464
506
|
type: "defineLabel",
|
|
@@ -468,88 +510,103 @@ export class Compiler {
|
|
|
468
510
|
this.bytecode.push(instr);
|
|
469
511
|
}
|
|
470
512
|
}
|
|
513
|
+
this.mainRegCount = mainCtx.maxRegTop;
|
|
514
|
+
this.mainFn = desc;
|
|
515
|
+
this._currentCtx = savedCtx;
|
|
471
516
|
}
|
|
472
517
|
|
|
473
|
-
// Statements
|
|
518
|
+
// ── Statements ────────────────────────────────────────────────────────────
|
|
519
|
+
// Wrapper that resets temps after every statement so that short-lived
|
|
520
|
+
// expression temps don't accumulate across statements.
|
|
474
521
|
_compileStatement(node, scope, bc) {
|
|
522
|
+
this._compileStatementImpl(node, scope, bc);
|
|
523
|
+
this._currentCtx?.resetTemps();
|
|
524
|
+
}
|
|
525
|
+
_compileStatementImpl(node, scope, bc) {
|
|
526
|
+
const ctx = this._currentCtx;
|
|
475
527
|
switch (node.type) {
|
|
476
528
|
case "EmptyStatement":
|
|
477
|
-
|
|
478
|
-
// nothing to emit -- bare semicolon is a no-op
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
529
|
+
break;
|
|
481
530
|
case "DebuggerStatement":
|
|
482
531
|
this.emit(bc, [this.OP.DEBUGGER], node);
|
|
483
532
|
break;
|
|
484
533
|
case "BlockStatement":
|
|
485
|
-
{
|
|
486
|
-
|
|
487
|
-
this._compileStatement(stmt, scope, bc);
|
|
488
|
-
}
|
|
489
|
-
break;
|
|
534
|
+
for (const stmt of node.body) {
|
|
535
|
+
this._compileStatement(stmt, scope, bc);
|
|
490
536
|
}
|
|
537
|
+
break;
|
|
491
538
|
case "FunctionDeclaration":
|
|
492
539
|
{
|
|
493
|
-
// Nested function -- compile it into a descriptor, then emit
|
|
494
|
-
// MAKE_CLOSURE so it's captured as a live closure at runtime.
|
|
495
|
-
// (_compileFunctionDecl pushes/pops _currentCtx internally)
|
|
496
540
|
const desc = this._compileFunctionDecl(node);
|
|
497
|
-
this._emitMakeClosure(desc, node, bc);
|
|
541
|
+
const closureReg = this._emitMakeClosure(desc, node, bc);
|
|
498
542
|
if (scope) {
|
|
499
|
-
const slot = scope.
|
|
500
|
-
|
|
543
|
+
const slot = scope._locals.get(node.id.name);
|
|
544
|
+
if (closureReg !== slot) {
|
|
545
|
+
this.emit(bc, [this.OP.MOVE, slot, closureReg], node);
|
|
546
|
+
}
|
|
501
547
|
} else {
|
|
502
|
-
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)], node);
|
|
548
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name), closureReg], node);
|
|
503
549
|
}
|
|
504
550
|
break;
|
|
505
551
|
}
|
|
506
552
|
case "ThrowStatement":
|
|
507
553
|
{
|
|
508
|
-
this._compileExpr(node.argument, scope, bc);
|
|
509
|
-
this.emit(bc, [this.OP.THROW], node);
|
|
554
|
+
const reg = this._compileExpr(node.argument, scope, bc);
|
|
555
|
+
this.emit(bc, [this.OP.THROW, reg], node);
|
|
510
556
|
break;
|
|
511
557
|
}
|
|
512
558
|
case "ReturnStatement":
|
|
513
559
|
{
|
|
560
|
+
let reg;
|
|
514
561
|
if (node.argument) {
|
|
515
|
-
this._compileExpr(node.argument, scope, bc);
|
|
562
|
+
reg = this._compileExpr(node.argument, scope, bc);
|
|
516
563
|
} else {
|
|
517
|
-
|
|
564
|
+
reg = ctx.allocReg();
|
|
565
|
+
this.emit(bc, [this.OP.LOAD_CONST, reg, b.constantOperand(undefined)], node);
|
|
518
566
|
}
|
|
519
|
-
// Disarm any open try handlers before leaving the function.
|
|
520
|
-
// TRY_END only touches frame._handlerStack, not the value stack,
|
|
521
|
-
// so the return value sitting on top is safe.
|
|
522
567
|
for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
|
|
523
568
|
if (this._loopStack[_ri].type === "try") {
|
|
524
569
|
this.emit(bc, [this.OP.TRY_END], node);
|
|
525
570
|
}
|
|
526
571
|
}
|
|
527
|
-
this.emit(bc, [this.OP.RETURN], node);
|
|
572
|
+
this.emit(bc, [this.OP.RETURN, reg], node);
|
|
528
573
|
break;
|
|
529
574
|
}
|
|
530
575
|
case "ExpressionStatement":
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
576
|
+
this._compileExpr(node.expression, scope, bc);
|
|
577
|
+
// Result is discarded; resetTemps in the wrapper handles cleanup.
|
|
578
|
+
break;
|
|
536
579
|
case "VariableDeclaration":
|
|
537
580
|
{
|
|
538
581
|
for (const decl of node.declarations) {
|
|
539
|
-
// Push the initialiser (or undefined if absent)
|
|
540
|
-
if (decl.init) {
|
|
541
|
-
this._compileExpr(decl.init, scope, bc);
|
|
542
|
-
} else {
|
|
543
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
|
|
544
|
-
}
|
|
545
582
|
ok(decl.id.type === "Identifier", "Only simple identifiers can be declared");
|
|
546
|
-
|
|
547
|
-
// Store: local slot if inside a function, global name otherwise
|
|
583
|
+
const name = decl.id.name;
|
|
548
584
|
if (scope) {
|
|
549
|
-
const slot = scope.
|
|
550
|
-
|
|
585
|
+
const slot = scope._locals.get(name); // already defined by _hoistVars
|
|
586
|
+
if (decl.init) {
|
|
587
|
+
const srcReg = this._compileExpr(decl.init, scope, bc);
|
|
588
|
+
if (srcReg !== slot) {
|
|
589
|
+
this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
this.emit(bc, [this.OP.MOVE, slot, ctx.allocReg()], node);
|
|
593
|
+
// Load undefined into the just-allocated temp, then move.
|
|
594
|
+
// Actually: just emit LOAD_CONST directly into slot.
|
|
595
|
+
// Undo the allocReg – instead emit directly:
|
|
596
|
+
ctx.regTop--; // undo the allocReg above
|
|
597
|
+
const tmp = ctx.allocReg();
|
|
598
|
+
this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
|
|
599
|
+
if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
|
|
600
|
+
}
|
|
551
601
|
} else {
|
|
552
|
-
|
|
602
|
+
if (decl.init) {
|
|
603
|
+
const srcReg = this._compileExpr(decl.init, scope, bc);
|
|
604
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), srcReg], node);
|
|
605
|
+
} else {
|
|
606
|
+
const tmp = ctx.allocReg();
|
|
607
|
+
this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
|
|
608
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), tmp], node);
|
|
609
|
+
}
|
|
553
610
|
}
|
|
554
611
|
}
|
|
555
612
|
break;
|
|
@@ -557,42 +614,37 @@ export class Compiler {
|
|
|
557
614
|
case "IfStatement":
|
|
558
615
|
{
|
|
559
616
|
const elseOrEndLabel = this._makeLabel("if_else");
|
|
560
|
-
|
|
561
|
-
this._compileExpr(node.test, scope, bc);
|
|
562
|
-
|
|
563
|
-
this.emit(bc, [this.OP.JUMP_IF_FALSE, {
|
|
617
|
+
const savedTop = ctx.regTop;
|
|
618
|
+
const testReg = this._compileExpr(node.test, scope, bc);
|
|
619
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
564
620
|
type: "label",
|
|
565
621
|
label: elseOrEndLabel
|
|
566
622
|
}], node);
|
|
567
|
-
|
|
623
|
+
ctx.regTop = savedTop; // free test temps
|
|
624
|
+
|
|
568
625
|
const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
|
|
569
626
|
for (const stmt of consequentBody) {
|
|
570
627
|
this._compileStatement(stmt, scope, bc);
|
|
571
628
|
}
|
|
572
629
|
if (node.alternate) {
|
|
573
|
-
// 4a. Consequent needs to jump OVER the else block when done
|
|
574
630
|
const endLabel = this._makeLabel("if_end");
|
|
575
631
|
this.emit(bc, [this.OP.JUMP, {
|
|
576
632
|
type: "label",
|
|
577
633
|
label: endLabel
|
|
578
634
|
}], node);
|
|
579
|
-
// Mark start of else
|
|
580
635
|
this.emit(bc, [null, {
|
|
581
636
|
type: "defineLabel",
|
|
582
637
|
label: elseOrEndLabel
|
|
583
638
|
}], node);
|
|
584
|
-
|
|
585
|
-
const altBody = node.alternate.type === "BlockStatement" ? node.alternate.body : [node.alternate]; // handles `else if` -- it's just a nested IfStatement
|
|
639
|
+
const altBody = node.alternate.type === "BlockStatement" ? node.alternate.body : [node.alternate];
|
|
586
640
|
for (const stmt of altBody) {
|
|
587
641
|
this._compileStatement(stmt, scope, bc);
|
|
588
642
|
}
|
|
589
|
-
// Mark end (consequent's jump lands here)
|
|
590
643
|
this.emit(bc, [null, {
|
|
591
644
|
type: "defineLabel",
|
|
592
645
|
label: endLabel
|
|
593
646
|
}], node);
|
|
594
647
|
} else {
|
|
595
|
-
// 4b. No else -- label lands right after the then block
|
|
596
648
|
this.emit(bc, [null, {
|
|
597
649
|
type: "defineLabel",
|
|
598
650
|
label: elseOrEndLabel
|
|
@@ -610,17 +662,19 @@ export class Compiler {
|
|
|
610
662
|
type: "loop",
|
|
611
663
|
label: _wLabel,
|
|
612
664
|
breakLabel: exitLabel,
|
|
613
|
-
continueLabel: loopTopLabel
|
|
665
|
+
continueLabel: loopTopLabel
|
|
614
666
|
});
|
|
615
667
|
this.emit(bc, [null, {
|
|
616
668
|
type: "defineLabel",
|
|
617
669
|
label: loopTopLabel
|
|
618
670
|
}], node);
|
|
619
|
-
|
|
620
|
-
this.
|
|
671
|
+
const savedTop = ctx.regTop;
|
|
672
|
+
const testReg = this._compileExpr(node.test, scope, bc);
|
|
673
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
621
674
|
type: "label",
|
|
622
675
|
label: exitLabel
|
|
623
676
|
}], node);
|
|
677
|
+
ctx.regTop = savedTop;
|
|
624
678
|
const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
625
679
|
for (const stmt of whileBody) {
|
|
626
680
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -647,7 +701,7 @@ export class Compiler {
|
|
|
647
701
|
type: "loop",
|
|
648
702
|
label: _dwLabel,
|
|
649
703
|
breakLabel: exitLabel,
|
|
650
|
-
continueLabel: continueLabel
|
|
704
|
+
continueLabel: continueLabel
|
|
651
705
|
});
|
|
652
706
|
this.emit(bc, [null, {
|
|
653
707
|
type: "defineLabel",
|
|
@@ -657,17 +711,17 @@ export class Compiler {
|
|
|
657
711
|
for (const stmt of doWhileBody) {
|
|
658
712
|
this._compileStatement(stmt, scope, bc);
|
|
659
713
|
}
|
|
660
|
-
|
|
661
|
-
// continue -> skip rest of body, fall through to test
|
|
662
714
|
this.emit(bc, [null, {
|
|
663
715
|
type: "defineLabel",
|
|
664
716
|
label: continueLabel
|
|
665
717
|
}], node);
|
|
666
|
-
|
|
667
|
-
this.
|
|
718
|
+
const savedTop = ctx.regTop;
|
|
719
|
+
const testReg = this._compileExpr(node.test, scope, bc);
|
|
720
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
668
721
|
type: "label",
|
|
669
722
|
label: exitLabel
|
|
670
723
|
}], node);
|
|
724
|
+
ctx.regTop = savedTop;
|
|
671
725
|
this.emit(bc, [this.OP.JUMP, {
|
|
672
726
|
type: "label",
|
|
673
727
|
label: loopTopLabel
|
|
@@ -685,7 +739,6 @@ export class Compiler {
|
|
|
685
739
|
this._pendingLabel = null;
|
|
686
740
|
const loopTopLabel = this._makeLabel("for_top");
|
|
687
741
|
const exitLabel = this._makeLabel("for_exit");
|
|
688
|
-
// continue jumps to the update clause if present, else straight to test
|
|
689
742
|
const updateLabel = node.update ? this._makeLabel("for_update") : loopTopLabel;
|
|
690
743
|
this._loopStack.push({
|
|
691
744
|
type: "loop",
|
|
@@ -698,7 +751,7 @@ export class Compiler {
|
|
|
698
751
|
this._compileStatement(node.init, scope, bc);
|
|
699
752
|
} else {
|
|
700
753
|
this._compileExpr(node.init, scope, bc);
|
|
701
|
-
|
|
754
|
+
// result discarded; resetTemps in next iteration
|
|
702
755
|
}
|
|
703
756
|
}
|
|
704
757
|
this.emit(bc, [null, {
|
|
@@ -706,25 +759,25 @@ export class Compiler {
|
|
|
706
759
|
label: loopTopLabel
|
|
707
760
|
}], node);
|
|
708
761
|
if (node.test) {
|
|
709
|
-
|
|
710
|
-
this.
|
|
762
|
+
const savedTop = ctx.regTop;
|
|
763
|
+
const testReg = this._compileExpr(node.test, scope, bc);
|
|
764
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
711
765
|
type: "label",
|
|
712
766
|
label: exitLabel
|
|
713
767
|
}], node);
|
|
768
|
+
ctx.regTop = savedTop;
|
|
714
769
|
}
|
|
715
770
|
const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
716
771
|
for (const stmt of forBody) {
|
|
717
772
|
this._compileStatement(stmt, scope, bc);
|
|
718
773
|
}
|
|
719
|
-
|
|
720
|
-
// continue -> run update (if any) then back to test
|
|
721
774
|
if (node.update) {
|
|
722
775
|
this.emit(bc, [null, {
|
|
723
776
|
type: "defineLabel",
|
|
724
777
|
label: updateLabel
|
|
725
778
|
}], node);
|
|
726
779
|
this._compileExpr(node.update, scope, bc);
|
|
727
|
-
|
|
780
|
+
ctx.resetTemps(); // discard update expression result
|
|
728
781
|
}
|
|
729
782
|
this.emit(bc, [this.OP.JUMP, {
|
|
730
783
|
type: "label",
|
|
@@ -739,7 +792,6 @@ export class Compiler {
|
|
|
739
792
|
}
|
|
740
793
|
case "BreakStatement":
|
|
741
794
|
{
|
|
742
|
-
// Find the jump target in the loop stack.
|
|
743
795
|
let _bTargetIdx = -1;
|
|
744
796
|
if (node.label) {
|
|
745
797
|
const _bLabelName = node.label.name;
|
|
@@ -751,7 +803,6 @@ export class Compiler {
|
|
|
751
803
|
}
|
|
752
804
|
if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
|
|
753
805
|
} else {
|
|
754
|
-
// Find innermost loop/switch/block (skip "try" entries)
|
|
755
806
|
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
756
807
|
if (this._loopStack[_bi].type !== "try") {
|
|
757
808
|
_bTargetIdx = _bi;
|
|
@@ -760,7 +811,6 @@ export class Compiler {
|
|
|
760
811
|
}
|
|
761
812
|
if (_bTargetIdx === -1) throw new Error("break outside loop");
|
|
762
813
|
}
|
|
763
|
-
// Emit TRY_END for every open try block between here and the target.
|
|
764
814
|
for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
|
|
765
815
|
if (this._loopStack[_bi].type === "try") {
|
|
766
816
|
this.emit(bc, [this.OP.TRY_END], node);
|
|
@@ -774,7 +824,6 @@ export class Compiler {
|
|
|
774
824
|
}
|
|
775
825
|
case "ContinueStatement":
|
|
776
826
|
{
|
|
777
|
-
// Find the target loop in the loop stack.
|
|
778
827
|
let _cTargetIdx = -1;
|
|
779
828
|
if (node.label) {
|
|
780
829
|
const _cLabelName = node.label.name;
|
|
@@ -786,7 +835,6 @@ export class Compiler {
|
|
|
786
835
|
}
|
|
787
836
|
if (_cTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found for continue`);
|
|
788
837
|
} else {
|
|
789
|
-
// Find the innermost loop (skip switch, block, and try contexts)
|
|
790
838
|
for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
|
|
791
839
|
if (this._loopStack[_ci].type === "loop") {
|
|
792
840
|
_cTargetIdx = _ci;
|
|
@@ -795,7 +843,6 @@ export class Compiler {
|
|
|
795
843
|
}
|
|
796
844
|
if (_cTargetIdx === -1) throw new Error("continue outside loop");
|
|
797
845
|
}
|
|
798
|
-
// Emit TRY_END for every open try block between here and the target loop.
|
|
799
846
|
for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
|
|
800
847
|
if (this._loopStack[_ci].type === "try") {
|
|
801
848
|
this.emit(bc, [this.OP.TRY_END], node);
|
|
@@ -816,32 +863,29 @@ export class Compiler {
|
|
|
816
863
|
type: "switch",
|
|
817
864
|
label: _swLabel,
|
|
818
865
|
breakLabel: switchBreakLabel,
|
|
819
|
-
continueLabel: switchBreakLabel
|
|
866
|
+
continueLabel: switchBreakLabel
|
|
820
867
|
});
|
|
821
868
|
|
|
822
|
-
// Compile
|
|
823
|
-
this._compileExpr(node.discriminant, scope, bc);
|
|
869
|
+
// Compile discriminant into a register that lives for the whole switch.
|
|
870
|
+
const discReg = this._compileExpr(node.discriminant, scope, bc);
|
|
824
871
|
const cases = node.cases;
|
|
825
872
|
const defaultIdx = cases.findIndex(c => c.test === null);
|
|
826
|
-
|
|
827
|
-
// Pre-allocate a label for each case body so dispatch can reference them
|
|
828
873
|
const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
|
|
829
874
|
|
|
830
|
-
// Dispatch
|
|
875
|
+
// Dispatch: for each non-default case, test and jump.
|
|
831
876
|
for (let i = 0; i < cases.length; i++) {
|
|
832
877
|
const cas = cases[i];
|
|
833
|
-
if (cas.test === null) continue;
|
|
834
|
-
|
|
878
|
+
if (cas.test === null) continue;
|
|
835
879
|
const nextCheckLabel = this._makeLabel("sw_next");
|
|
836
|
-
|
|
837
|
-
this._compileExpr(cas.test, scope, bc);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
this.emit(bc, [this.OP.JUMP_IF_FALSE, {
|
|
880
|
+
const savedTop = ctx.regTop;
|
|
881
|
+
const caseValReg = this._compileExpr(cas.test, scope, bc);
|
|
882
|
+
const cmpReg = ctx.allocReg();
|
|
883
|
+
this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
|
|
884
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, cmpReg, {
|
|
841
885
|
type: "label",
|
|
842
886
|
label: nextCheckLabel
|
|
843
887
|
}], node);
|
|
844
|
-
|
|
888
|
+
ctx.regTop = savedTop;
|
|
845
889
|
this.emit(bc, [this.OP.JUMP, {
|
|
846
890
|
type: "label",
|
|
847
891
|
label: caseLabels[i]
|
|
@@ -851,14 +895,10 @@ export class Compiler {
|
|
|
851
895
|
label: nextCheckLabel
|
|
852
896
|
}], node);
|
|
853
897
|
}
|
|
854
|
-
|
|
855
|
-
// No case matched: jump to default body or exit (which pops discriminant)
|
|
856
898
|
this.emit(bc, [this.OP.JUMP, {
|
|
857
899
|
type: "label",
|
|
858
900
|
label: defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel
|
|
859
901
|
}], node);
|
|
860
|
-
|
|
861
|
-
// Body section: compile all case bodies in source order (fallthrough intact)
|
|
862
902
|
for (let i = 0; i < cases.length; i++) {
|
|
863
903
|
this.emit(bc, [null, {
|
|
864
904
|
type: "defineLabel",
|
|
@@ -869,12 +909,11 @@ export class Compiler {
|
|
|
869
909
|
}
|
|
870
910
|
}
|
|
871
911
|
|
|
872
|
-
//
|
|
912
|
+
// Break lands here – discriminant register is simply abandoned.
|
|
873
913
|
this.emit(bc, [null, {
|
|
874
914
|
type: "defineLabel",
|
|
875
915
|
label: switchBreakLabel
|
|
876
916
|
}], node);
|
|
877
|
-
this.emit(bc, [this.OP.POP], node);
|
|
878
917
|
this._loopStack.pop();
|
|
879
918
|
break;
|
|
880
919
|
}
|
|
@@ -885,18 +924,16 @@ export class Compiler {
|
|
|
885
924
|
const _lIsLoop = _lBody.type === "ForStatement" || _lBody.type === "WhileStatement" || _lBody.type === "DoWhileStatement" || _lBody.type === "ForInStatement";
|
|
886
925
|
const _lIsSwitch = _lBody.type === "SwitchStatement";
|
|
887
926
|
if (_lIsLoop || _lIsSwitch) {
|
|
888
|
-
// Pass label down to the loop/switch handler via _pendingLabel
|
|
889
927
|
this._pendingLabel = _lName;
|
|
890
928
|
this._compileStatement(_lBody, scope, bc);
|
|
891
|
-
this._pendingLabel = null;
|
|
929
|
+
this._pendingLabel = null;
|
|
892
930
|
} else {
|
|
893
|
-
// Non-loop labeled statement (e.g. labeled block) -- only break is valid
|
|
894
931
|
const blockBreakLabel = this._makeLabel("block_break");
|
|
895
932
|
this._loopStack.push({
|
|
896
933
|
type: "block",
|
|
897
934
|
label: _lName,
|
|
898
935
|
breakLabel: blockBreakLabel,
|
|
899
|
-
continueLabel: blockBreakLabel
|
|
936
|
+
continueLabel: blockBreakLabel
|
|
900
937
|
});
|
|
901
938
|
this._compileStatement(_lBody, scope, bc);
|
|
902
939
|
this._loopStack.pop();
|
|
@@ -912,72 +949,56 @@ export class Compiler {
|
|
|
912
949
|
const _fiLabel = this._pendingLabel;
|
|
913
950
|
this._pendingLabel = null;
|
|
914
951
|
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
// FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
|
|
918
|
-
this.emit(bc, [this.OP.FOR_IN_SETUP], node);
|
|
952
|
+
// Iterator register was reserved by _hoistVars.
|
|
953
|
+
const iterSlot = node._iterSlot;
|
|
919
954
|
|
|
920
|
-
//
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (scope) {
|
|
924
|
-
// Reserve a hidden local slot (no name mapping needed)
|
|
925
|
-
const iterSlot = scope._next++;
|
|
926
|
-
emitLoadIter = () => this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
|
|
927
|
-
emitStoreIter = () => this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
|
|
928
|
-
} else {
|
|
929
|
-
// Top level -- use a synthetic global that won't collide with user code
|
|
930
|
-
const iterNameIdx = b.constantOperand("__fi" + this._forInCount++);
|
|
931
|
-
emitLoadIter = () => this.emit(bc, [this.OP.LOAD_GLOBAL, iterNameIdx], node);
|
|
932
|
-
emitStoreIter = () => this.emit(bc, [this.OP.STORE_GLOBAL, iterNameIdx], node);
|
|
933
|
-
}
|
|
934
|
-
emitStoreIter();
|
|
955
|
+
// FOR_IN_SETUP dst, src
|
|
956
|
+
const objReg = this._compileExpr(node.right, scope, bc);
|
|
957
|
+
this.emit(bc, [this.OP.FOR_IN_SETUP, iterSlot, objReg], node);
|
|
935
958
|
const loopTopLabel = this._makeLabel("forin_top");
|
|
936
959
|
const exitLabel = this._makeLabel("forin_exit");
|
|
937
960
|
this._loopStack.push({
|
|
938
961
|
type: "loop",
|
|
939
962
|
label: _fiLabel,
|
|
940
963
|
breakLabel: exitLabel,
|
|
941
|
-
continueLabel: loopTopLabel
|
|
964
|
+
continueLabel: loopTopLabel
|
|
942
965
|
});
|
|
943
966
|
this.emit(bc, [null, {
|
|
944
967
|
type: "defineLabel",
|
|
945
968
|
label: loopTopLabel
|
|
946
969
|
}], node);
|
|
947
970
|
|
|
948
|
-
//
|
|
949
|
-
|
|
950
|
-
this.emit(bc, [this.OP.FOR_IN_NEXT, {
|
|
971
|
+
// FOR_IN_NEXT keyDst, iter, exitTarget
|
|
972
|
+
const keyReg = ctx.allocReg();
|
|
973
|
+
this.emit(bc, [this.OP.FOR_IN_NEXT, keyReg, iterSlot, {
|
|
951
974
|
type: "label",
|
|
952
975
|
label: exitLabel
|
|
953
976
|
}], node);
|
|
954
977
|
|
|
955
|
-
// Assign the key
|
|
978
|
+
// Assign the key to the loop variable.
|
|
956
979
|
if (node.left.type === "VariableDeclaration") {
|
|
957
980
|
const identifier = node.left.declarations[0].id;
|
|
958
981
|
ok(identifier.type === "Identifier", "Only simple identifiers can be declared in for-in loops");
|
|
959
982
|
const name = identifier.name;
|
|
960
983
|
if (scope) {
|
|
961
|
-
const slot = scope.
|
|
962
|
-
this.emit(bc, [this.OP.
|
|
984
|
+
const slot = scope._locals.get(name);
|
|
985
|
+
if (keyReg !== slot) this.emit(bc, [this.OP.MOVE, slot, keyReg], node);
|
|
963
986
|
} else {
|
|
964
|
-
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name)], node);
|
|
987
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), keyReg], node);
|
|
965
988
|
}
|
|
966
989
|
} else if (node.left.type === "Identifier") {
|
|
967
990
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
968
991
|
if (res.kind === "local") {
|
|
969
|
-
this.emit(bc, [this.OP.
|
|
992
|
+
if (keyReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, keyReg], node);
|
|
970
993
|
} else if (res.kind === "upvalue") {
|
|
971
|
-
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
994
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
|
|
972
995
|
} else {
|
|
973
|
-
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)], node);
|
|
996
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name), keyReg], node);
|
|
974
997
|
}
|
|
975
998
|
} else {
|
|
976
999
|
const src = generate(node.left).code;
|
|
977
1000
|
throw new Error(`Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`);
|
|
978
1001
|
}
|
|
979
|
-
|
|
980
|
-
// Compile the loop body
|
|
981
1002
|
const fiBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
982
1003
|
for (const stmt of fiBody) {
|
|
983
1004
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -999,72 +1020,44 @@ export class Compiler {
|
|
|
999
1020
|
throw new Error("try..finally is not supported. Use a helper function instead");
|
|
1000
1021
|
}
|
|
1001
1022
|
if (!node.handler) {
|
|
1002
|
-
// try without catch requires finally — not supported
|
|
1003
1023
|
throw new Error("try without catch is not supported (requires finally).");
|
|
1004
1024
|
}
|
|
1005
1025
|
const catchLabel = this._makeLabel("catch");
|
|
1006
1026
|
const afterCatchLabel = this._makeLabel("after_catch");
|
|
1007
1027
|
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1028
|
+
// Determine where the caught exception is written.
|
|
1029
|
+
const exceptionReg = node.handler.param?.type === "Identifier" ? scope?._locals.get(node.handler.param.name) ?? ctx.allocReg() // shouldn't normally reach here
|
|
1030
|
+
: node._exceptionSlot;
|
|
1010
1031
|
this.emit(bc, [this.OP.TRY_SETUP, {
|
|
1011
1032
|
type: "label",
|
|
1012
1033
|
label: catchLabel
|
|
1013
|
-
}], node);
|
|
1014
|
-
|
|
1015
|
-
// Track the open try block so that break/continue/return inside the
|
|
1016
|
-
// try body can emit the matching TRY_END before their jump.
|
|
1034
|
+
}, exceptionReg], node);
|
|
1017
1035
|
this._loopStack.push({
|
|
1018
1036
|
type: "try",
|
|
1019
1037
|
label: null,
|
|
1020
1038
|
breakLabel: "",
|
|
1021
|
-
|
|
1022
|
-
continueLabel: "" // unused
|
|
1039
|
+
continueLabel: ""
|
|
1023
1040
|
});
|
|
1024
|
-
|
|
1025
|
-
// Compile try body
|
|
1026
1041
|
for (const stmt of node.block.body) {
|
|
1027
1042
|
this._compileStatement(stmt, scope, bc);
|
|
1028
1043
|
}
|
|
1029
|
-
|
|
1030
|
-
// Done compiling the try body — pop the tracking entry.
|
|
1031
1044
|
this._loopStack.pop();
|
|
1032
|
-
|
|
1033
|
-
// Normal exit: disarm the exception handler.
|
|
1034
1045
|
this.emit(bc, [this.OP.TRY_END], node);
|
|
1035
|
-
|
|
1036
|
-
// Jump over the catch block on normal path.
|
|
1037
1046
|
this.emit(bc, [this.OP.JUMP, {
|
|
1038
1047
|
type: "label",
|
|
1039
1048
|
label: afterCatchLabel
|
|
1040
1049
|
}], node);
|
|
1041
1050
|
|
|
1042
|
-
// Catch block:
|
|
1051
|
+
// Catch block: exceptionReg already holds the caught value.
|
|
1043
1052
|
this.emit(bc, [null, {
|
|
1044
1053
|
type: "defineLabel",
|
|
1045
1054
|
label: catchLabel
|
|
1046
1055
|
}], node);
|
|
1047
|
-
const handler = node.handler;
|
|
1048
|
-
if (handler.param) {
|
|
1049
|
-
// Bind the exception value to the catch variable.
|
|
1050
|
-
const name = handler.param.name;
|
|
1051
|
-
if (scope) {
|
|
1052
|
-
const slot = scope.define(name);
|
|
1053
|
-
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
1054
|
-
} else {
|
|
1055
|
-
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name)], node);
|
|
1056
|
-
}
|
|
1057
|
-
} else {
|
|
1058
|
-
// Optional catch binding (catch without a variable — ES2019+)
|
|
1059
|
-
this.emit(bc, [this.OP.POP], node);
|
|
1060
|
-
}
|
|
1061
1056
|
|
|
1062
|
-
//
|
|
1063
|
-
for (const stmt of handler.body.body) {
|
|
1057
|
+
// If no param binding, just ignore the exception (it's in the dummy slot).
|
|
1058
|
+
for (const stmt of node.handler.body.body) {
|
|
1064
1059
|
this._compileStatement(stmt, scope, bc);
|
|
1065
1060
|
}
|
|
1066
|
-
|
|
1067
|
-
// Normal-path jump lands here (after the catch block).
|
|
1068
1061
|
this.emit(bc, [null, {
|
|
1069
1062
|
type: "defineLabel",
|
|
1070
1063
|
label: afterCatchLabel
|
|
@@ -1073,139 +1066,154 @@ export class Compiler {
|
|
|
1073
1066
|
}
|
|
1074
1067
|
default:
|
|
1075
1068
|
{
|
|
1076
|
-
// Use @babel/generator to reproduce the source of unsupported nodes
|
|
1077
|
-
// so we can emit a clear error with context.
|
|
1078
1069
|
const src = generate(node).code;
|
|
1079
1070
|
throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
|
|
1080
1071
|
}
|
|
1081
1072
|
}
|
|
1082
1073
|
}
|
|
1083
1074
|
|
|
1084
|
-
// Expressions
|
|
1075
|
+
// ── Expressions ───────────────────────────────────────────────────────────
|
|
1076
|
+
// Returns the register index that holds the result.
|
|
1077
|
+
// For local variables: returns their slot directly (no instruction emitted).
|
|
1078
|
+
// For all others: allocates a fresh temp register, emits the instruction(s),
|
|
1079
|
+
// and returns the allocated register.
|
|
1085
1080
|
_compileExpr(node, scope, bc) {
|
|
1081
|
+
const ctx = this._currentCtx;
|
|
1086
1082
|
switch (node.type) {
|
|
1087
1083
|
case "NumericLiteral":
|
|
1088
1084
|
case "StringLiteral":
|
|
1089
|
-
{
|
|
1090
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.value)], node);
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
1085
|
case "BooleanLiteral":
|
|
1094
1086
|
{
|
|
1095
|
-
|
|
1096
|
-
|
|
1087
|
+
const dst = ctx.allocReg();
|
|
1088
|
+
this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(node.value)], node);
|
|
1089
|
+
return dst;
|
|
1097
1090
|
}
|
|
1098
1091
|
case "NullLiteral":
|
|
1099
1092
|
{
|
|
1100
|
-
|
|
1101
|
-
|
|
1093
|
+
const dst = ctx.allocReg();
|
|
1094
|
+
this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(null)], node);
|
|
1095
|
+
return dst;
|
|
1102
1096
|
}
|
|
1103
1097
|
case "Identifier":
|
|
1104
1098
|
{
|
|
1105
|
-
// scope=null means we're at the top-level -> always global
|
|
1106
1099
|
const res = this._resolve(node.name, this._currentCtx);
|
|
1107
|
-
if (res.kind === "local")
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1111
|
-
|
|
1112
|
-
this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.name)], node);
|
|
1100
|
+
if (res.kind === "local") return res.slot; // register IS the local
|
|
1101
|
+
if (res.kind === "upvalue") {
|
|
1102
|
+
const dst = ctx.allocReg();
|
|
1103
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
|
|
1104
|
+
return dst;
|
|
1113
1105
|
}
|
|
1114
|
-
|
|
1106
|
+
// global
|
|
1107
|
+
const dst = ctx.allocReg();
|
|
1108
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, dst, b.constantOperand(node.name)], node);
|
|
1109
|
+
return dst;
|
|
1115
1110
|
}
|
|
1116
1111
|
case "ThisExpression":
|
|
1117
1112
|
{
|
|
1118
|
-
|
|
1119
|
-
|
|
1113
|
+
const dst = ctx.allocReg();
|
|
1114
|
+
this.emit(bc, [this.OP.LOAD_THIS, dst], node);
|
|
1115
|
+
return dst;
|
|
1120
1116
|
}
|
|
1121
1117
|
case "NewExpression":
|
|
1122
1118
|
{
|
|
1123
|
-
|
|
1124
|
-
this._compileExpr(
|
|
1125
|
-
|
|
1126
|
-
this.emit(bc, [this.OP.NEW, node.arguments.length], node);
|
|
1127
|
-
|
|
1119
|
+
const calleeReg = this._compileExpr(node.callee, scope, bc);
|
|
1120
|
+
const argRegs = node.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1121
|
+
const dst = ctx.allocReg();
|
|
1122
|
+
this.emit(bc, [this.OP.NEW, dst, calleeReg, node.arguments.length, ...argRegs], node);
|
|
1123
|
+
return dst;
|
|
1128
1124
|
}
|
|
1129
1125
|
case "SequenceExpression":
|
|
1130
1126
|
{
|
|
1131
|
-
|
|
1132
|
-
for (let i = 0; i <
|
|
1133
|
-
|
|
1134
|
-
this.
|
|
1127
|
+
const exprs = node.expressions;
|
|
1128
|
+
for (let i = 0; i < exprs.length - 1; i++) {
|
|
1129
|
+
const savedTop = ctx.regTop;
|
|
1130
|
+
this._compileExpr(exprs[i], scope, bc);
|
|
1131
|
+
ctx.regTop = savedTop; // discard intermediate result
|
|
1135
1132
|
}
|
|
1136
|
-
|
|
1137
|
-
this._compileExpr(node.expressions[node.expressions.length - 1], scope, bc);
|
|
1138
|
-
break;
|
|
1133
|
+
return this._compileExpr(exprs[exprs.length - 1], scope, bc);
|
|
1139
1134
|
}
|
|
1140
1135
|
case "ConditionalExpression":
|
|
1141
1136
|
{
|
|
1142
|
-
|
|
1137
|
+
const n = node;
|
|
1143
1138
|
const elseLabel = this._makeLabel("ternary_else");
|
|
1144
1139
|
const endLabel = this._makeLabel("ternary_end");
|
|
1145
|
-
|
|
1146
|
-
|
|
1140
|
+
|
|
1141
|
+
// Compile test; free its temps after the jump is emitted.
|
|
1142
|
+
const baseTop = ctx.regTop;
|
|
1143
|
+
const testReg = this._compileExpr(n.test, scope, bc);
|
|
1144
|
+
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
1147
1145
|
type: "label",
|
|
1148
1146
|
label: elseLabel
|
|
1149
1147
|
}], node);
|
|
1150
|
-
|
|
1148
|
+
ctx.regTop = baseTop; // free test temps
|
|
1149
|
+
|
|
1150
|
+
// Reserve reg_result at the base of the temp space.
|
|
1151
|
+
const reg_result = ctx.allocReg();
|
|
1152
|
+
|
|
1153
|
+
// Consequent branch.
|
|
1154
|
+
const consReg = this._compileExpr(n.consequent, scope, bc);
|
|
1155
|
+
if (consReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, consReg], node);
|
|
1151
1156
|
this.emit(bc, [this.OP.JUMP, {
|
|
1152
1157
|
type: "label",
|
|
1153
1158
|
label: endLabel
|
|
1154
1159
|
}], node);
|
|
1160
|
+
|
|
1161
|
+
// Alternate branch: reset to baseTop then re-reserve reg_result.
|
|
1155
1162
|
this.emit(bc, [null, {
|
|
1156
1163
|
type: "defineLabel",
|
|
1157
1164
|
label: elseLabel
|
|
1158
1165
|
}], node);
|
|
1159
|
-
|
|
1166
|
+
ctx.regTop = baseTop;
|
|
1167
|
+
ctx.allocReg(); // re-occupy reg_result slot
|
|
1168
|
+
const altReg = this._compileExpr(n.alternate, scope, bc);
|
|
1169
|
+
if (altReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
|
|
1160
1170
|
this.emit(bc, [null, {
|
|
1161
1171
|
type: "defineLabel",
|
|
1162
1172
|
label: endLabel
|
|
1163
1173
|
}], node);
|
|
1164
|
-
|
|
1174
|
+
|
|
1175
|
+
// Leave reg_result allocated above baseTop.
|
|
1176
|
+
ctx.regTop = baseTop + 1;
|
|
1177
|
+
return reg_result;
|
|
1165
1178
|
}
|
|
1166
1179
|
case "LogicalExpression":
|
|
1167
1180
|
{
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
type: "defineLabel",
|
|
1197
|
-
label: endLabel
|
|
1198
|
-
}], node);
|
|
1199
|
-
} else {
|
|
1200
|
-
throw new Error(`Unsupported logical operator: ${node.operator}`);
|
|
1201
|
-
}
|
|
1202
|
-
break;
|
|
1181
|
+
const n = node;
|
|
1182
|
+
const endLabel = this._makeLabel("logical_end");
|
|
1183
|
+
const isOr = n.operator === "||";
|
|
1184
|
+
if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
|
|
1185
|
+
const baseTop = ctx.regTop;
|
|
1186
|
+
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1187
|
+
ctx.regTop = baseTop;
|
|
1188
|
+
const reg_result = ctx.allocReg();
|
|
1189
|
+
if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
|
|
1190
|
+
|
|
1191
|
+
// For ||: if truthy keep LHS, jump past RHS.
|
|
1192
|
+
// For &&: if falsy keep LHS, jump past RHS.
|
|
1193
|
+
this.emit(bc, [isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE, reg_result, {
|
|
1194
|
+
type: "label",
|
|
1195
|
+
label: endLabel
|
|
1196
|
+
}], node);
|
|
1197
|
+
|
|
1198
|
+
// Compile RHS into reg_result.
|
|
1199
|
+
ctx.regTop = baseTop;
|
|
1200
|
+
ctx.allocReg(); // re-occupy reg_result
|
|
1201
|
+
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1202
|
+
if (rhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
|
|
1203
|
+
this.emit(bc, [null, {
|
|
1204
|
+
type: "defineLabel",
|
|
1205
|
+
label: endLabel
|
|
1206
|
+
}], node);
|
|
1207
|
+
ctx.regTop = baseTop + 1;
|
|
1208
|
+
return reg_result;
|
|
1203
1209
|
}
|
|
1204
1210
|
case "BinaryExpression":
|
|
1205
1211
|
{
|
|
1206
|
-
|
|
1207
|
-
this._compileExpr(
|
|
1208
|
-
const
|
|
1212
|
+
const n = node;
|
|
1213
|
+
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1214
|
+
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1215
|
+
const dst = ctx.allocReg();
|
|
1216
|
+
const op = {
|
|
1209
1217
|
"+": this.OP.ADD,
|
|
1210
1218
|
"-": this.OP.SUB,
|
|
1211
1219
|
"*": this.OP.MUL,
|
|
@@ -1216,9 +1224,7 @@ export class Compiler {
|
|
|
1216
1224
|
"^": this.OP.BXOR,
|
|
1217
1225
|
"<<": this.OP.SHL,
|
|
1218
1226
|
">>": this.OP.SHR,
|
|
1219
|
-
">>>": this.OP.USHR
|
|
1220
|
-
}[node.operator];
|
|
1221
|
-
const cmpOp = {
|
|
1227
|
+
">>>": this.OP.USHR,
|
|
1222
1228
|
"<": this.OP.LT,
|
|
1223
1229
|
">": this.OP.GT,
|
|
1224
1230
|
"===": this.OP.EQ,
|
|
@@ -1228,38 +1234,65 @@ export class Compiler {
|
|
|
1228
1234
|
"!==": this.OP.NEQ,
|
|
1229
1235
|
"!=": this.OP.LOOSE_NEQ,
|
|
1230
1236
|
in: this.OP.IN,
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
this.emit(bc, [resolvedOp], node);
|
|
1237
|
-
break;
|
|
1237
|
+
instanceof: this.OP.INSTANCEOF
|
|
1238
|
+
}[n.operator];
|
|
1239
|
+
if (op === undefined) throw new Error(`Unsupported operator: ${n.operator}`);
|
|
1240
|
+
this.emit(bc, [op, dst, lhsReg, rhsReg], node);
|
|
1241
|
+
return dst;
|
|
1238
1242
|
}
|
|
1239
1243
|
case "UpdateExpression":
|
|
1240
1244
|
{
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1245
|
+
const n = node;
|
|
1246
|
+
ok(n.argument.type === "Identifier", "UpdateExpression requires identifier");
|
|
1247
|
+
const name = n.argument.name;
|
|
1248
|
+
const res = this._resolve(name, this._currentCtx);
|
|
1249
|
+
const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1250
|
+
|
|
1251
|
+
// Load current value into a register (locals are already in place)
|
|
1252
|
+
let curReg;
|
|
1253
|
+
if (res.kind === "local") {
|
|
1254
|
+
curReg = res.slot;
|
|
1255
|
+
} else if (res.kind === "upvalue") {
|
|
1256
|
+
curReg = ctx.allocReg();
|
|
1257
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
1258
|
+
} else {
|
|
1259
|
+
curReg = ctx.allocReg();
|
|
1260
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, curReg, b.constantOperand(name)], node);
|
|
1261
|
+
}
|
|
1258
1262
|
|
|
1259
|
-
|
|
1263
|
+
// For postfix we need to preserve the *old* value as the result
|
|
1264
|
+
let resultReg;
|
|
1265
|
+
if (!n.prefix) {
|
|
1266
|
+
resultReg = ctx.allocReg();
|
|
1267
|
+
this.emit(bc, [this.OP.MOVE, resultReg, curReg], node);
|
|
1268
|
+
} else {
|
|
1269
|
+
resultReg = -1; // placeholder – will become newReg below
|
|
1270
|
+
}
|
|
1271
|
+
const oneReg = ctx.allocReg();
|
|
1272
|
+
this.emit(bc, [this.OP.LOAD_CONST, oneReg, b.constantOperand(1)], node);
|
|
1273
|
+
|
|
1274
|
+
// Compute new value (always into a fresh register)
|
|
1275
|
+
const newReg = ctx.allocReg();
|
|
1276
|
+
this.emit(bc, [bumpOp, newReg, curReg, oneReg], node);
|
|
1277
|
+
|
|
1278
|
+
// Write the new value back (local = MOVE, others = STORE_xxx)
|
|
1279
|
+
if (res.kind === "local") {
|
|
1280
|
+
this.emit(bc, [this.OP.MOVE, res.slot, newReg], node);
|
|
1281
|
+
} else if (res.kind === "upvalue") {
|
|
1282
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, newReg], node);
|
|
1283
|
+
} else {
|
|
1284
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name), newReg], node);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Prefix returns the *new* value (we already have it in newReg – no reload needed)
|
|
1288
|
+
if (n.prefix) {
|
|
1289
|
+
resultReg = newReg;
|
|
1290
|
+
}
|
|
1291
|
+
return resultReg;
|
|
1260
1292
|
}
|
|
1261
1293
|
case "AssignmentExpression":
|
|
1262
1294
|
{
|
|
1295
|
+
const n = node;
|
|
1263
1296
|
const compoundOp = {
|
|
1264
1297
|
"+=": this.OP.ADD,
|
|
1265
1298
|
"-=": this.OP.SUB,
|
|
@@ -1272,268 +1305,242 @@ export class Compiler {
|
|
|
1272
1305
|
"<<=": this.OP.SHL,
|
|
1273
1306
|
">>=": this.OP.SHR,
|
|
1274
1307
|
">>>=": this.OP.USHR
|
|
1275
|
-
}[
|
|
1308
|
+
}[n.operator];
|
|
1276
1309
|
const isCompound = compoundOp !== undefined;
|
|
1277
|
-
if (
|
|
1278
|
-
throw new Error(`Unsupported assignment operator: ${node.operator}`);
|
|
1279
|
-
}
|
|
1310
|
+
if (n.operator !== "=" && !isCompound) throw new Error(`Unsupported assignment operator: ${n.operator}`);
|
|
1280
1311
|
|
|
1281
1312
|
// Member assignment: obj.x = val or arr[i] = val
|
|
1282
|
-
if (
|
|
1283
|
-
this._compileExpr(
|
|
1284
|
-
|
|
1285
|
-
if (
|
|
1286
|
-
this._compileExpr(
|
|
1313
|
+
if (n.left.type === "MemberExpression") {
|
|
1314
|
+
const objReg = this._compileExpr(n.left.object, scope, bc);
|
|
1315
|
+
let keyReg;
|
|
1316
|
+
if (n.left.computed) {
|
|
1317
|
+
keyReg = this._compileExpr(n.left.property, scope, bc);
|
|
1287
1318
|
} else {
|
|
1288
|
-
|
|
1319
|
+
keyReg = ctx.allocReg();
|
|
1320
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(n.left.property.name)], node);
|
|
1289
1321
|
}
|
|
1322
|
+
let valReg;
|
|
1290
1323
|
if (isCompound) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
this._compileExpr(node.left.object, scope, bc);
|
|
1297
|
-
if (node.left.computed) {
|
|
1298
|
-
this._compileExpr(node.left.property, scope, bc);
|
|
1299
|
-
} else {
|
|
1300
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)], node);
|
|
1301
|
-
}
|
|
1302
|
-
this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
|
|
1303
|
-
this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
|
|
1304
|
-
this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
|
|
1324
|
+
const curReg = ctx.allocReg();
|
|
1325
|
+
this.emit(bc, [this.OP.GET_PROP, curReg, objReg, keyReg], node);
|
|
1326
|
+
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1327
|
+
valReg = ctx.allocReg();
|
|
1328
|
+
this.emit(bc, [compoundOp, valReg, curReg, rhsReg], node);
|
|
1305
1329
|
} else {
|
|
1306
|
-
this._compileExpr(
|
|
1330
|
+
valReg = this._compileExpr(n.right, scope, bc);
|
|
1307
1331
|
}
|
|
1308
|
-
this.emit(bc, [this.OP.SET_PROP
|
|
1309
|
-
|
|
1332
|
+
this.emit(bc, [this.OP.SET_PROP, objReg, keyReg, valReg], node);
|
|
1333
|
+
return valReg;
|
|
1310
1334
|
}
|
|
1311
1335
|
|
|
1312
|
-
// Plain identifier assignment
|
|
1313
|
-
const res = this._resolve(
|
|
1336
|
+
// Plain identifier assignment.
|
|
1337
|
+
const res = this._resolve(n.left.name, this._currentCtx);
|
|
1338
|
+
let rhsReg;
|
|
1314
1339
|
if (isCompound) {
|
|
1315
|
-
// Load
|
|
1340
|
+
// Load current value of the variable.
|
|
1341
|
+
let curReg;
|
|
1316
1342
|
if (res.kind === "local") {
|
|
1317
|
-
|
|
1343
|
+
curReg = res.slot;
|
|
1318
1344
|
} else if (res.kind === "upvalue") {
|
|
1319
|
-
|
|
1345
|
+
curReg = ctx.allocReg();
|
|
1346
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
1320
1347
|
} else {
|
|
1321
|
-
|
|
1348
|
+
curReg = ctx.allocReg();
|
|
1349
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, curReg, b.constantOperand(n.left.name)], node);
|
|
1322
1350
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
this.
|
|
1351
|
+
const rhs2 = this._compileExpr(n.right, scope, bc);
|
|
1352
|
+
rhsReg = ctx.allocReg();
|
|
1353
|
+
this.emit(bc, [compoundOp, rhsReg, curReg, rhs2], node);
|
|
1354
|
+
} else {
|
|
1355
|
+
rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1328
1356
|
}
|
|
1329
1357
|
|
|
1330
|
-
// Store
|
|
1358
|
+
// Store result and return it.
|
|
1331
1359
|
if (res.kind === "local") {
|
|
1332
|
-
this.emit(bc, [this.OP.
|
|
1333
|
-
|
|
1360
|
+
if (rhsReg !== res.slot) this.emit(bc, [this.OP.MOVE, res.slot, rhsReg], node);
|
|
1361
|
+
return res.slot;
|
|
1334
1362
|
} else if (res.kind === "upvalue") {
|
|
1335
|
-
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
1336
|
-
|
|
1363
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
|
|
1364
|
+
return rhsReg;
|
|
1337
1365
|
} else {
|
|
1338
|
-
const nameIdx = b.constantOperand(
|
|
1339
|
-
this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx], node);
|
|
1340
|
-
|
|
1366
|
+
const nameIdx = b.constantOperand(n.left.name);
|
|
1367
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx, rhsReg], node);
|
|
1368
|
+
return rhsReg;
|
|
1341
1369
|
}
|
|
1342
|
-
break;
|
|
1343
1370
|
}
|
|
1344
1371
|
case "CallExpression":
|
|
1345
1372
|
{
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
//
|
|
1349
|
-
this._compileExpr(
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1373
|
+
const n = node;
|
|
1374
|
+
if (n.callee.type === "MemberExpression") {
|
|
1375
|
+
// Method call: receiver.method(args)
|
|
1376
|
+
const receiverReg = this._compileExpr(n.callee.object, scope, bc);
|
|
1377
|
+
let methodKeyReg;
|
|
1378
|
+
if (n.callee.computed) {
|
|
1379
|
+
methodKeyReg = this._compileExpr(n.callee.property, scope, bc);
|
|
1380
|
+
} else {
|
|
1381
|
+
methodKeyReg = ctx.allocReg();
|
|
1382
|
+
this.emit(bc, [this.OP.LOAD_CONST, methodKeyReg, b.constantOperand(n.callee.property.name)], node);
|
|
1383
|
+
}
|
|
1384
|
+
const calleeReg = ctx.allocReg();
|
|
1385
|
+
this.emit(bc, [this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg], node);
|
|
1386
|
+
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1387
|
+
const dst = ctx.allocReg();
|
|
1388
|
+
this.emit(bc, [this.OP.CALL_METHOD, dst, receiverReg, calleeReg, n.arguments.length, ...argRegs], node);
|
|
1389
|
+
return dst;
|
|
1356
1390
|
} else {
|
|
1357
|
-
//
|
|
1358
|
-
this._compileExpr(
|
|
1359
|
-
|
|
1360
|
-
|
|
1391
|
+
// Plain call: fn(args)
|
|
1392
|
+
const calleeReg = this._compileExpr(n.callee, scope, bc);
|
|
1393
|
+
const argRegs = n.arguments.map(a => this._compileExpr(a, scope, bc));
|
|
1394
|
+
const dst = ctx.allocReg();
|
|
1395
|
+
this.emit(bc, [this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs], node);
|
|
1396
|
+
return dst;
|
|
1361
1397
|
}
|
|
1362
|
-
break;
|
|
1363
1398
|
}
|
|
1364
1399
|
case "UnaryExpression":
|
|
1365
1400
|
{
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
//
|
|
1369
|
-
if (
|
|
1370
|
-
const res = this._resolve(
|
|
1401
|
+
const n = node;
|
|
1402
|
+
|
|
1403
|
+
// typeof on a potentially-undeclared global -- safe guard.
|
|
1404
|
+
if (n.operator === "typeof" && n.argument.type === "Identifier") {
|
|
1405
|
+
const res = this._resolve(n.argument.name, this._currentCtx);
|
|
1371
1406
|
if (res.kind === "global") {
|
|
1372
|
-
|
|
1373
|
-
this.emit(bc, [this.OP.
|
|
1374
|
-
|
|
1375
|
-
break;
|
|
1407
|
+
const dst = ctx.allocReg();
|
|
1408
|
+
this.emit(bc, [this.OP.TYPEOF_SAFE, dst, b.constantOperand(n.argument.name)], node);
|
|
1409
|
+
return dst;
|
|
1376
1410
|
}
|
|
1377
|
-
// Known local or upvalue -- safe to load first, then typeof
|
|
1378
1411
|
}
|
|
1379
1412
|
|
|
1380
|
-
//
|
|
1381
|
-
if (
|
|
1382
|
-
const arg =
|
|
1413
|
+
// delete expression.
|
|
1414
|
+
if (n.operator === "delete") {
|
|
1415
|
+
const arg = n.argument;
|
|
1383
1416
|
if (arg.type === "MemberExpression") {
|
|
1384
|
-
this._compileExpr(arg.object, scope, bc);
|
|
1417
|
+
const objReg = this._compileExpr(arg.object, scope, bc);
|
|
1418
|
+
let keyReg;
|
|
1385
1419
|
if (arg.computed) {
|
|
1386
|
-
this._compileExpr(arg.property, scope, bc);
|
|
1420
|
+
keyReg = this._compileExpr(arg.property, scope, bc);
|
|
1387
1421
|
} else {
|
|
1388
|
-
|
|
1422
|
+
keyReg = ctx.allocReg();
|
|
1423
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(arg.property.name)], node);
|
|
1389
1424
|
}
|
|
1390
|
-
|
|
1425
|
+
const dst = ctx.allocReg();
|
|
1426
|
+
this.emit(bc, [this.OP.DELETE_PROP, dst, objReg, keyReg], node);
|
|
1427
|
+
return dst;
|
|
1391
1428
|
} else {
|
|
1392
|
-
// delete x
|
|
1393
|
-
|
|
1429
|
+
// delete x or delete 0 -- always true in sloppy mode.
|
|
1430
|
+
const dst = ctx.allocReg();
|
|
1431
|
+
this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(true)], node);
|
|
1432
|
+
return dst;
|
|
1394
1433
|
}
|
|
1395
|
-
break;
|
|
1396
1434
|
}
|
|
1397
1435
|
|
|
1398
|
-
// All other unary
|
|
1399
|
-
this._compileExpr(
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
break;
|
|
1413
|
-
case "typeof":
|
|
1414
|
-
this.emit(bc, [this.OP.TYPEOF], node);
|
|
1415
|
-
break;
|
|
1416
|
-
case "void":
|
|
1417
|
-
this.emit(bc, [this.OP.VOID], node);
|
|
1418
|
-
break;
|
|
1419
|
-
default:
|
|
1420
|
-
throw new Error(`Unsupported unary operator: ${node.operator}`);
|
|
1421
|
-
}
|
|
1422
|
-
break;
|
|
1436
|
+
// All other unary operators.
|
|
1437
|
+
const srcReg = this._compileExpr(n.argument, scope, bc);
|
|
1438
|
+
const dst = ctx.allocReg();
|
|
1439
|
+
const unaryOp = {
|
|
1440
|
+
"-": this.OP.UNARY_NEG,
|
|
1441
|
+
"+": this.OP.UNARY_POS,
|
|
1442
|
+
"!": this.OP.UNARY_NOT,
|
|
1443
|
+
"~": this.OP.UNARY_BITNOT,
|
|
1444
|
+
typeof: this.OP.TYPEOF,
|
|
1445
|
+
void: this.OP.VOID
|
|
1446
|
+
}[n.operator];
|
|
1447
|
+
if (unaryOp === undefined) throw new Error(`Unsupported unary operator: ${n.operator}`);
|
|
1448
|
+
this.emit(bc, [unaryOp, dst, srcReg], node);
|
|
1449
|
+
return dst;
|
|
1423
1450
|
}
|
|
1424
1451
|
case "RegExpLiteral":
|
|
1425
1452
|
{
|
|
1426
|
-
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1429
|
-
this.emit(bc, [this.OP.
|
|
1430
|
-
|
|
1431
|
-
this.emit(bc, [this.OP.
|
|
1432
|
-
|
|
1453
|
+
const n = node;
|
|
1454
|
+
// new RegExp(pattern, flags)
|
|
1455
|
+
const regExpReg = ctx.allocReg();
|
|
1456
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, regExpReg, b.constantOperand("RegExp")], node);
|
|
1457
|
+
const patternReg = ctx.allocReg();
|
|
1458
|
+
this.emit(bc, [this.OP.LOAD_CONST, patternReg, b.constantOperand(n.pattern)], node);
|
|
1459
|
+
const flagsReg = ctx.allocReg();
|
|
1460
|
+
this.emit(bc, [this.OP.LOAD_CONST, flagsReg, b.constantOperand(n.flags)], node);
|
|
1461
|
+
const dst = ctx.allocReg();
|
|
1462
|
+
this.emit(bc, [this.OP.NEW, dst, regExpReg, 2, patternReg, flagsReg], node);
|
|
1463
|
+
return dst;
|
|
1433
1464
|
}
|
|
1434
1465
|
case "FunctionExpression":
|
|
1435
1466
|
{
|
|
1436
|
-
// Compile into a descriptor exactly like a declaration,
|
|
1437
|
-
// but leave the resulting closure ON THE STACK -- no store.
|
|
1438
|
-
// The surrounding expression (assignment, call arg, return) consumes it.
|
|
1439
1467
|
const desc = this._compileFunctionDecl(node);
|
|
1440
|
-
this._emitMakeClosure(desc, node, bc);
|
|
1441
|
-
break;
|
|
1468
|
+
return this._emitMakeClosure(desc, node, bc);
|
|
1442
1469
|
}
|
|
1443
1470
|
case "MemberExpression":
|
|
1444
1471
|
{
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1472
|
+
const n = node;
|
|
1473
|
+
const objReg = this._compileExpr(n.object, scope, bc);
|
|
1474
|
+
let keyReg;
|
|
1475
|
+
if (n.computed) {
|
|
1476
|
+
keyReg = this._compileExpr(n.property, scope, bc);
|
|
1449
1477
|
} else {
|
|
1450
|
-
|
|
1451
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(
|
|
1478
|
+
keyReg = ctx.allocReg();
|
|
1479
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(n.property.name)], node);
|
|
1452
1480
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
// where the receiver must survive on the stack for CALL_METHOD.
|
|
1457
|
-
this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
|
|
1458
|
-
break;
|
|
1481
|
+
const dst = ctx.allocReg();
|
|
1482
|
+
this.emit(bc, [this.OP.GET_PROP, dst, objReg, keyReg], node);
|
|
1483
|
+
return dst;
|
|
1459
1484
|
}
|
|
1460
1485
|
case "ArrayExpression":
|
|
1461
1486
|
{
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
for (const el of node.elements) {
|
|
1487
|
+
const n = node;
|
|
1488
|
+
const elemRegs = n.elements.map(el => {
|
|
1465
1489
|
if (el === null) {
|
|
1466
|
-
|
|
1467
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
|
|
1468
|
-
|
|
1469
|
-
this._compileExpr(el, scope, bc);
|
|
1490
|
+
const r = ctx.allocReg();
|
|
1491
|
+
this.emit(bc, [this.OP.LOAD_CONST, r, b.constantOperand(undefined)], node);
|
|
1492
|
+
return r;
|
|
1470
1493
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1494
|
+
return this._compileExpr(el, scope, bc);
|
|
1495
|
+
});
|
|
1496
|
+
const dst = ctx.allocReg();
|
|
1497
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, dst, n.elements.length, ...elemRegs], node);
|
|
1498
|
+
return dst;
|
|
1474
1499
|
}
|
|
1475
1500
|
case "ObjectExpression":
|
|
1476
1501
|
{
|
|
1477
|
-
|
|
1502
|
+
const n = node;
|
|
1478
1503
|
const regularProps = [];
|
|
1479
1504
|
const accessorProps = [];
|
|
1480
|
-
for (const prop of
|
|
1481
|
-
if (prop.type === "SpreadElement")
|
|
1482
|
-
throw new Error("Object spread not supported");
|
|
1483
|
-
}
|
|
1505
|
+
for (const prop of n.properties) {
|
|
1506
|
+
if (prop.type === "SpreadElement") throw new Error("Object spread not supported");
|
|
1484
1507
|
if (prop.type === "ObjectMethod") {
|
|
1485
1508
|
if (prop.kind === "get" || prop.kind === "set") {
|
|
1486
|
-
if (prop.computed)
|
|
1487
|
-
throw new Error("Computed getter/setter keys are not supported");
|
|
1488
|
-
}
|
|
1509
|
+
if (prop.computed) throw new Error("Computed getter/setter keys are not supported");
|
|
1489
1510
|
accessorProps.push(prop);
|
|
1490
1511
|
} else {
|
|
1491
|
-
throw new Error(
|
|
1512
|
+
throw new Error("Shorthand method syntax is not supported");
|
|
1492
1513
|
}
|
|
1493
1514
|
} else {
|
|
1494
1515
|
regularProps.push(prop);
|
|
1495
1516
|
}
|
|
1496
1517
|
}
|
|
1497
1518
|
|
|
1498
|
-
// Build
|
|
1519
|
+
// Build flat [key, val, key, val, …] register list.
|
|
1520
|
+
const pairRegs = [];
|
|
1499
1521
|
for (const prop of regularProps) {
|
|
1500
|
-
const key = prop.key;
|
|
1501
1522
|
let keyStr;
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
|
|
1510
|
-
this._compileExpr(prop.value, scope, bc);
|
|
1523
|
+
const key = prop.key;
|
|
1524
|
+
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}`);
|
|
1525
|
+
const keyReg = ctx.allocReg();
|
|
1526
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
|
|
1527
|
+
const valReg = this._compileExpr(prop.value, scope, bc);
|
|
1528
|
+
pairRegs.push(keyReg, valReg);
|
|
1511
1529
|
}
|
|
1512
|
-
|
|
1530
|
+
const dst = ctx.allocReg();
|
|
1531
|
+
this.emit(bc, [this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs], node);
|
|
1513
1532
|
|
|
1514
|
-
// Define
|
|
1515
|
-
// Stack after BUILD_OBJECT: [..., obj]
|
|
1516
|
-
// For each accessor: DUP obj, push key, compile fn, DEFINE_GETTER/DEFINE_SETTER
|
|
1517
|
-
// DEFINE_GETTER/DEFINE_SETTER pops fn+key+obj, leaving the original obj.
|
|
1533
|
+
// Define accessors on the object now sitting in `dst`.
|
|
1518
1534
|
for (const prop of accessorProps) {
|
|
1519
1535
|
const key = prop.key;
|
|
1520
1536
|
let keyStr;
|
|
1521
|
-
if (key.type === "Identifier") {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1527
|
-
}
|
|
1528
|
-
this.emit(bc, [this.OP.DUP], node); // dup so the original obj stays after the define
|
|
1529
|
-
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
|
|
1530
|
-
|
|
1531
|
-
// Compile the accessor body as an anonymous function descriptor.
|
|
1532
|
-
const desc = this._compileFunctionDecl(prop);
|
|
1533
|
-
this._emitMakeClosure(desc, prop, bc);
|
|
1534
|
-
this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER], node);
|
|
1537
|
+
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}`);
|
|
1538
|
+
const keyReg = ctx.allocReg();
|
|
1539
|
+
this.emit(bc, [this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)], node);
|
|
1540
|
+
const fnReg = this._emitMakeClosure(this._compileFunctionDecl(prop), prop, bc);
|
|
1541
|
+
this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER, dst, keyReg, fnReg], node);
|
|
1535
1542
|
}
|
|
1536
|
-
|
|
1543
|
+
return dst;
|
|
1537
1544
|
}
|
|
1538
1545
|
default:
|
|
1539
1546
|
{
|
|
@@ -1543,10 +1550,7 @@ export class Compiler {
|
|
|
1543
1550
|
}
|
|
1544
1551
|
}
|
|
1545
1552
|
|
|
1546
|
-
// Serializer
|
|
1547
|
-
// Turns the compiled output into a commented JS source string.
|
|
1548
|
-
// Expects fully-resolved bytecode (all label refs and constant refs already
|
|
1549
|
-
// converted to plain integers by resolveLabels + resolveConstants passes).
|
|
1553
|
+
// ── Serializer ────────────────────────────────────────────────────────────────
|
|
1550
1554
|
class Serializer {
|
|
1551
1555
|
constructor(compiler) {
|
|
1552
1556
|
this.compiler = compiler;
|
|
@@ -1563,18 +1567,27 @@ class Serializer {
|
|
|
1563
1567
|
get JUMP_OPS() {
|
|
1564
1568
|
return this.compiler.JUMP_OPS;
|
|
1565
1569
|
}
|
|
1566
|
-
|
|
1567
|
-
// Produce a JS literal for a constant pool entry
|
|
1568
1570
|
_serializeConst(val) {
|
|
1569
1571
|
if (val === null) return "null";
|
|
1570
1572
|
if (val === undefined) return "undefined";
|
|
1571
|
-
return JSON.stringify(val);
|
|
1573
|
+
return JSON.stringify(val);
|
|
1572
1574
|
}
|
|
1573
1575
|
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
|
|
1576
|
+
// Reverse the concealment applied by resolveConstants so disassembly comments
|
|
1577
|
+
// always show the plaintext value regardless of the concealConstants option.
|
|
1578
|
+
_decryptConst(constants, idx, key) {
|
|
1579
|
+
const v = constants[idx];
|
|
1580
|
+
if (!key) return v;
|
|
1581
|
+
if (typeof v === "number") return v ^ key;
|
|
1582
|
+
// String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
|
|
1583
|
+
const bytes = Buffer.from(v, "base64");
|
|
1584
|
+
let out = "";
|
|
1585
|
+
for (let i = 0; i < bytes.length / 2; i++) {
|
|
1586
|
+
const code = bytes[i * 2] | bytes[i * 2 + 1] << 8;
|
|
1587
|
+
out += String.fromCharCode(code ^ key + i & 0xffff);
|
|
1588
|
+
}
|
|
1589
|
+
return out;
|
|
1590
|
+
}
|
|
1578
1591
|
_serializeInstr(instr, constants) {
|
|
1579
1592
|
const op = instr[0];
|
|
1580
1593
|
const operands = instr.slice(1);
|
|
@@ -1584,66 +1597,75 @@ class Serializer {
|
|
|
1584
1597
|
ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
|
|
1585
1598
|
}
|
|
1586
1599
|
ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
|
|
1587
|
-
const operand = resolvedOperands[0]; // first operand for single-operand comment cases
|
|
1588
1600
|
const name = this.OP_NAME[op] || `OP_${op}`;
|
|
1589
1601
|
let comment = name;
|
|
1602
|
+
function formatLoc(loc) {
|
|
1603
|
+
return loc ? `${loc.line}:${loc.column}` : "";
|
|
1604
|
+
}
|
|
1590
1605
|
const sourceNode = instr[SOURCE_NODE_SYM];
|
|
1591
|
-
const sourceLocation = sourceNode ? sourceNode.loc.start
|
|
1592
|
-
|
|
1593
|
-
// Annotate with human-readable operand meaning
|
|
1606
|
+
const sourceLocation = sourceNode?.loc ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)].filter(Boolean).join("-") : "";
|
|
1594
1607
|
if (resolvedOperands.length > 0) {
|
|
1608
|
+
// Operand[0] is always `dst` for instruction types that produce a value.
|
|
1609
|
+
const dst = resolvedOperands[0];
|
|
1595
1610
|
switch (op) {
|
|
1596
1611
|
case this.OP.LOAD_CONST:
|
|
1597
1612
|
{
|
|
1598
|
-
|
|
1599
|
-
|
|
1613
|
+
// resolvedOperands: [dst, constIdx, concealKey]
|
|
1614
|
+
const val = this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2]);
|
|
1615
|
+
comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
|
|
1600
1616
|
break;
|
|
1601
1617
|
}
|
|
1602
|
-
case this.OP.
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
comment += ` slot[${operand}]`;
|
|
1618
|
+
case this.OP.LOAD_GLOBAL:
|
|
1619
|
+
// resolvedOperands: [dst, constIdx, concealKey]
|
|
1620
|
+
comment += ` reg[${dst}] = ${this._decryptConst(constants, resolvedOperands[1], resolvedOperands[2])}`;
|
|
1621
|
+
break;
|
|
1622
|
+
case this.OP.STORE_GLOBAL:
|
|
1623
|
+
// resolvedOperands: [constIdx, concealKey, srcReg]
|
|
1624
|
+
comment += ` ${this._decryptConst(constants, resolvedOperands[0], resolvedOperands[1])} = reg[${resolvedOperands[2]}]`;
|
|
1610
1625
|
break;
|
|
1611
1626
|
case this.OP.LOAD_UPVALUE:
|
|
1627
|
+
comment += ` reg[${dst}] = upvalue[${resolvedOperands[1]}]`;
|
|
1628
|
+
break;
|
|
1612
1629
|
case this.OP.STORE_UPVALUE:
|
|
1613
|
-
comment += ` upvalue[${
|
|
1630
|
+
comment += ` upvalue[${resolvedOperands[0]}] = reg[${resolvedOperands[1]}]`;
|
|
1614
1631
|
break;
|
|
1615
|
-
case this.OP.
|
|
1616
|
-
|
|
1617
|
-
|
|
1632
|
+
case this.OP.MOVE:
|
|
1633
|
+
comment += ` reg[${dst}] = reg[${resolvedOperands[1]}]`;
|
|
1634
|
+
break;
|
|
1635
|
+
case this.OP.MAKE_CLOSURE:
|
|
1636
|
+
comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
|
|
1618
1637
|
break;
|
|
1619
1638
|
case this.OP.CALL:
|
|
1639
|
+
comment += ` reg[${dst}] = call(reg[${resolvedOperands[1]}], ${resolvedOperands[2]} args)`;
|
|
1640
|
+
break;
|
|
1620
1641
|
case this.OP.CALL_METHOD:
|
|
1621
|
-
comment += ` (${
|
|
1642
|
+
comment += ` reg[${dst}] = method(recv=reg[${resolvedOperands[1]}], fn=reg[${resolvedOperands[2]}], ${resolvedOperands[3]} args)`;
|
|
1643
|
+
break;
|
|
1644
|
+
case this.OP.NEW:
|
|
1645
|
+
comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
|
|
1646
|
+
break;
|
|
1647
|
+
case this.OP.RETURN:
|
|
1648
|
+
comment += ` reg[${resolvedOperands[0]}]`;
|
|
1622
1649
|
break;
|
|
1623
1650
|
case this.OP.BUILD_ARRAY:
|
|
1624
|
-
comment += `
|
|
1651
|
+
comment += ` reg[${dst}] = [${resolvedOperands[2]} elems]`;
|
|
1625
1652
|
break;
|
|
1626
1653
|
case this.OP.BUILD_OBJECT:
|
|
1627
|
-
comment += `
|
|
1628
|
-
break;
|
|
1629
|
-
case this.OP.NEW:
|
|
1630
|
-
comment += ` (${operand} args)`;
|
|
1654
|
+
comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
|
|
1631
1655
|
break;
|
|
1632
1656
|
default:
|
|
1633
|
-
comment += resolvedOperands.length === 1 ? ` ${
|
|
1657
|
+
comment += resolvedOperands.length === 1 ? ` ${resolvedOperands[0]}` : ` [${resolvedOperands.join(", ")}]`;
|
|
1634
1658
|
}
|
|
1635
1659
|
}
|
|
1636
|
-
comment = comment.padEnd(
|
|
1660
|
+
comment = comment.padEnd(50) + sourceLocation;
|
|
1637
1661
|
const values = [op, ...resolvedOperands];
|
|
1638
1662
|
const instrText = `[${values.join(", ")}]`;
|
|
1639
|
-
const text = `${(instrText + ",").padEnd(
|
|
1663
|
+
const text = `${(instrText + ",").padEnd(20)} ${comment}`;
|
|
1640
1664
|
return {
|
|
1641
1665
|
text,
|
|
1642
1666
|
values
|
|
1643
1667
|
};
|
|
1644
1668
|
}
|
|
1645
|
-
|
|
1646
|
-
// Serialize the CONSTANTS array
|
|
1647
1669
|
_serializeConstants(constants) {
|
|
1648
1670
|
const lines = ["var CONSTANTS = ["];
|
|
1649
1671
|
constants.forEach((val, idx) => {
|
|
@@ -1652,19 +1674,17 @@ class Serializer {
|
|
|
1652
1674
|
lines.push("];");
|
|
1653
1675
|
return lines.join("\n");
|
|
1654
1676
|
}
|
|
1655
|
-
|
|
1656
|
-
// Filter out any remaining null-opcode pseudo-instructions.
|
|
1657
|
-
// (defineLabel pseudo-ops are already stripped by resolveLabels.)
|
|
1658
1677
|
_serializeBytecode(bytecode, compiler) {
|
|
1659
1678
|
const serialized = [];
|
|
1660
1679
|
for (const instr of bytecode) {
|
|
1661
1680
|
if (instr[0] === null) continue;
|
|
1662
1681
|
const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
|
|
1663
1682
|
if (specializedOpInfo) {
|
|
1664
|
-
const
|
|
1683
|
+
const operands = instr.slice(1);
|
|
1684
|
+
const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
|
|
1665
1685
|
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
1666
|
-
compiler.OP_NAME[instr[0]] = `${originalName}_${
|
|
1667
|
-
specializedOpInfo.
|
|
1686
|
+
compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValues.join("_")}`;
|
|
1687
|
+
specializedOpInfo.resolvedOperands = operands;
|
|
1668
1688
|
}
|
|
1669
1689
|
serialized.push(instr);
|
|
1670
1690
|
}
|
|
@@ -1673,7 +1693,6 @@ class Serializer {
|
|
|
1673
1693
|
};
|
|
1674
1694
|
}
|
|
1675
1695
|
_encodeBytecode(flat) {
|
|
1676
|
-
// Encode as little-endian Uint16Array -> base64.
|
|
1677
1696
|
const buf = new Uint8Array(flat.length * 2);
|
|
1678
1697
|
flat.forEach((w, i) => {
|
|
1679
1698
|
buf[i * 2] = w & 0xff;
|
|
@@ -1683,6 +1702,7 @@ class Serializer {
|
|
|
1683
1702
|
}
|
|
1684
1703
|
serialize(bytecode, constants, compiler) {
|
|
1685
1704
|
const mainStartPc = compiler.mainStartPc;
|
|
1705
|
+
const mainRegCount = compiler.mainRegCount;
|
|
1686
1706
|
let sections = [];
|
|
1687
1707
|
var textForm = [];
|
|
1688
1708
|
var initBody = [];
|
|
@@ -1700,24 +1720,16 @@ class Serializer {
|
|
|
1700
1720
|
if (this.options.encodeBytecode) {
|
|
1701
1721
|
sections.push(`var BYTECODE = "${this._encodeBytecode(flat)}";`);
|
|
1702
1722
|
} else {
|
|
1703
|
-
// Flatten each [op, ...operands] instruction into individual u16 slots.
|
|
1704
|
-
|
|
1705
1723
|
sections.push(`var BYTECODE = [${flat.join(",")}]`);
|
|
1706
1724
|
}
|
|
1707
|
-
|
|
1708
|
-
// MAIN_START_PC
|
|
1709
1725
|
sections.push(`var MAIN_START_PC = ${mainStartPc};`);
|
|
1726
|
+
sections.push(`var MAIN_REG_COUNT = ${mainRegCount};`);
|
|
1710
1727
|
sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
|
|
1711
1728
|
sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
|
|
1712
|
-
// Opcodes
|
|
1713
1729
|
const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
|
|
1714
1730
|
sections.push(`var OP = ${generate(object).code};`);
|
|
1715
|
-
|
|
1716
|
-
// Constants must be defined before the bytecode
|
|
1717
1731
|
initBody.push(this._serializeConstants(constants));
|
|
1718
1732
|
sections = [...initBody, ...sections];
|
|
1719
|
-
|
|
1720
|
-
// VM runtime
|
|
1721
1733
|
sections.push(VM_RUNTIME);
|
|
1722
1734
|
return sections.join("\n\n");
|
|
1723
1735
|
}
|
|
@@ -1725,12 +1737,8 @@ class Serializer {
|
|
|
1725
1737
|
export async function compileAndSerialize(sourceCode, options) {
|
|
1726
1738
|
const compiler = new Compiler(options);
|
|
1727
1739
|
let bytecode = compiler.compile(sourceCode);
|
|
1728
|
-
|
|
1729
|
-
// User transform passes (operate on unresolved IR with label/constant refs)
|
|
1730
|
-
// macroOpcodes must run after selfModifying (so PATCH-stub bodies are in place)
|
|
1731
1740
|
const passes = [];
|
|
1732
|
-
|
|
1733
|
-
// Due to current implementation, specialized must run BEFORE macroOpcodes
|
|
1741
|
+
passes.push(concealConstants);
|
|
1734
1742
|
if (options.specializedOpcodes) {
|
|
1735
1743
|
passes.push(specializedOpcodes);
|
|
1736
1744
|
}
|
|
@@ -1740,22 +1748,30 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1740
1748
|
if (options.selfModifying) {
|
|
1741
1749
|
passes.push(selfModifying);
|
|
1742
1750
|
}
|
|
1751
|
+
if (options.aliasedOpcodes) {
|
|
1752
|
+
passes.push(aliasedOpcodes);
|
|
1753
|
+
}
|
|
1743
1754
|
for (const pass of passes) {
|
|
1744
1755
|
const passResult = pass(bytecode, compiler);
|
|
1745
1756
|
bytecode = passResult.bytecode;
|
|
1746
1757
|
}
|
|
1747
1758
|
|
|
1748
|
-
//
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1759
|
+
// Resolve label references to flat bytecode indices.
|
|
1760
|
+
const labelsResult = resolveLabels(bytecode, compiler);
|
|
1761
|
+
bytecode = labelsResult.bytecode;
|
|
1762
|
+
|
|
1763
|
+
// Set mainStartPc from the first function descriptor (or 0 for top-level start).
|
|
1764
|
+
compiler.mainStartPc = compiler.mainFn.startPc;
|
|
1765
|
+
|
|
1766
|
+
// Resolve constant references to pool indices (+ conceal key operand).
|
|
1767
|
+
const constResult = resolveConstants(bytecode, compiler);
|
|
1768
|
+
bytecode = constResult.bytecode;
|
|
1769
|
+
compiler.constants = constResult.constants;
|
|
1770
|
+
|
|
1771
|
+
// Build and obfuscate the runtime.
|
|
1772
|
+
const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
|
|
1773
|
+
const code = await obfuscateRuntime(runtimeSource, bytecode, options, compiler);
|
|
1758
1774
|
return {
|
|
1759
|
-
code
|
|
1775
|
+
code
|
|
1760
1776
|
};
|
|
1761
1777
|
}
|