"use strict";
/**
 * Bytecode compiler: lowers Nexus AST to instructions for the TS VM.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.BytecodeCompiler = void 0;
exports.compileProgram = compileProgram;
const values_1 = require("../language/values");
const builtins_1 = require("../language/builtins");
const code_1 = require("./code");
const symbols_1 = require("./symbols");
class BytecodeCompiler {
    constants = [];
    symbolTable;
    scopes = [
        {
            instructions: [],
            lastInstruction: { opcode: code_1.Opcode.OpPop, position: -1 },
            previousInstruction: { opcode: code_1.Opcode.OpPop, position: -1 },
        },
    ];
    scopeIndex = 0;
    constructor(symbolTable, constants) {
        this.symbolTable = symbolTable ?? new symbols_1.SymbolTable();
        if (!symbolTable) {
            for (let i = 0; i < builtins_1.BUILTIN_NAMES.length; i++) {
                this.symbolTable.defineBuiltin(i, builtins_1.BUILTIN_NAMES[i]);
            }
        }
        if (constants) {
            this.constants = constants;
        }
    }
    bytecode() {
        return {
            instructions: Uint8Array.from(this.currentInstructions()),
            constants: this.constants,
        };
    }
    compile(node) {
        switch (node.type) {
            case "Program":
                for (const s of node.statements) {
                    const err = this.compile(s);
                    if (err) {
                        return err;
                    }
                }
                return null;
            case "ExpressionStatement": {
                const es = node;
                if (es.expression) {
                    const err = this.compile(es.expression);
                    if (err) {
                        return err;
                    }
                    this.emit(code_1.Opcode.OpPop);
                }
                return null;
            }
            case "BlockStatement":
                for (const s of node.statements) {
                    const err = this.compile(s);
                    if (err) {
                        return err;
                    }
                }
                return null;
            case "LetStatement": {
                const ls = node;
                const symbol = this.symbolTable.define(ls.name.value);
                if (ls.value?.type === "FunctionLiteral") {
                    const err = this.compileFunction(ls.value, ls.name.value);
                    if (err) {
                        return err;
                    }
                }
                else if (ls.value) {
                    const err = this.compile(ls.value);
                    if (err) {
                        return err;
                    }
                }
                else {
                    this.emit(code_1.Opcode.OpNull);
                }
                if (symbol.scope === "GLOBAL") {
                    this.emit(code_1.Opcode.OpSetGlobal, symbol.index);
                }
                else {
                    this.emit(code_1.Opcode.OpSetLocal, symbol.index);
                }
                return null;
            }
            case "ReturnStatement": {
                const rs = node;
                if (rs.returnValue) {
                    const err = this.compile(rs.returnValue);
                    if (err) {
                        return err;
                    }
                    this.emit(code_1.Opcode.OpReturnValue);
                }
                else {
                    this.emit(code_1.Opcode.OpReturn);
                }
                return null;
            }
            case "WhileStatement":
                return this.compileWhile(node);
            case "ImportStatement":
                return "import is not supported by the bytecode VM; use the tree-walk engine (nexus.executionEngine=eval)";
            case "IntegerLiteral":
                this.emit(code_1.Opcode.OpConstant, this.addConstant(new values_1.IntegerObj(node.value)));
                return null;
            case "StringLiteral":
                this.emit(code_1.Opcode.OpConstant, this.addConstant(new values_1.StringObj(node.value)));
                return null;
            case "BooleanLiteral":
                this.emit(node.value ? code_1.Opcode.OpTrue : code_1.Opcode.OpFalse);
                return null;
            case "NullLiteral":
                this.emit(code_1.Opcode.OpNull);
                return null;
            case "PrefixExpression": {
                const p = node;
                if (!p.right) {
                    return "prefix missing operand";
                }
                const err = this.compile(p.right);
                if (err) {
                    return err;
                }
                if (p.operator === "!") {
                    this.emit(code_1.Opcode.OpBang);
                }
                else if (p.operator === "-") {
                    this.emit(code_1.Opcode.OpMinus);
                }
                else {
                    return `unknown prefix operator ${p.operator}`;
                }
                return null;
            }
            case "InfixExpression":
                return this.compileInfix(node);
            case "IfExpression":
                return this.compileIf(node);
            case "Identifier":
                return this.compileIdentifier(node);
            case "AssignExpression":
                return this.compileAssign(node);
            case "FunctionLiteral":
                return this.compileFunction(node, "");
            case "CallExpression": {
                const call = node;
                let err = this.compile(call.function);
                if (err) {
                    return err;
                }
                for (const a of call.arguments) {
                    err = this.compile(a);
                    if (err) {
                        return err;
                    }
                }
                this.emit(code_1.Opcode.OpCall, call.arguments.length);
                return null;
            }
            case "ArrayLiteral": {
                const arr = node;
                for (const el of arr.elements) {
                    const err = this.compile(el);
                    if (err) {
                        return err;
                    }
                }
                this.emit(code_1.Opcode.OpArray, arr.elements.length);
                return null;
            }
            case "HashLiteral": {
                const hash = node;
                for (const { key, value } of hash.pairs) {
                    let err = this.compile(key);
                    if (err) {
                        return err;
                    }
                    err = this.compile(value);
                    if (err) {
                        return err;
                    }
                }
                this.emit(code_1.Opcode.OpHash, hash.pairs.length * 2);
                return null;
            }
            case "IndexExpression": {
                const ix = node;
                let err = this.compile(ix.left);
                if (err) {
                    return err;
                }
                if (!ix.index) {
                    return "index missing";
                }
                err = this.compile(ix.index);
                if (err) {
                    return err;
                }
                this.emit(code_1.Opcode.OpIndex);
                return null;
            }
            case "MemberExpression": {
                const m = node;
                const err = this.compile(m.object);
                if (err) {
                    return err;
                }
                const fieldIdx = this.addConstant(new values_1.StringObj(m.property.value));
                this.emit(code_1.Opcode.OpMember, fieldIdx);
                return null;
            }
            case "EnumDeclaration":
            case "StructDeclaration":
            case "EffectDeclaration":
            case "MacroDefinition":
            case "ExternDeclaration":
                return null;
            default:
                return `compilation not supported for ${node.type}`;
        }
    }
    compileInfix(node) {
        if (node.operator === "<") {
            if (!node.right) {
                return "missing rhs";
            }
            let err = this.compile(node.right);
            if (err) {
                return err;
            }
            err = this.compile(node.left);
            if (err) {
                return err;
            }
            this.emit(code_1.Opcode.OpGreaterThan);
            return null;
        }
        if (node.operator === "<=") {
            if (!node.right) {
                return "missing rhs";
            }
            let err = this.compile(node.right);
            if (err) {
                return err;
            }
            err = this.compile(node.left);
            if (err) {
                return err;
            }
            this.emit(code_1.Opcode.OpGreaterEqual);
            return null;
        }
        let err = this.compile(node.left);
        if (err) {
            return err;
        }
        if (!node.right) {
            return "missing rhs";
        }
        err = this.compile(node.right);
        if (err) {
            return err;
        }
        switch (node.operator) {
            case "+":
                this.emit(code_1.Opcode.OpAdd);
                break;
            case "-":
                this.emit(code_1.Opcode.OpSub);
                break;
            case "*":
                this.emit(code_1.Opcode.OpMul);
                break;
            case "/":
                this.emit(code_1.Opcode.OpDiv);
                break;
            case "%":
                this.emit(code_1.Opcode.OpMod);
                break;
            case "==":
                this.emit(code_1.Opcode.OpEqual);
                break;
            case "!=":
                this.emit(code_1.Opcode.OpNotEqual);
                break;
            case ">":
                this.emit(code_1.Opcode.OpGreaterThan);
                break;
            case ">=":
                this.emit(code_1.Opcode.OpGreaterEqual);
                break;
            default:
                return `unknown operator ${node.operator}`;
        }
        return null;
    }
    compileIf(node) {
        if (!node.condition) {
            return "if missing condition";
        }
        let err = this.compile(node.condition);
        if (err) {
            return err;
        }
        const jumpNotTruthyPos = this.emit(code_1.Opcode.OpJumpNotTruthy, 9999);
        err = this.compile(node.consequence);
        if (err) {
            return err;
        }
        if (this.lastInstructionIs(code_1.Opcode.OpPop)) {
            this.removeLastPop();
        }
        const jumpPos = this.emit(code_1.Opcode.OpJump, 9999);
        this.changeOperand(jumpNotTruthyPos, this.currentInstructions().length);
        if (node.alternative) {
            err = this.compile(node.alternative);
            if (err) {
                return err;
            }
            if (this.lastInstructionIs(code_1.Opcode.OpPop)) {
                this.removeLastPop();
            }
        }
        else {
            this.emit(code_1.Opcode.OpNull);
        }
        this.changeOperand(jumpPos, this.currentInstructions().length);
        return null;
    }
    compileWhile(node) {
        const loopStart = this.currentInstructions().length;
        if (!node.condition) {
            return "while missing condition";
        }
        let err = this.compile(node.condition);
        if (err) {
            return err;
        }
        const jumpNotTruthyPos = this.emit(code_1.Opcode.OpJumpNotTruthy, 9999);
        err = this.compile(node.body);
        if (err) {
            return err;
        }
        if (this.lastInstructionIs(code_1.Opcode.OpPop)) {
            this.removeLastPop();
        }
        this.emit(code_1.Opcode.OpJump, loopStart);
        this.changeOperand(jumpNotTruthyPos, this.currentInstructions().length);
        this.emit(code_1.Opcode.OpNull);
        return null;
    }
    compileIdentifier(node) {
        const symbol = this.symbolTable.resolve(node.value);
        if (!symbol) {
            return `undefined variable ${node.value}`;
        }
        return this.loadSymbol(symbol);
    }
    loadSymbol(symbol) {
        switch (symbol.scope) {
            case "GLOBAL":
                this.emit(code_1.Opcode.OpGetGlobal, symbol.index);
                break;
            case "LOCAL":
                this.emit(code_1.Opcode.OpGetLocal, symbol.index);
                break;
            case "BUILTIN":
                this.emit(code_1.Opcode.OpGetBuiltin, symbol.index);
                break;
            case "FREE":
                this.emit(code_1.Opcode.OpGetFree, symbol.index);
                break;
            case "FUNCTION":
                this.emit(code_1.Opcode.OpCurrentClosure);
                break;
            default:
                return `unknown symbol scope ${symbol.scope}`;
        }
        return null;
    }
    compileAssign(node) {
        if (node.name.type === "Identifier") {
            const name = node.name.value;
            const symbol = this.symbolTable.resolve(name);
            if (!symbol) {
                return `undefined variable ${name}`;
            }
            if (!node.value) {
                return "assign missing value";
            }
            const err = this.compile(node.value);
            if (err) {
                return err;
            }
            if (symbol.scope === "GLOBAL") {
                this.emit(code_1.Opcode.OpSetGlobal, symbol.index);
                this.emit(code_1.Opcode.OpGetGlobal, symbol.index);
            }
            else if (symbol.scope === "LOCAL") {
                this.emit(code_1.Opcode.OpSetLocal, symbol.index);
                this.emit(code_1.Opcode.OpGetLocal, symbol.index);
            }
            else {
                return `cannot assign to ${symbol.scope} binding`;
            }
            return null;
        }
        if (node.name.type === "IndexExpression") {
            const ix = node.name;
            let err = this.compile(ix.left);
            if (err) {
                return err;
            }
            if (!ix.index) {
                return "index missing";
            }
            err = this.compile(ix.index);
            if (err) {
                return err;
            }
            if (!node.value) {
                return "assign missing value";
            }
            err = this.compile(node.value);
            if (err) {
                return err;
            }
            this.emit(code_1.Opcode.OpSetIndex);
            return null;
        }
        return "invalid assignment target";
    }
    compileFunction(node, name) {
        this.enterScope();
        if (name) {
            this.symbolTable.defineFunctionName(name);
        }
        for (const p of node.parameters) {
            this.symbolTable.define(p.value);
        }
        const err = this.compile(node.body);
        if (err) {
            return err;
        }
        if (this.lastInstructionIs(code_1.Opcode.OpPop)) {
            this.replaceLastPopWithReturn();
        }
        if (!this.lastInstructionIs(code_1.Opcode.OpReturnValue) &&
            !this.lastInstructionIs(code_1.Opcode.OpReturn)) {
            this.emit(code_1.Opcode.OpReturn);
        }
        const freeSymbols = [...this.symbolTable.freeSymbols];
        const numLocals = this.symbolTable.numDefinitions;
        const instructions = this.leaveScope();
        for (const s of freeSymbols) {
            const loadErr = this.loadSymbol(s);
            if (loadErr) {
                return loadErr;
            }
        }
        const compiled = new values_1.CompiledFunctionObj(Uint8Array.from(instructions), numLocals, node.parameters.length);
        const fnIndex = this.addConstant(compiled);
        this.emit(code_1.Opcode.OpClosure, fnIndex, freeSymbols.length);
        return null;
    }
    addConstant(obj) {
        this.constants.push(obj);
        return this.constants.length - 1;
    }
    emit(op, ...operands) {
        const ins = (0, code_1.make)(op, ...operands);
        const pos = this.addInstruction(ins);
        this.setLastInstruction(op, pos);
        return pos;
    }
    addInstruction(ins) {
        const pos = this.currentInstructions().length;
        this.scopes[this.scopeIndex].instructions.push(...ins);
        return pos;
    }
    setLastInstruction(op, pos) {
        const prev = this.scopes[this.scopeIndex].lastInstruction;
        this.scopes[this.scopeIndex].previousInstruction = prev;
        this.scopes[this.scopeIndex].lastInstruction = { opcode: op, position: pos };
    }
    lastInstructionIs(op) {
        if (this.currentInstructions().length === 0) {
            return false;
        }
        return this.scopes[this.scopeIndex].lastInstruction.opcode === op;
    }
    removeLastPop() {
        const last = this.scopes[this.scopeIndex].lastInstruction;
        const prev = this.scopes[this.scopeIndex].previousInstruction;
        this.scopes[this.scopeIndex].instructions =
            this.currentInstructions().slice(0, last.position);
        this.scopes[this.scopeIndex].lastInstruction = prev;
    }
    replaceLastPopWithReturn() {
        const lastPos = this.scopes[this.scopeIndex].lastInstruction.position;
        this.replaceInstruction(lastPos, (0, code_1.make)(code_1.Opcode.OpReturnValue));
        this.scopes[this.scopeIndex].lastInstruction.opcode =
            code_1.Opcode.OpReturnValue;
    }
    replaceInstruction(pos, newInstruction) {
        const ins = this.scopes[this.scopeIndex].instructions;
        for (let i = 0; i < newInstruction.length; i++) {
            ins[pos + i] = newInstruction[i];
        }
    }
    changeOperand(opPos, operand) {
        const op = this.currentInstructions()[opPos];
        const newInstruction = (0, code_1.make)(op, operand);
        this.replaceInstruction(opPos, newInstruction);
    }
    currentInstructions() {
        return this.scopes[this.scopeIndex].instructions;
    }
    enterScope() {
        this.scopes.push({
            instructions: [],
            lastInstruction: { opcode: code_1.Opcode.OpPop, position: -1 },
            previousInstruction: { opcode: code_1.Opcode.OpPop, position: -1 },
        });
        this.scopeIndex++;
        this.symbolTable = new symbols_1.SymbolTable(this.symbolTable);
    }
    leaveScope() {
        const instructions = this.currentInstructions();
        this.scopes.pop();
        this.scopeIndex--;
        this.symbolTable = this.symbolTable.outer;
        return instructions;
    }
}
exports.BytecodeCompiler = BytecodeCompiler;
function compileProgram(program) {
    const c = new BytecodeCompiler();
    const err = c.compile(program);
    if (err) {
        return { error: err };
    }
    return { bytecode: c.bytecode() };
}
//# sourceMappingURL=compiler.js.map