"use strict";
/**
 * Stack-based bytecode virtual machine for Nexus.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.VirtualMachine = void 0;
exports.runBytecode = runBytecode;
const builtins_1 = require("../language/builtins");
const values_1 = require("../language/values");
const code_1 = require("./code");
const STACK_SIZE = 2048;
const GLOBALS_SIZE = 65536;
const MAX_FRAMES = 1024;
class Frame {
    cl;
    basePointer;
    ip = -1;
    constructor(cl, basePointer) {
        this.cl = cl;
        this.basePointer = basePointer;
    }
    instructions() {
        return this.cl.fn.instructions;
    }
}
class VirtualMachine {
    constants;
    stack = new Array(STACK_SIZE);
    sp = 0;
    globals;
    frames = new Array(MAX_FRAMES);
    framesIndex = 1;
    builtins;
    lastPopped = values_1.NULL_OBJ;
    output = [];
    constructor(bytecode, globals) {
        const host = {
            writeOutput: (line) => this.output.push(line),
            applyFunction: (fn, args) => this.applyForBuiltin(fn, args),
        };
        this.builtins = (0, builtins_1.createBuiltins)(host);
        this.constants = bytecode.constants;
        this.globals = globals ?? new Array(GLOBALS_SIZE);
        const mainFn = new values_1.CompiledFunctionObj(bytecode.instructions, 0, 0);
        const mainClosure = new values_1.ClosureObj(mainFn);
        this.frames[0] = new Frame(mainClosure, 0);
    }
    lastPoppedStackElem() {
        return this.lastPopped;
    }
    run() {
        while (true) {
            const frame = this.currentFrame();
            if (frame.ip >= frame.instructions().length - 1) {
                break;
            }
            frame.ip++;
            const ip = frame.ip;
            const ins = frame.instructions();
            const op = ins[ip];
            switch (op) {
                case code_1.Opcode.OpConstant: {
                    const constIndex = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    const err = this.push(this.constants[constIndex]);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpPop:
                    this.lastPopped = this.pop();
                    break;
                case code_1.Opcode.OpAdd:
                case code_1.Opcode.OpSub:
                case code_1.Opcode.OpMul:
                case code_1.Opcode.OpDiv:
                case code_1.Opcode.OpMod: {
                    const err = this.executeBinary(op);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpTrue: {
                    const err = this.push(values_1.TRUE_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpFalse: {
                    const err = this.push(values_1.FALSE_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpNull: {
                    const err = this.push(values_1.NULL_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpEqual:
                case code_1.Opcode.OpNotEqual:
                case code_1.Opcode.OpGreaterThan:
                case code_1.Opcode.OpGreaterEqual:
                case code_1.Opcode.OpLessThan:
                case code_1.Opcode.OpLessEqual: {
                    const err = this.executeComparison(op);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpBang: {
                    const err = this.push((0, values_1.isTruthy)(this.pop()) ? values_1.FALSE_OBJ : values_1.TRUE_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpMinus: {
                    const operand = this.pop();
                    if (!(operand instanceof values_1.IntegerObj)) {
                        return `unsupported type for negation: ${operand.type}`;
                    }
                    const err = this.push(new values_1.IntegerObj(-operand.value));
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpJump: {
                    const pos = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip = pos - 1;
                    break;
                }
                case code_1.Opcode.OpJumpNotTruthy: {
                    const pos = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    const condition = this.pop();
                    if (!(0, values_1.isTruthy)(condition)) {
                        frame.ip = pos - 1;
                    }
                    break;
                }
                case code_1.Opcode.OpSetGlobal: {
                    const globalIndex = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    this.globals[globalIndex] = this.pop();
                    break;
                }
                case code_1.Opcode.OpGetGlobal: {
                    const globalIndex = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    const err = this.push(this.globals[globalIndex] ?? values_1.NULL_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpSetLocal: {
                    const localIndex = ins[ip + 1];
                    frame.ip++;
                    this.stack[frame.basePointer + localIndex] = this.pop();
                    break;
                }
                case code_1.Opcode.OpGetLocal: {
                    const localIndex = ins[ip + 1];
                    frame.ip++;
                    const err = this.push(this.stack[frame.basePointer + localIndex] ?? values_1.NULL_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpGetBuiltin: {
                    const builtinIndex = ins[ip + 1];
                    frame.ip++;
                    const name = builtins_1.BUILTIN_NAMES[builtinIndex];
                    const builtin = name ? this.builtins.get(name) : undefined;
                    if (!builtin) {
                        return `invalid builtin index ${builtinIndex}`;
                    }
                    const err = this.push(builtin);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpArray: {
                    const numElements = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    const array = this.buildArray(this.sp - numElements, this.sp);
                    this.sp -= numElements;
                    const err = this.push(array);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpHash: {
                    const numElements = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    const hash = this.buildHash(this.sp - numElements, this.sp);
                    this.sp -= numElements;
                    const err = this.push(hash);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpIndex: {
                    const index = this.pop();
                    const left = this.pop();
                    const err = this.executeIndex(left, index);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpMember: {
                    const fieldIndex = (0, code_1.readUint16)(ins, ip + 1);
                    frame.ip += 2;
                    const fieldObj = this.constants[fieldIndex];
                    if (!(fieldObj instanceof values_1.StringObj)) {
                        return "member field constant must be string";
                    }
                    const left = this.pop();
                    const err = this.executeMember(left, fieldObj.value);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpSetIndex: {
                    const val = this.pop();
                    const index = this.pop();
                    const left = this.pop();
                    const setErr = this.executeSetIndex(left, index, val);
                    if (setErr) {
                        return setErr;
                    }
                    const err = this.push(val);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpCall: {
                    const numArgs = ins[ip + 1];
                    frame.ip++;
                    const err = this.executeCall(numArgs);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpReturnValue: {
                    const returnValue = this.pop();
                    const popped = this.popFrame();
                    this.sp = popped.basePointer - 1;
                    const err = this.push(returnValue);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpReturn: {
                    const popped = this.popFrame();
                    this.sp = popped.basePointer - 1;
                    const err = this.push(values_1.NULL_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpClosure: {
                    const constIndex = (0, code_1.readUint16)(ins, ip + 1);
                    const numFree = ins[ip + 3];
                    frame.ip += 3;
                    const err = this.pushClosure(constIndex, numFree);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpGetFree: {
                    const freeIndex = ins[ip + 1];
                    frame.ip++;
                    const err = this.push(this.currentFrame().cl.free[freeIndex] ?? values_1.NULL_OBJ);
                    if (err) {
                        return err;
                    }
                    break;
                }
                case code_1.Opcode.OpCurrentClosure: {
                    const err = this.push(this.currentFrame().cl);
                    if (err) {
                        return err;
                    }
                    break;
                }
                default:
                    return `unknown opcode ${op}`;
            }
        }
        return null;
    }
    applyForBuiltin(fn, args) {
        if (fn instanceof builtins_1.BuiltinObj) {
            return fn.fn(...args);
        }
        if (fn instanceof values_1.ClosureObj) {
            // Run a nested mini-call via stack (limited: use recursive VM call pattern)
            return (0, values_1.newError)("vm map/filter of closures: use tree-walk engine");
        }
        return (0, values_1.newError)(`not a function: ${fn.type}`);
    }
    currentFrame() {
        return this.frames[this.framesIndex - 1];
    }
    pushFrame(f) {
        this.frames[this.framesIndex] = f;
        this.framesIndex++;
    }
    popFrame() {
        this.framesIndex--;
        return this.frames[this.framesIndex];
    }
    push(obj) {
        if (this.sp >= STACK_SIZE) {
            return "stack overflow";
        }
        this.stack[this.sp] = obj;
        this.sp++;
        return null;
    }
    pop() {
        this.sp--;
        const obj = this.stack[this.sp] ?? values_1.NULL_OBJ;
        this.lastPopped = obj;
        return obj;
    }
    pushClosure(constIndex, numFree) {
        const constant = this.constants[constIndex];
        if (!(constant instanceof values_1.CompiledFunctionObj)) {
            return `not a function: ${constant?.type}`;
        }
        const free = [];
        for (let i = 0; i < numFree; i++) {
            free.push(this.stack[this.sp - numFree + i] ?? values_1.NULL_OBJ);
        }
        this.sp -= numFree;
        return this.push(new values_1.ClosureObj(constant, free));
    }
    executeCall(numArgs) {
        const callee = this.stack[this.sp - 1 - numArgs];
        if (callee instanceof values_1.ClosureObj) {
            return this.callClosure(callee, numArgs);
        }
        if (callee instanceof builtins_1.BuiltinObj) {
            return this.callBuiltin(callee, numArgs);
        }
        return `calling non-function: ${callee?.type}`;
    }
    callClosure(cl, numArgs) {
        if (numArgs !== cl.fn.numParameters) {
            return `wrong number of arguments: want=${cl.fn.numParameters}, got=${numArgs}`;
        }
        const frame = new Frame(cl, this.sp - numArgs);
        this.pushFrame(frame);
        this.sp = frame.basePointer + cl.fn.numLocals;
        return null;
    }
    callBuiltin(builtin, numArgs) {
        const args = [];
        for (let i = 0; i < numArgs; i++) {
            args.push(this.stack[this.sp - numArgs + i] ?? values_1.NULL_OBJ);
        }
        const result = builtin.fn(...args);
        this.sp = this.sp - numArgs - 1;
        if (result instanceof values_1.ErrorObj) {
            return result.message;
        }
        return this.push(result);
    }
    buildArray(start, end) {
        const elements = [];
        for (let i = start; i < end; i++) {
            elements.push(this.stack[i] ?? values_1.NULL_OBJ);
        }
        return new values_1.ArrayObj(elements);
    }
    buildHash(start, end) {
        const hash = new values_1.HashObj();
        for (let i = start; i < end; i += 2) {
            hash.set(this.stack[i] ?? values_1.NULL_OBJ, this.stack[i + 1] ?? values_1.NULL_OBJ);
        }
        return hash;
    }
    executeIndex(left, index) {
        if (left instanceof values_1.ArrayObj && index instanceof values_1.IntegerObj) {
            const el = left.elements[index.value];
            return this.push(el ?? values_1.NULL_OBJ);
        }
        if (left instanceof values_1.HashObj) {
            return this.push(left.get(index));
        }
        if (left instanceof values_1.StringObj && index instanceof values_1.IntegerObj) {
            if (index.value < 0 || index.value >= left.value.length) {
                return this.push(values_1.NULL_OBJ);
            }
            return this.push(new values_1.StringObj(left.value[index.value]));
        }
        return `index operator not supported: ${left.type}`;
    }
    executeMember(left, field) {
        if (!(left instanceof values_1.HashObj)) {
            return `member access on non-hash: ${left.type}`;
        }
        return this.push(left.getString(field));
    }
    executeSetIndex(left, index, val) {
        if (left instanceof values_1.ArrayObj) {
            if (!(index instanceof values_1.IntegerObj)) {
                return "array index must be INTEGER";
            }
            if (index.value < 0 || index.value >= left.elements.length) {
                return "array index out of bounds";
            }
            left.elements[index.value] = val;
            return null;
        }
        if (left instanceof values_1.HashObj) {
            left.set(index, val);
            return null;
        }
        return `index assignment not supported on ${left.type}`;
    }
    executeBinary(op) {
        const right = this.pop();
        const left = this.pop();
        if (left instanceof values_1.IntegerObj && right instanceof values_1.IntegerObj) {
            return this.executeBinaryInteger(op, left, right);
        }
        if (left instanceof values_1.StringObj &&
            right instanceof values_1.StringObj &&
            op === code_1.Opcode.OpAdd) {
            return this.push(new values_1.StringObj(left.value + right.value));
        }
        return `unsupported types for binary operation: ${left.type} ${right.type}`;
    }
    executeBinaryInteger(op, left, right) {
        let result;
        switch (op) {
            case code_1.Opcode.OpAdd:
                result = left.value + right.value;
                break;
            case code_1.Opcode.OpSub:
                result = left.value - right.value;
                break;
            case code_1.Opcode.OpMul:
                result = left.value * right.value;
                break;
            case code_1.Opcode.OpDiv:
                if (right.value === 0) {
                    return "division by zero";
                }
                result = Math.trunc(left.value / right.value);
                break;
            case code_1.Opcode.OpMod:
                if (right.value === 0) {
                    return "modulo by zero";
                }
                result = left.value % right.value;
                break;
            default:
                return `unknown integer operator ${op}`;
        }
        return this.push(new values_1.IntegerObj(result));
    }
    executeComparison(op) {
        const right = this.pop();
        const left = this.pop();
        if (left instanceof values_1.IntegerObj && right instanceof values_1.IntegerObj) {
            switch (op) {
                case code_1.Opcode.OpEqual:
                    return this.push(nativeBool(left.value === right.value));
                case code_1.Opcode.OpNotEqual:
                    return this.push(nativeBool(left.value !== right.value));
                case code_1.Opcode.OpGreaterThan:
                    return this.push(nativeBool(left.value > right.value));
                case code_1.Opcode.OpGreaterEqual:
                    return this.push(nativeBool(left.value >= right.value));
                case code_1.Opcode.OpLessThan:
                    return this.push(nativeBool(left.value < right.value));
                case code_1.Opcode.OpLessEqual:
                    return this.push(nativeBool(left.value <= right.value));
            }
        }
        switch (op) {
            case code_1.Opcode.OpEqual:
                return this.push(nativeBool(left === right || left.inspect() === right.inspect()));
            case code_1.Opcode.OpNotEqual:
                return this.push(nativeBool(!(left === right || left.inspect() === right.inspect())));
            default:
                return `unsupported comparison on ${left.type}`;
        }
    }
}
exports.VirtualMachine = VirtualMachine;
function nativeBool(v) {
    return v ? values_1.TRUE_OBJ : values_1.FALSE_OBJ;
}
function runBytecode(bytecode, globals) {
    const vm = new VirtualMachine(bytecode, globals);
    const error = vm.run();
    if (error) {
        return { value: (0, values_1.newError)(error), output: vm.output, error };
    }
    return { value: vm.lastPoppedStackElem(), output: vm.output };
}
//# sourceMappingURL=vm.js.map