UNPKG

microvium

Version:

A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.

2,198 lines 100 kB
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileBinaryExpression = exports.compileUpdateExpression = exports.compileUnaryExpression = exports.getSlotAccessor = exports.getGlobalAccessor = exports.compileAssignmentExpression = exports.compileLogicalExpression = exports.compileCallExpression = exports.compileNewExpression = exports.compileMemberExpression = exports.compileObjectExpression = exports.compileArrayExpression = exports.compileConditionalExpression = exports.compileThisExpression = exports.compileFunctionExpression = exports.compileArrowFunctionExpression = exports.compileTemplateLiteral = exports.compileAwaitExpression = exports.compileExpression = exports.compileSwitchStatement = exports.compileBreakStatement = exports.compileTryStatement = exports.compileClassConstructor = exports.compileClassMethod = exports.getFieldKey = exports.compileClassDeclaration = exports.compileStatement = exports.nameOperand = exports.indexOperand = exports.flagOperand = exports.countOperand = exports.labelOperand = exports.literalOperand = exports.labelOfBlock = exports.compileIfStatement = exports.compileBlockStatement = exports.compileDoWhileStatement = exports.compileWhileStatement = exports.compileBlockEpilogue = exports.compileForStatement = exports.compileThrowStatement = exports.compileReturnStatement = exports.compileExpressionStatement = exports.compilePrologue = exports.compileFunction = exports.compileModuleVariableDeclaration = exports.compileExportNamedDeclaration = exports.compileModuleStatement = exports.parseToAst = exports.compileScript = void 0;
exports.computeMaximumStackDepth = exports.compileVariableDeclaration = exports.compilingEndOfNode = exports.compilingNode = exports.compileIdentifier = void 0;
const babylon = __importStar(require("@babel/parser"));
const B = __importStar(require("./supported-babel-types"));
const IL = __importStar(require("../il"));
const utils_1 = require("../utils");
const runtime_types_1 = require("../runtime-types");
const il_opcodes_1 = require("../il-opcodes");
const analyze_scopes_1 = require("./analyze-scopes");
const common_1 = require("./common");
const outputStackDepthComments = false;
// The labels pointing to each predeclared block
const predeclaredBlocks = new WeakMap();
// There are some syntactic representations that are compiled using a special meaning
const specialForms = new Set(['$$MicroviumNopInstruction']);
function moveCursor(cur, toLocation) {
    Object.assign(cur, toLocation);
}
/**
 * Compile the given source code.
 *
 * Note: if opts.awaitStackDepths is only used internally (this function
 * recursively calls itself).
 */
function compileScript(filename, scriptText, opts) {
    const file = parseToAst(filename, scriptText);
    const scopeAnalysis = (0, analyze_scopes_1.analyzeScopes)(file, filename, opts?.awaitStackDepths);
    const ctx = {
        filename,
        // Global counter for generating block IDs. I'm not sure why I made this
        // global, but I suspect one advantage is when looking at the IL, it's easy
        // to jump to a label
        nextBlockID: 1,
        scopeAnalysis: scopeAnalysis,
        awaitStackDepths: opts?.awaitStackDepths,
    };
    const unit = {
        sourceFilename: filename,
        functions: {},
        moduleVariables: scopeAnalysis.globalSlots.map(s => s.name),
        freeVariables: [...scopeAnalysis.freeVariables].filter(x => !specialForms.has(x)),
        entryFunctionID: undefined,
        moduleImports: [],
    };
    const cur = {
        ctx,
        filename,
        breakScope: undefined,
        scopeStack: undefined,
        stackDepth: 0,
        reachable: true,
        node: file,
        unit,
        // This is one of the few places where we're not actually in a function yet
        func: undefined,
        block: undefined,
    };
    // # Imports
    // Note that imports don't require any IL to be emitted (e.g. a `require`
    // call) since the imported modules are just loaded automatically at load
    // time.
    for (const statement of file.program.body) {
        if (statement.type === 'ImportDeclaration') {
            compilingNode(cur, statement);
            const source = statement.source.value;
            const info = scopeAnalysis.moduleImports.get(source);
            unit.moduleImports.push({
                variableName: info?.name,
                source: statement.source.value
            });
        }
    }
    // Entry function
    const entryFunc = compileEntryFunction(cur, file.program);
    unit.entryFunctionID = entryFunc.id;
    // If there was no stack depth information for the await points, we can now
    // calculate it by walking the emitted IL
    if (!opts?.awaitStackDepths) {
        const awaitPoints = findAwaitPoints(unit);
        // If there are no await points, then the resulting IL is synchronous and so
        // the fact that we didn't have await stack depths is not a problem.
        // Otherwise, we run the whole compiler again with the await stack depths.
        // It's kinda ridiculous to run the compiler twice on the same unit but it's
        // a quick and reliable way to get to MVP and we can optimize later.
        if (awaitPoints.length !== 0) {
            const awaitStackDepths = new Map();
            for (const awaitPoint of awaitPoints) {
                // Note: the location is not unique across multiple files, but we're
                // only using it as a key within a single file here. It's an awkward key
                // to use but it's the only thing in the output that currently
                // identifies which await point we're talking about.
                const loc = awaitPoint.sourceLoc ?? (0, utils_1.unexpected)();
                const locStr = `${loc.line}:${loc.column + 1}`;
                const stackDepth = awaitPoint.stackDepthBefore ?? (0, utils_1.unexpected)();
                awaitStackDepths.set(locStr, stackDepth);
            }
            return compileScript(filename, scriptText, { awaitStackDepths });
        }
    }
    return { unit, scopeAnalysis };
}
exports.compileScript = compileScript;
// Similar to compileFunction but deals with module-level statements
function compileEntryFunction(cur, program) {
    const ctx = cur.ctx;
    const funcInfo = ctx.scopeAnalysis.moduleScope;
    const entryFunction = {
        type: 'Function',
        sourceFilename: cur.filename,
        id: '#entry',
        entryBlockID: 'entry',
        maxStackDepth: 0,
        blocks: {}
    };
    cur.unit.functions[entryFunction.id] = entryFunction;
    const entryBlock = {
        id: 'entry',
        expectedStackDepthAtEntry: 0,
        operations: []
    };
    entryFunction.blocks[entryBlock.id] = entryBlock;
    const bodyCur = {
        ctx: cur.ctx,
        filename: cur.ctx.filename,
        breakScope: undefined,
        scopeStack: undefined,
        reachable: true,
        stackDepth: 0,
        node: program,
        unit: cur.unit,
        func: entryFunction,
        block: entryBlock
    };
    // Load module object which is passed as an argument to the entry function
    addOp(bodyCur, 'LoadArg', indexOperand(0));
    addOp(bodyCur, 'StoreGlobal', nameOperand(ctx.scopeAnalysis.thisModuleSlot.name));
    // Note: unlike compileFunction, here we don't need to compile hoisted
    // functions and variable declarations because they're bound to module-level
    // variables which aren't tied to the lifetime of the entry function. This
    // applies even to `let` bindings and bindings in nested blocks.
    compilePrologue(bodyCur, funcInfo.prologue);
    // General root-level code
    for (const statement of program.body) {
        compileModuleStatement(bodyCur, statement);
        bodyCur.commentNext = undefined;
    }
    addOp(bodyCur, 'Literal', literalOperand(undefined));
    addOp(bodyCur, 'Return');
    computeMaximumStackDepth(entryFunction);
    return entryFunction;
}
function parseToAst(filename, scriptText) {
    (0, utils_1.hardAssert)(typeof scriptText === 'string');
    try {
        return babylon.parse(scriptText, {
            sourceType: 'module',
            plugins: ['nullishCoalescingOperator', 'numericSeparator']
        });
    }
    catch (e) {
        throw !e.loc ? e : new utils_1.MicroviumSyntaxError(`${e.message}\n      at (${filename}:${e.loc.line}:${e.loc.column})`);
    }
}
exports.parseToAst = parseToAst;
function compileModuleStatement(cur, statement) {
    const statement_ = statement;
    compilingNode(cur, statement_);
    switch (statement_.type) {
        case 'VariableDeclaration': return compileModuleVariableDeclaration(cur, statement_);
        case 'ExportNamedDeclaration': return compileExportNamedDeclaration(cur, statement_);
        case 'FunctionDeclaration': return compileFunction(cur, statement_);
        // Import declarations are hoisted so they're not compiled here
        case 'ImportDeclaration': return;
        default:
            // This assignment should give a type error if the above switch hasn't covered all the SupportedModuleStatement cases
            const normalStatement = statement_;
            compileStatement(cur, normalStatement);
            break;
    }
}
exports.compileModuleStatement = compileModuleStatement;
function compileExportNamedDeclaration(cur, statement) {
    if (statement.source || statement.specifiers.length) {
        return (0, common_1.compileError)(cur, 'Only simple export syntax is supported');
    }
    const declaration = statement.declaration;
    if (!declaration) {
        // Older versions of babel didn't seem to allow for a null declaration, so
        // I'm thinking maybe it's to support a new language feature. I haven't
        // looked into it.
        return (0, common_1.featureNotSupported)(cur, 'Expected a declaration');
    }
    if (declaration.type === 'VariableDeclaration') {
        compileModuleVariableDeclaration(cur, declaration);
    }
    else if (declaration.type === 'FunctionDeclaration') {
        compileFunction(cur, declaration);
    }
    else if (declaration.type === 'ClassDeclaration') {
        compileClassDeclaration(cur, declaration);
    }
    else {
        return (0, common_1.compileError)(cur, `Not supported: export of ${declaration.type}`);
    }
}
exports.compileExportNamedDeclaration = compileExportNamedDeclaration;
function compileModuleVariableDeclaration(cur, decl) {
    /*
    Example:
  
        let x = 5;
  
    These are variable declarations at the root scope of the module, which are
    added to the `Unit.moduleVariables` list (if they're not imported or
    exported).
  
    */
    for (const d of decl.declarations) {
        compilingNode(cur, d);
        if (d.id.type !== 'Identifier') {
            return (0, common_1.compileError)(cur, 'Only simple variable declarations are supported.');
        }
        const variableName = d.id.name;
        if (!(0, utils_1.isNameString)(variableName)) {
            return (0, common_1.compileError)(cur, `Invalid variable identifier: "${variableName}"`);
        }
        const init = d.init;
        const initialValue = init
            ? LazyValue(cur => compileExpression(cur, init))
            : LazyValue(cur => addOp(cur, 'Literal', literalOperand(undefined)));
        const slot = accessVariable(cur, d.id, { forInitialization: true });
        slot.store(cur, initialValue);
    }
}
exports.compileModuleVariableDeclaration = compileModuleVariableDeclaration;
function LazyValue(load) {
    return { load };
}
// This does not add any IL at `cur`. It just creates a new ILFunction (in the
// unit) corresponding to the `func` AST. The choice was made to call
// `compileFunction` at the point in the AST where we encounter the function,
// because certain functions may need to be context-aware. E.g. class
// constructors need to be aware of property initializers.
//
// Note: `cur` is the cursor in the parent body (module entry function or parent
// function). This function creates its own `bodyCur` that points to the
// beginning of the new IL function code.
function compileFunction(cur, func) {
    compilingNode(cur, func);
    const entryBlock = {
        id: 'entry',
        expectedStackDepthAtEntry: 0,
        operations: []
    };
    if (func.generator) {
        return (0, common_1.featureNotSupported)(cur, `Generators not supported.`);
    }
    const funcInfo = cur.ctx.scopeAnalysis.scopes.get(func) ?? (0, utils_1.unexpected)();
    if (funcInfo.type !== 'FunctionScope')
        return (0, utils_1.unexpected)();
    const funcIL = {
        type: 'Function',
        sourceFilename: cur.unit.sourceFilename,
        id: funcInfo.ilFunctionId,
        entryBlockID: 'entry',
        maxStackDepth: 0,
        blocks: {
            ['entry']: entryBlock
        }
    };
    if (cur.commentNext) {
        funcIL.comments = cur.commentNext;
        cur.commentNext = undefined;
    }
    const bodyCur = {
        ctx: cur.ctx,
        filename: cur.ctx.filename,
        breakScope: undefined,
        scopeStack: undefined,
        stackDepth: 0,
        reachable: true,
        node: func,
        unit: cur.unit,
        func: funcIL,
        block: entryBlock
    };
    cur.unit.functions[funcIL.id] = funcIL;
    // Compile scope prologue
    const scope = enterScope(bodyCur, funcInfo);
    // Body of function (may be an expression if the function is an arrow function)
    const body = func.body;
    if (body.type === 'BlockStatement') {
        compileBlockStatement(bodyCur, body);
        addOp(bodyCur, 'Literal', literalOperand(undefined));
    }
    else {
        compileExpression(bodyCur, body);
    }
    if (funcInfo.isAsyncFunction) {
        addOp(bodyCur, 'AsyncReturn');
    }
    else {
        addOp(bodyCur, 'Return');
    }
    scope.leaveScope(bodyCur, 'return');
    computeMaximumStackDepth(funcIL);
    return funcIL;
}
exports.compileFunction = compileFunction;
function compilePrologue(cur, prolog) {
    for (const step of prolog) {
        switch (step.type) {
            case 'ScopePush': {
                addOp(cur, 'ScopePush', countOperand(step.slotCount));
                break;
            }
            case 'ScopeNew': {
                addOp(cur, 'ScopeNew', countOperand(step.slotCount));
                break;
            }
            case 'AsyncStart': {
                addOp(cur, 'AsyncStart', countOperand(step.slotCount), flagOperand(step.captureParent));
                break;
            }
            case 'InitFunctionDeclaration': {
                const value = LazyValue(cur => {
                    addOp(cur, 'Literal', functionLiteralOperand(step.functionId));
                    if (step.closureType === 'embedded') {
                        // Store the function pointer in the first closure slot, to make the
                        // function properly executable.
                        addOp(cur, 'StoreScoped', indexOperand(0));
                        addOp(cur, 'LoadReg', nameOperand('closure'));
                    }
                    else if (step.closureType === 'non-embedded') {
                        addOp(cur, 'ClosureNew');
                    }
                    else {
                        (0, utils_1.hardAssert)(step.closureType === 'none');
                    }
                });
                initializeSlot(step.slot, value);
                break;
            }
            case 'InitVarDeclaration': {
                const value = LazyValue(cur => addOp(cur, 'Literal', literalOperand(undefined)));
                initializeSlot(step.slot, value);
                break;
            }
            case 'InitLexicalDeclaration': {
                const value = LazyValue(cur => {
                    addOp(cur, 'Literal', {
                        type: 'LiteralOperand',
                        literal: IL.deletedValue
                    }).nameHint = step.nameHint;
                });
                initializeSlot(step.slot, value);
                break;
            }
            case 'InitParameter': {
                const value = LazyValue(cur => addOp(cur, 'LoadArg', indexOperand(step.argIndex)));
                initializeSlot(step.slot, value);
                break;
            }
            case 'InitThis': {
                const value = LazyValue(cur => addOp(cur, 'LoadArg', indexOperand(0)));
                initializeSlot(step.slot, value);
                break;
            }
            case 'InitCatchParam': {
                // This is a bit of a hack, but since the value is already at the top of
                // the stack (by the throw operation), we only need to emit the
                // instructions to save it. So the load instructions are a no-op. This
                // will be fine as long as we never have an `initializeSlot` that needs
                // to push other stuff onto the stack before the value, such as an
                // object reference (e.g. if we ever needed to store the catch param in
                // an object, for some weird reason). But in this case, that doesn't
                // make sense.
                const value = LazyValue(() => { });
                // This step is completely omitted if the slot is a local slot, since
                // the `throw` will put it in the right place anyway.
                (0, utils_1.hardAssert)(step.slot.type === 'ClosureSlotAccess');
                initializeSlot(step.slot, value);
                break;
            }
            case 'DiscardCatchParam': {
                addOp(cur, 'Pop', countOperand(1));
                break;
            }
            case 'DummyPushException': {
                // The catch block starts with an additional value on the stack. This
                // value is already on the stack, but I'm artificially pretending it's
                // part of the prologue (you can imagine that `throw` jumps to the block
                // *before* pushing the exception to the stack, so this approach is
                // still sensible) so that the block can maintain the invariant that the
                // exit stack depth is the same as the entry stack depth, and that the
                // epilogue pops as much as the prologue pushes. This is important
                // because `enterScope` and `leaveScope` calculate automatically how
                // many variables to pop off the stack and checks that the stack has the
                // same depth at exit as at entry.
                cur.stackDepth++;
                break;
            }
            case 'StartTry': {
                addOp(cur, 'StartTry', labelOfBlock(cur.scopeStack?.catchTarget ?? (0, utils_1.unexpected)()));
                break;
            }
            default: (0, utils_1.assertUnreachable)(step);
        }
    }
    function initializeSlot(slot, value) {
        // In the special case of a local slot, the prologue is ordered such that
        // the slot is in the correct place already so there is no work to do.
        if (slot.type === 'LocalSlot') {
            (0, utils_1.hardAssert)(cur.stackDepth === slot.index);
            value.load(cur);
        }
        else {
            getSlotAccessor(cur, slot).store(cur, value);
        }
    }
}
exports.compilePrologue = compilePrologue;
function compileExpressionStatement(cur, statement) {
    // Special case: a function call as a statement is compiled as a void call, so
    // that the result is never pushed in the first place. Apart from saving on
    // the Pop instruction, the runtime can perform other optimizations if it
    // knows that the result is never used. In particular, async functions do not
    // need to synthesize a return Promise if they're void-called.
    if (statement.expression.type === 'CallExpression') {
        compileCallExpression(cur, statement.expression, true, false);
        /* No Pop */
        return;
    }
    compileExpression(cur, statement.expression);
    // Pop the result of the expression off the stack
    addOp(cur, 'Pop', countOperand(1));
}
exports.compileExpressionStatement = compileExpressionStatement;
function compileReturnStatement(cur, statement) {
    // Making a copy of the cursor, like with `break`, since the flow is completely broken by a return
    const tempCur = { ...cur };
    cur.reachable = false;
    // Execute all the relevant block epilogues
    while (tempCur.scopeStack?.scope.type === 'BlockScope' || tempCur.scopeStack?.scope.type === 'ModuleScope') {
        (0, utils_1.hardAssert)(tempCur.scopeStack !== undefined);
        tempCur.scopeStack.helper.leaveScope(tempCur, 'return');
    }
    compilingNode(tempCur, statement);
    if (statement.argument) {
        compileExpression(tempCur, statement.argument);
    }
    else {
        addOp(tempCur, 'Literal', literalOperand(undefined));
    }
    const func = getContainingFunction(cur);
    if (func === undefined)
        (0, utils_1.unexpected)();
    compilingNode(tempCur, statement);
    if (func.isAsyncFunction) {
        addOp(tempCur, 'AsyncReturn');
    }
    else {
        addOp(tempCur, 'Return');
    }
}
exports.compileReturnStatement = compileReturnStatement;
function compileThrowStatement(cur, statement) {
    compileExpression(cur, statement.argument);
    addOp(cur, 'Throw');
    // The rest of the block is unreachable
    cur.reachable = false;
}
exports.compileThrowStatement = compileThrowStatement;
function compileForStatement(cur, statement) {
    const loopBlock = predeclareBlock();
    const terminateBlock = predeclareBlock();
    const bodyBlock = predeclareBlock();
    const forBlockScope = (0, utils_1.notUndefined)(cur.ctx.scopeAnalysis.scopes.get(statement));
    (0, utils_1.hardAssert)(forBlockScope.type === 'BlockScope');
    const hasClosureScope = !!forBlockScope.closureSlots;
    // Init
    if (!statement.init)
        return (0, utils_1.notImplemented)('for-loop without initializer');
    const scope = enterScope(cur, forBlockScope); // Also compiles the prolog
    compilingNode(cur, statement.init);
    if (statement.init.type === 'VariableDeclaration') {
        compileVariableDeclaration(cur, statement.init);
    }
    else {
        compileExpression(cur, statement.init);
        addOp(cur, 'Pop', countOperand(1));
    }
    // Note: the terminateBlock contains the epilogue for the `for`, including
    // popping the loop variable or closure scope. So breaking to `statement` (the
    // for loop) does not exit the loop itself
    pushBreakScope(cur, statement, terminateBlock);
    // Jump into loop from initializer
    addOp(cur, 'Jump', labelOfBlock(loopBlock));
    const loopCur = createBlock(cur, loopBlock);
    // Loop test expression
    if (!statement.test)
        return (0, utils_1.notImplemented)('for-loop without test expression');
    compileExpression(loopCur, statement.test);
    // Branch after test
    addOp(loopCur, 'Branch', labelOfBlock(bodyBlock), labelOfBlock(terminateBlock));
    // Body
    const bodyCur = createBlock(loopCur, bodyBlock);
    compileStatement(bodyCur, statement.body);
    // If any loop variables are closed over, we need to clone the loop variable
    // closure scope so that each iteration of the loop has a fresh copy of the
    // loop variable for its inner closure to remember. This happens before the
    // update expression because we want the current iteration to "remember" its
    // state before it changed in the update.
    if (hasClosureScope) {
        addOp(bodyCur, 'ScopeClone');
    }
    if (!statement.update)
        return (0, utils_1.notImplemented)('for-loop without update expression');
    compileExpression(bodyCur, statement.update);
    addOp(bodyCur, 'Pop', countOperand(1)); // Expression result not used
    // Loop back at end of body
    addOp(bodyCur, 'Jump', labelOfBlock(loopBlock));
    const terminateBlockCur = createBlock(bodyCur, terminateBlock);
    moveCursor(cur, terminateBlockCur);
    popBreakScope(cur, statement);
    scope.leaveScope(cur, 'normal'); // Also compiles the epilog
}
exports.compileForStatement = compileForStatement;
function compileBlockEpilogue(cur, block, currentOperation) {
    for (const step of block.epilogue) {
        if (currentOperation === 'return' && !step.requiredDuringReturn) {
            continue;
        }
        switch (step.type) {
            case 'Pop': {
                // Pop extra local variables off the stack
                addOp(cur, 'Pop', countOperand(step.count));
                break;
            }
            case 'EndTry': {
                const op = addOp(cur, 'EndTry');
                // EndTry unwinds the stack
                cur.stackDepth = step.stackDepthAfter;
                op.stackDepthAfter = step.stackDepthAfter;
                break;
            }
            case 'ScopePop': {
                // Pop the top closure scope
                addOp(cur, 'ScopePop');
                break;
            }
            case 'ScopeDiscard': {
                // Drop the top (only) closure
                addOp(cur, 'ScopeDiscard');
                break;
            }
            default: {
                (0, utils_1.assertUnreachable)(step);
            }
        }
    }
}
exports.compileBlockEpilogue = compileBlockEpilogue;
function compileWhileStatement(cur, statement) {
    const exitBlock = predeclareBlock();
    const testBlock = predeclareBlock();
    const bodyBlock = predeclareBlock();
    pushBreakScope(cur, statement, exitBlock);
    // Jump into loop
    addOp(cur, 'Jump', labelOfBlock(testBlock));
    // Test block
    const testCur = createBlock(cur, testBlock);
    compileExpression(testCur, statement.test);
    addOp(testCur, 'Branch', labelOfBlock(bodyBlock), labelOfBlock(exitBlock));
    // Body block
    const bodyCur = createBlock(cur, bodyBlock);
    compileStatement(bodyCur, statement.body);
    addOp(bodyCur, 'Jump', labelOfBlock(testBlock));
    // Exit block
    const exitCur = createBlock(cur, exitBlock);
    moveCursor(cur, exitCur);
    popBreakScope(cur, statement);
}
exports.compileWhileStatement = compileWhileStatement;
function compileDoWhileStatement(cur, statement) {
    const after = predeclareBlock();
    const body = predeclareBlock();
    pushBreakScope(cur, statement, after);
    // Jump into loop
    addOp(cur, 'Jump', labelOfBlock(body));
    // Loop body
    const bodyCur = createBlock(cur, body);
    compileStatement(bodyCur, statement.body);
    compileExpression(bodyCur, statement.test);
    addOp(bodyCur, 'Branch', labelOfBlock(body), labelOfBlock(after));
    // After block
    const afterCur = createBlock(bodyCur, after);
    moveCursor(cur, afterCur);
    popBreakScope(cur, statement);
}
exports.compileDoWhileStatement = compileDoWhileStatement;
function compileBlockStatement(cur, statement, opts) {
    const scopeInfo = cur.ctx.scopeAnalysis.scopes.get(statement) ?? (0, utils_1.unexpected)();
    // Compile scope prologue
    const scope = enterScope(cur, scopeInfo, opts);
    const ambientDepth = cur.stackDepth;
    // Find and compile functions
    for (const s of statement.body) {
        if (s.type === 'FunctionDeclaration') {
            compileFunction(cur, s);
        }
    }
    for (const s of statement.body) {
        (0, utils_1.hardAssert)(cur.stackDepth === ambientDepth);
        if (!cur.reachable)
            break;
        compileStatement(cur, s);
    }
    scope.leaveScope(cur, 'normal');
}
exports.compileBlockStatement = compileBlockStatement;
function compileIfStatement(cur, statement) {
    if (statement.alternate) {
        const consequent = predeclareBlock();
        const alternate = predeclareBlock();
        const after = predeclareBlock();
        // Test and branch
        compileExpression(cur, statement.test);
        compilingNode(cur, statement);
        addOp(cur, 'Branch', labelOfBlock(consequent), labelOfBlock(alternate));
        // Consequent block
        const consequentCur = createBlock(cur, consequent);
        compileStatement(consequentCur, statement.consequent);
        compilingNode(consequentCur, statement.consequent);
        addOp(consequentCur, 'Jump', labelOfBlock(after));
        // Alternate block
        const alternateCur = createBlock(cur, alternate);
        compileStatement(alternateCur, statement.alternate);
        compilingNode(alternateCur, statement);
        addOp(alternateCur, 'Jump', labelOfBlock(after));
        // After block
        const afterCur = createBlock(consequentCur, after);
        moveCursor(cur, afterCur);
    }
    else {
        const consequent = predeclareBlock();
        const after = predeclareBlock();
        // Test and branch
        compileExpression(cur, statement.test);
        compilingNode(cur, statement.test);
        addOp(cur, 'Branch', labelOfBlock(consequent), labelOfBlock(after));
        // Consequent block
        const consequentCur = createBlock(cur, consequent);
        compileStatement(consequentCur, statement.consequent);
        compilingNode(consequentCur, statement.consequent);
        addOp(consequentCur, 'Jump', labelOfBlock(after));
        // After block
        const afterCur = createBlock(cur, after);
        moveCursor(cur, afterCur);
    }
}
exports.compileIfStatement = compileIfStatement;
/**
 * Pre-declare a block to be created by createBlock. This doesn't return a
 * cursor because you can't append to the block until you properly "create it".
 *
 * The block returned from this is just a placeholder that's suitable for
 * `labelOfBlock`.
 *
 * This is used because the order that we call createBlock affects the order of
 * placement in the bytecode, and we sometimes want to have a forward-reference
 * to a block that we only want to create later.
 *
 * Every call to predeclareBlock should be matched with a corresponding call to
 * createBlock. createBlock will go back and update all the LabelOperands that
 * reference the block.
 */
function predeclareBlock() {
    const block = {};
    predeclaredBlocks.set(block, []);
    return block;
}
/**
 * Creates a block and returns a cursor at the start of the block
 *
 * @param cur The cursor from which the block follows (typically the cursor just after a branch of jump statement)
 * @param predeclaredBlock Predeclaration of the block (see predeclareBlock)
 */
function createBlock(cur, predeclaredBlock) {
    let block = {
        id: `block${cur.ctx.nextBlockID++}`,
        expectedStackDepthAtEntry: cur.stackDepth,
        operations: []
    };
    if (predeclaredBlock) {
        const dependentLabels = predeclaredBlocks.get(predeclaredBlock) ?? (0, utils_1.unexpected)();
        // Assume the object identity of the predeclaredBlock
        Object.assign(predeclaredBlock, block);
        block = predeclaredBlock;
        // Update all the labels that point to this block
        dependentLabels.forEach(l => l.targetBlockId = block.id);
        predeclaredBlocks.delete(predeclaredBlock);
    }
    if (cur.commentNext) {
        block.comments = cur.commentNext;
        cur.commentNext = undefined;
    }
    cur.func.blocks[block.id] = block;
    const blockCursor = {
        filename: cur.filename,
        breakScope: cur.breakScope,
        scopeStack: cur.scopeStack,
        reachable: true,
        ctx: cur.ctx,
        func: cur.func,
        node: cur.node,
        stackDepth: cur.stackDepth,
        unit: cur.unit,
        block
    };
    return blockCursor;
}
function addOp(cur, opcode, ...operands) {
    cur.ctx;
    const meta = IL.opcodes[opcode];
    for (const [i, expectedType] of meta.operands.entries()) {
        const operand = operands[i];
        if (!operand && expectedType.endsWith('?')) {
            continue;
        }
        if (operand.type !== expectedType) {
            return (0, common_1.internalCompileError)(cur, `Expected operand of type "${expectedType}" but received "${operand.type}", for opcode "${opcode}"`);
        }
        switch (operand.type) {
            case 'NameOperand': break;
            case 'IndexOperand': {
                if (operand.index < 0 || operand.index > IL.MAX_INDEX) {
                    return (0, common_1.internalCompileError)(cur, `Index out of range: ${operand.index}`);
                }
                break;
            }
            case 'CountOperand': {
                if (operand.count < 0 || operand.count > IL.MAX_COUNT) {
                    return (0, common_1.internalCompileError)(cur, `Count out of range: ${operand.count}`);
                }
                break;
            }
            case 'FlagOperand': {
                if (operand.flag !== true && operand.flag !== false) {
                    return (0, common_1.internalCompileError)(cur, `Flag operand incorrect: ${operand.flag}`);
                }
                break;
            }
        }
    }
    if (operands.length < (0, il_opcodes_1.minOperandCount)(opcode)) {
        return (0, common_1.internalCompileError)(cur, `Incorrect number of operands to operation with opcode "${opcode}"`);
    }
    const nodeLoc = (0, utils_1.notUndefined)(cur.node.loc);
    const loc = cur.endOfNode ? nodeLoc.end : nodeLoc.start;
    const opcode_ = opcode;
    const operation = {
        opcode: opcode_,
        operands,
        sourceLoc: { filename: cur.filename, line: loc.line, column: loc.column },
        stackDepthBefore: cur.stackDepth,
        stackDepthAfter: undefined // Assign later
    };
    if (!cur.reachable)
        return operation; // Don't add to block
    if (outputStackDepthComments) {
        cur.commentNext = [`stackDepth = ${cur.stackDepth}`];
    }
    if (cur.commentNext) {
        operation.comments = cur.commentNext;
        cur.commentNext = undefined;
    }
    cur.block.operations.push(operation);
    if (opcode !== 'EndTry') {
        const stackChange = IL.calcStaticStackChangeOfOp(operation);
        cur.stackDepth += stackChange ?? (0, utils_1.unexpected)();
    }
    else {
        // A bit of a hack. The caller will set the stack depth
        cur.stackDepth = undefined;
    }
    // console.log(`Stack is ${cur.stackDepth} after ${stringifyOperation(operation)} at ${cur.filename}:${loc.line}:${loc.column} in block ${cur.block.id}`);
    if (cur.stackDepth < 0)
        (0, common_1.internalCompileError)(cur, 'Stack imbalance');
    operation.stackDepthAfter = cur.stackDepth;
    if (opcode === 'Jump') {
        const target = operation.operands[0];
        if (target.type !== 'LabelOperand') {
            return (0, utils_1.unexpected)();
        }
        // Note: targetBlockId can be undefined if the block is predeclared (see predeclared blocks)
        if (target.targetBlockId) {
            const targetBlock = cur.func.blocks[target.targetBlockId];
            if (targetBlock.expectedStackDepthAtEntry !== operation.stackDepthAfter) {
                return (0, common_1.internalCompileError)(cur, `Jumping from stack depth of ${operation.stackDepthAfter} to block with stack depth of ${targetBlock.expectedStackDepthAtEntry}`);
            }
        }
    }
    else if (opcode === 'Branch') {
        const targetTrue = operation.operands[0];
        const targetFalse = operation.operands[1];
        if (targetTrue.type !== 'LabelOperand') {
            return (0, utils_1.unexpected)();
        }
        if (targetFalse.type !== 'LabelOperand') {
            return (0, utils_1.unexpected)();
        }
        // Note: targetBlockId can be undefined if the block is predeclared (see predeclared blocks)
        if (targetTrue.targetBlockId !== undefined) {
            const targetBlockTrue = cur.func.blocks[targetTrue.targetBlockId];
            if (targetBlockTrue.expectedStackDepthAtEntry !== operation.stackDepthAfter) {
                return (0, common_1.internalCompileError)(cur, `Branching (true branch) from stack depth of ${operation.stackDepthAfter} to block with stack depth of ${targetBlockTrue.expectedStackDepthAtEntry}`);
            }
        }
        if (targetFalse.targetBlockId !== undefined) {
            const targetBlockFalse = cur.func.blocks[targetFalse.targetBlockId];
            if (targetBlockFalse.expectedStackDepthAtEntry !== operation.stackDepthAfter) {
                return (0, common_1.internalCompileError)(cur, `Branching (false branch) from stack depth of ${operation.stackDepthAfter} to block with stack depth of ${targetBlockFalse.expectedStackDepthAtEntry}`);
            }
        }
    }
    return operation;
}
function labelOfBlock(block) {
    const labelOperand = {
        type: 'LabelOperand',
        // Note: ID can be undefined here if if the block is predeclared. It would
        // then be filled out later when createBlock is called.
        targetBlockId: block.id
    };
    const predeclaredBlockLabels = predeclaredBlocks.get(block);
    if (predeclaredBlockLabels) {
        predeclaredBlockLabels.push(labelOperand);
    }
    return labelOperand;
}
exports.labelOfBlock = labelOfBlock;
function literalOperand(value) {
    return {
        type: 'LiteralOperand',
        literal: literalOperandValue(value)
    };
}
exports.literalOperand = literalOperand;
// Note: better to use labelOfBlock if you can, but there are some places this manual form is convenient.
function labelOperand(targetBlockId) {
    return {
        type: 'LabelOperand',
        targetBlockId
    };
}
exports.labelOperand = labelOperand;
function functionLiteralOperand(functionId) {
    return {
        type: 'LiteralOperand',
        literal: {
            type: 'FunctionValue',
            value: functionId
        }
    };
}
function countOperand(count) {
    return {
        type: 'CountOperand',
        count
    };
}
exports.countOperand = countOperand;
function flagOperand(flag) {
    return {
        type: 'FlagOperand',
        flag
    };
}
exports.flagOperand = flagOperand;
function indexOperand(index) {
    return {
        type: 'IndexOperand',
        index
    };
}
exports.indexOperand = indexOperand;
function nameOperand(name) {
    return {
        type: 'NameOperand',
        name
    };
}
exports.nameOperand = nameOperand;
function opOperand(subOperation) {
    return {
        type: 'OpOperand',
        subOperation
    };
}
function literalOperandValue(value) {
    if (value === null) {
        return IL.nullValue;
    }
    switch (typeof value) {
        case 'undefined': return IL.undefinedValue;
        case 'boolean': return { type: 'BooleanValue', value };
        case 'number': return { type: 'NumberValue', value };
        case 'string': return { type: 'StringValue', value };
        default: return (0, utils_1.assertUnreachable)(value);
    }
}
function compileStatement(cur, statement_) {
    if (!cur.reachable)
        return;
    const statement = statement_;
    compilingNode(cur, statement);
    if (compileNopSpecialForm(cur, statement)) {
        return;
    }
    switch (statement.type) {
        case 'IfStatement': return compileIfStatement(cur, statement);
        case 'BlockStatement': return compileBlockStatement(cur, statement);
        case 'ExpressionStatement': return compileExpressionStatement(cur, statement);
        case 'WhileStatement': return compileWhileStatement(cur, statement);
        case 'DoWhileStatement': return compileDoWhileStatement(cur, statement);
        case 'VariableDeclaration': return compileVariableDeclaration(cur, statement);
        case 'ForStatement': return compileForStatement(cur, statement);
        case 'ReturnStatement': return compileReturnStatement(cur, statement);
        case 'ThrowStatement': return compileThrowStatement(cur, statement);
        case 'SwitchStatement': return compileSwitchStatement(cur, statement);
        case 'BreakStatement': return compileBreakStatement(cur, statement);
        case 'TryStatement': return compileTryStatement(cur, statement);
        case 'FunctionDeclaration': return; // Hoisted to block level
        case 'ExportNamedDeclaration': return (0, common_1.compileError)(cur, 'Named export declarations not supported');
        case 'ClassDeclaration': return compileClassDeclaration(cur, statement);
        default: return (0, common_1.compileErrorIfReachable)(cur, statement);
    }
}
exports.compileStatement = compileStatement;
// Note: `cur` is the cursor in the parent body (module entry function or parent function)
function compileClassDeclaration(cur, classDecl) {
    if (classDecl.superClass)
        (0, common_1.compileError)(cur, 'Extends not supported', classDecl.superClass);
    if (classDecl.decorators)
        (0, common_1.compileError)(cur, 'Decorators not supported', classDecl.decorators?.[0]);
    const createClass = LazyValue(cur => {
        // Push the constructor
        compileClassConstructor(cur, classDecl);
        // Create an object for the static props. Note: we can't actually populate the
        // static props yet until the class has been bound to the name, since static
        // property initializers are allowed to refer to the class itself.
        addOp(cur, 'ObjectNew').nameHint = 'static props';
        // Create the class (tuple of constructor and props)
        addOp(cur, 'ClassCreate').nameHint = classDecl.id.name;
    });
    // We need to assign to a variable early, because a lot of the initializers to
    // come are allowed to have side effects.
    const classSlot = accessVariable(cur, classDecl.id, { forInitialization: true });
    classSlot.store(cur, createClass);
    // Static "prototype" property
    const createClassPrototype = LazyValue(cur => compileClassPrototype(cur, classDecl));
    getObjectMemberAccessor(cur, classSlot, 'prototype').store(cur, createClassPrototype);
    const fields = classDecl.body.body.filter(B.isClassField);
    // Computed class keys are not supported because the order of evaluation is
    // difficult to get right. Even for something as simple as methods, computed
    // keys are able to "see" the current state of the class variable, so the
    // order that we evaluate everything becomes much more important.
    for (const field of fields) {
        if (field.computed) {
            (0, common_1.featureNotSupported)(cur, 'Computed class keys', field.key);
        }
    }
    // Static methods
    for (const field of fields) {
        // Note: Non-static members are built in `compileClassPrototype`
        if (field.static && field.type === 'ClassMethod') {
            const key = getFieldKey(field);
            const method = compileClassMethod(cur, field);
            getObjectMemberAccessor(cur, classSlot, key).store(cur, method);
        }
    }
    // Static properties
    for (const field of fields) {
        // Note: Non-static members are built in `compileClassPrototype`
        if (field.static && field.type === 'ClassProperty') {
            const key = getFieldKey(field);
            const value = LazyValue(cur => field.value
                ? compileExpression(cur, field.value)
                : addOp(cur, 'Literal', literalOperand(undefined)));
            getObjectMemberAccessor(cur, classSlot, key).store(cur, value);
        }
    }
}
exports.compileClassDeclaration = compileClassDeclaration;
function compileClassPrototype(cur, classDecl) {
    !classDecl.superClass || (0, common_1.featureNotSupported)(cur, 'class inheritance', classDecl.superClass);
    const stackPositionOfPrototype = cur.stackDepth;
    addOp(cur, 'ObjectNew');
    const prototype = getSlotAccessor(cur, { type: 'LocalSlot', index: stackPositionOfPrototype, debugName: '' }, false, `${classDecl.id.name}.prototype`);
    const fields = classDecl.body.body.filter(B.isClassField);
    // Class methods
    for (const field of fields) {
        if (field.static)
            continue; // Static fields are handled separately
        if (field.type === 'ClassMethod') {
            // The constructor is compiled separately
            if (B.isConstructor(field))
                continue;
            const method = compileClassMethod(cur, field);
            getObjectMemberAccessor(cur, prototype, getFieldKey(field)).store(cur, method);
        }
        else if (field.type === 'ClassProperty') {
            // Class properties are put on the instance, not the prototype
        }
        else {
            (0, common_1.featureNotSupported)(cur, field.type, field);
        }
    }
}
function getFieldKey(field) {
    return LazyValue(cur => {
        if (field.computed) {
            compileExpression(cur, field.key);
        }
        else {
            // I think non-computed keys will always be identifiers
            if (field.key.type !== 'Identifier')
                (0, utils_1.unexpected)();
            addOp(cur, 'Literal', literalOperand(field.key.name));
        }
    });
}
exports.getFieldKey = getFieldKey;
function compileClassMethod(cur, field) {
    if (field.kind === 'get' || field.kind === 'set') {
        (0, common_1.featureNotSupported)(cur, 'Getters and setters not supported in Microvium', field);
    }
    if (field.async) {
        (0, common_1.featureNotSupported)(cur, 'Async methods not supported in Microvium', field);
    }
    if (field.generator) {
        (0, common_1.featureNotSupported)(cur, 'Generator methods not supported in Microvium', field);
    }
    // The constructor is treated separately to the other class methods
    if (field.kind === 'constructor') {
        return (0, utils_1.unexpected)();
    }
    return LazyValue(cur => compileGeneralFunctionExpression(cur, field));
}
exports.compileClassMethod = compileClassMethod;
function compileClassConstructor(cur, classDecl) {
    const entryBlock = {
        id: 'entry',
        expectedStackDepthAtEntry: 0,
        operations: []
    };
    const classInfo = cur.ctx.scopeAnalysis.scopes.get(classDecl) ?? (0, utils_1.unexpected)();
    if (classInfo.type !== 'ClassScope')
        (0, utils_1.unexpected)();
    const constructorInfo = classInfo.physicalConstructorScope;
    const constructorIL = {
        type: 'Function',
        sourceFilename: cur.unit.sourceFilename,
        id: constructorInfo.ilFunctionId,
        entryBlockID: 'entry',
        maxStackDepth: 0,
        blocks: {
            ['entry']: entryBlock
        }
    };
    cur.unit.functions[constructorIL.id] = constructorIL;
    if (cur.commentNext) {
        constructorIL.comments = cur.commentNext;
        cur.commentNext = undefined;
    }
    const bodyCur = {
        ctx: cur.ctx,
        filename: cur.ctx.filename,
        breakScope: undefined,
        scopeStack: undefined,
        stackDepth: 0,
        reachable: true,
        node: classDecl,
        unit: cur.unit,
        func: constructorIL,
        block: entryBlock
    };
    compileClassConstructorBody(bodyCur, classDecl);
    computeMaximumStackDepth(constructorIL);
    // Back in the declaring scope, we push a reference to the function
    addOp(cur, 'Literal', functionLiteralOperand(constructorIL.id));
    if (constructorInfo.functionIsClosure) {
        // I don't think the static analysis will ever embed a constructor closure (TODO: Check this)
        (0, utils_1.hardAssert)(!constructorInfo.embeddedInParentSlot);
        addOp(cur, 'ClosureNew');
    }
}
exports.compileClassConstructor = compileClassConstructor;
function compileClassConstructorBody(cur, classDecl) {
    const classInfo = cur.ctx.scopeAnalysis.scopes.get(classDecl) ?? (0, utils_1.unexpected)();
    if (classInfo.type !== 'ClassScope')
        (0, utils_1.unexpected)();
    const fields = classDecl.body.body.filter(B.isClassField);
    // Compile prologue
    const physicalConstructorScope = enterScope(cur, classInfo.physicalConstructorScope);
    // The instance itself at this point
    const inst = LazyValue(cur => addOp(cur, 'LoadArg', indexOperand(0)));
    // Compile pre-constructor field assignments
    for (const field of fields) {
        if (field.type === 'ClassProperty') {
            if (field.computed) {
                // I'm not supporting this at the moment because to keep to the spec we
                // would need to compute the key at the time the class is declared, but
                // then keep the key in a slot until the time that the class is
                // instantiated. I don't want to deal with slot assignment for such an
                // edge case.
                //
                // This ordering restriction also applies to static class properties.
                // All the class member keys must be calculated before any static
                // property initializers are evaluated, meaning we need to allocate
                // slots for their results.
                (0, common_1.featureNotSupported)(cur, 'Class properties with computed names.', field.key);
            }
            // We're only looking at instance properties here. Static properties are
            // dealt with in the class declaration itself.
            if (field.static)
                continue;
            // If the field is not computed (as asserted above), then it must be an identifier
            if (field.key.type !== 'Identifier')
                (0, utils_1.unexpected)();
            const value = LazyValue(cur => field.value
                ? compileExpression(cur, field.value)
                : addOp(cur, 'Literal', literalOperand(undefined)));
            getObjectMemberAccessor(cur, inst, field.key.name).store(cur, value);
        }
        else if (field.type === 'ClassMethod') {
            // Class methods are declared on the prototype during the declaration, not
            // in the constructor.
        }
        else
            (0, utils_1.unexpected)();
    }
    // Body of constructor
    const ctor = classDecl.body.body.find(B.isConstructor);
    if (ctor) {
        if (ctor.type !== 'ClassMethod')
            (0, utils_1.unexpected)();
        const info = cur.ctx.scopeAnalysis.scopes.get(ctor) ?? (0, utils_1.unexpected)();
        info === classInfo.virtualConstructorScope || (0, utils_1.unexpected)();
        // Prologue, including parameter bindings
        const virtualConstructorScope = enterScope(cur, info);
        const body = ctor.body;
        compileBlockStatement(cur, body);
        virtualConstructorScope.leaveScope(cur, 'normal');
    }
    // By default, the constructor returns the constructed object, which is the
    // first parameter passed to the function. This is the default behavior but of
    // course the user code can also `return` whatever it likes.
    addOp(cur, 'LoadArg', indexOperand(0));
    addOp(cur, 'Return');
    physicalConstructorScope.leaveScope(cur, 'return');
}
function compileTryStatement(cur, statement) {
    if (statement.finalizer) {
        compilingNode(cur, statement.finalizer);
        return (0, common_1.compileError)(cur, 'Not supported: finally');
    }
    if (!statement.handler) {
        // If we supported `finally` then the catch is optional, but a try on its
        // own doesn't make sense.
        return (0, common_1.compileError)(cur, 'Missing catch clause in try..catch');
    }
    const tryBody = statement.block;
    const catchBody = statement.handler;
    if (catchBody.param && catchBody.param.type !== 'Identifier') {
        compilingNode(cur, catchBody.param);
        return (0, common_1.compileError)(cur, 'Only simple binding supported in catch statement');
    }
    const catchBlock = predeclareBlock();
    const after = predeclareBlock();
    const stackDepth = cur.stackDepth;
    compileBlockStatement(cur, tryBody, { catchTarget: catchBlock, stackDepth });
    addOp(cur, 'Jump', labelOfBlock(after));
    const catchCur = createBlock(cur, catchBlock);
    compileBlockStatement(catchCur, catchBody.body);
    addOp(catchCur, 'Jump', labelOfBlock(after));
    const afterCur = createBlock(catchCur, after);
    moveCursor(cur, afterCur);
}
exports.compileTryStatement = compileTryStatement;
function cloneCursor(cur) {
    return { ...cur, commentNext: undefined };
}
function compileBreakStatement(cur, statement) {
    if (statement.label) {
        return (0, common_1.compileError)(cur, 'Not supported: labelled break statement');
    }
    const breakScope = cur.breakScope;
    if (!breakScope) {
        return (0, common_1.compileError)(cur, 'No valid break target identified');
    }
    (0, utils_1.hardAssert)(breakScope.breakToTarget);
    // Create a copy of the cursor. This is in a sense because we're "branching"
    // off. But more practically speaking, `compileBreakStatement` is being called
    // from some nested statement and the original cursor still needs to unwind as
    // it would normally.
    const tempCur = { ...cur };
    // Execute all the block epilogues (popping closure scopes etc)
    while (tempCur.scopeStack?.scope !== breakScope.scope) {
        (0, utils_1.hardAssert)(tempCur.scopeStack !== undefined);
        tempCur.scopeStack.helper.leaveScope(tempCur, 'break');
    }
    compilingNode(cur, statement);
    addOp(tempCur, 'Jump', labelOfBlock(breakScope.breakToTarget));
    // The rest of the block is unreachable
    cur.reachable = false;
}
exports.compileBreakStatement = compileBreakStatement;
function pushBreakScope(cur, statement, breakToTarget) {
    const breakScope = {
        breakToTarget,
        parent: cur.breakScope,
        statement,
        scope: cur.scopeStack?.scope
    };
    cur.breakScope = breakScope;
    return breakScope;
}
function popBreakScope(cur, statement) {
    if (!cur.breakScope)
        return (0, utils_1.unexpected)();
    (0, utils_1.hardAssert)(cur.breakScope.statement === statement);
    cur.breakScope = cur.breakScope.parent;
}
function compileSwitchStatement(cur, statement) {
    // Predeclarations for all the blocks
    const testBlocks = statement.cases.map(predeclareBlock);
    const consequentBlocks = statement.cases.map(predeclareBlock);
    const breakBlock = predeclareBlock();
    compileExpression(cur, statement.discriminant);
    // While in the switch statement, `break` statements go to the break block
    pushBreakScope(cur, statement, breakBlock);
    // Jump to first test block
    const firstBlock = testBlocks[0] ?? breakBlock;
    addOp(cur, 'Jump', labelOfBlock(firstBlock));
    let testBlockNum = 0;
    let consequentIndex = 0;
    let generatedDefaultCase = false;
    let generateDefaultCase;
    // Loop through all the tests first. I'm laying down the blocks basically in
    // the order I want them in ROM
    for (const switchCase of statement.cases) {
        const { test } = switchCase;
        const consequentBlock = consequentBlocks[consequentIndex];
        // Note: the test will be null if this is a "default" case
        if (test) {
            const thisTestCur = createBlock(cur, testBlocks[testBlockNum]);
            const nextTestBlock = testBlocks[testBlockNum + 1] ?? breakBlock;
            // Perform the test on a duplicate of the discriminant
            compileDup(thisTestCur);
            compileExpression(thisTestCur, test);
            addOp(thisTestCur, 'BinOp', opOperand('==='));
            addOp(thisTestCur, 'Branch', labelOfBlock(consequentBlock), labelOfBlock(nextTestBlock));
            testBlockNum++;
        }
        else {
            // If there's an existing default case it's a compile error (I'm not sure
            // if Babel already filters this case)
            if (generatedDefaultCase) {
                compilingNode(cur, switchCase);
                return (0, common_1.compileError)(cur, 'Duplicate `default` block in switch statement');
            }
            generatedDefaultCase = true;
            // We only generate the default case at the end, just because I want to
            // keep the blocks in the order in which they're executed.
            generateDefaultCase = () => {
                // If there is a default case, it needs to be tested last
                const thisTestCur = createBlock(cur, testBlocks[testBlocks.length - 1]);
                // Unconditional branch to consequent
                addOp(thisTestCur, 'Jump', labelOfBlock(consequentBlock));
            };
        }
        consequentIndex++;
    }
    generateDefaultCase && generateDefaultCase();
    // Loop through all the consequents
    consequentIndex = 0;
    for (const { consequent } of statement.cases) {
        const consequentBlockCur = createBlock(cur, consequentBlocks[consequentIndex]);
        for (const statement of consequent) {
            compileStatement(consequentBlockCur, statement);
        }
        // Fall through from one consequent to the next or break out of the switch
        const nextConsequentBlock = consequentBlocks[consequentIndex + 1] ?? breakBlock;
        addOp(consequentBlockCur, 'Jump', labelOfBlock(nextConsequentBlock));
        consequentIndex++;
    }
    // The break block needs to perform the matching `pop` of the original test
    // value. This can't be done in the consequents because each falls into the
    // next (and it would be more instructions)
    const breakBlockCur = createBlock(cur, breakBlock);
    addOp(breakBlockCur, 'Pop', countOperand(1));
    moveCursor(cur, breakBlockCur);
    popBreakScope(cur, statement);
}
exports.compileSwitchStatement = compileSwitchStatement;
function compileExpression(cur, expression_) {
    if (!cur.reachable)
        return;
    const expression = expression_;
    compilingNode(cur, expression);
    switch (expression.type) {
        case 'BooleanLiteral':
        case 'NumericLiteral':
        case 'StringLiteral':
            return addOp(cur, 'Literal', literalOperand(expression.value));
        case 'NullLiteral': return addOp(cur, 'Literal', literalOperand(null));
        case 'Identifier': return compileIdentifier(cur, expression);
        case 'BinaryExpression': return compileBinaryExpression(cur, expression);
        case 'UpdateExpression': return compileUpdateExpression(cur, expression);
        case 'UnaryExpression': return compileUnaryExpression(cur, expression);
        case 'AssignmentExpression': return compileAssignmentExpression(cur, expression);
        case 'LogicalExpression': return compileLogicalExpression(cur, expression);
        case 'CallExpression': return compileCallExpression(cur, expression, false, false);
        case 'NewExpression': return compileNewExpression(cur, expression);
        case 'MemberExpression': return compileMemberExpression(cur, expression);
        case 'ArrayExpression': return compileArrayExpression(cur, expression);
        case 'ObjectExpression': return compileObjectExpression(cur, expression);
        case 'ConditionalExpression': return compileConditionalExpression(cur, expression);
        case 'ThisExpression': return compileThisExpression(cur, expression);
        case 'ArrowFunctionExpression': return compileArrowFunctionExpression(cur, expression);
        case 'FunctionExpression': return compileFunctionExpression(cur, expression);
        case 'TemplateLiteral': return compileTemplateLiteral(cur, expression);
        case 'AwaitExpression': return compileAwaitExpression(cur, expression);
        case 'ClassExpression': return (0, common_1.featureNotSupported)(cur, 'class expressions');
        default: return (0, common_1.compileErrorIfReachable)(cur, expression);
    }
}
exports.compileExpression = compileExpression;
function compileAwaitExpression(cur, expression) {
    if (expression.argument.type === 'CallExpression') {
        // Await-call operation
        compileCallExpression(cur, expression.argument, false, true);
    }
    else {
        compileExpression(cur, expression.argument);
    }
    if (cur.ctx.awaitStackDepths) {
        // Check the stack depth from the previous run matches this run, since the
        // analysis assumes that the stack depth is the same for each run even though
        // the closure size changes in the second run.
        const loc = `${expression.loc?.start.line}:${expression.loc.start.column + 1}`;
        (0, utils_1.hardAssert)(cur.ctx.awaitStackDepths.get(loc) === cur.stackDepth);
    }
    compilingNode(cur, expression);
    addOp(cur, 'Await');
    // Look for the closest catch block in the same function
    let scope = cur.scopeStack;
    while (scope && !scope.catchTarget && scope.parent && scope.parent.scope.type !== 'FunctionScope') {
        if (scope.scope.type === 'ModuleScope') {
            (0, common_1.compileError)(cur, 'Await expression not allowed at module scope', expression);
        }
        scope = scope.parent;
    }
    if (!scope?.catchTarget)
        scope = undefined; // No catch block found in the same function
    (0, utils_1.hardAssert)(!scope || (scope.catchTarget && scope.stackDepth));
    // Either we found a catch block, or we just use the default catch block in
    // the async function which is in slots 1 and 2.
    const catchTargetDepth = scope?.stackDepth ?? 1;
    // The number of slots that need to be copied out of the closure into the call
    // stack. Note that the `Await` instruction itself has a stack change value of
    // -1, meaning that `cur.stackDepth` here does not include the value to be
    // awaited, nor the result of awaiting that value.
    const restoreSlotCount = cur.stackDepth
        - 1 // synchronous return value in var[0]
        - 2; // catch target
    // The "catchTarget" operand is the number of slots we have to go back
    // relative to the current stack depth to find the closest catch target after
    // resuming the async function. For example, if there are no other slots on
    // the stack then the current stack depth is 3 and the catch target is in slot
    // 1, so the `catchTarget` variable will be `2` to indicate that the catch
    // target is 2 slots behind the stack pointer.
    const catchTarget = cur.stackDepth - catchTargetDepth;
    (0, utils_1.hardAssert)(catchTarget >= 2); // Needs to be at least 2 slots back because a catch target consumes 2 slots.
    const resumeOp = addOp(cur, 'AsyncResume', countOperand(restoreSlotCount), countOperand(catchTarget));
    resumeOp.stackDepthBefore = 0; // Bit of a hack, but we expect the stack to be empty when we resume the async function
}
exports.compileAwaitExpression = compileAwaitExpression;
function compileTemplateLiteral(cur, expression) {
    /*
    This is for a plain template literal, without the tag. For example
    `abc${expr}xyz`.
  
    Basically I treat this as equivalent to a series of string concatenations.
    */
    // The quasis seems to be the string parts
    // I don't know under what circumstances the `cooked` field will not be populated
    const strings = expression.quasis.map(s => s.value.cooked ?? (0, utils_1.unexpected)());
    const expressions = expression.expressions;
    // I think there will always be one more string literal than expression.
    if (strings.length !== expressions.length + 1)
        (0, utils_1.unexpected)();
    // I think there will always be at least one string part
    const firstString = strings[0] ?? (0, utils_1.unexpected)();
    addOp(cur, 'Literal', literalOperand(firstString));
    for (let i = 0; i < expressions.length; i++) {
        const expression = expressions[i];
        // I don't know why these TSTypes would be valid "expressions"
        if (B.isTSType(expression)) {
            return (0, common_1.featureNotSupported)(cur, 'Expected expression');
        }
        compileExpression(cur, expression);
        addOp(cur, 'BinOp', opOperand('+'));
        const s = strings[i + 1];
        if (s !== undefined && s !== '') {
            addOp(cur, 'Literal', literalOperand(s));
            addOp(cur, 'BinOp', opOperand('+'));
        }
    }
}
exports.compileTemplateLiteral = compileTemplateLiteral;
function compileArrowFunctionExpression(cur, expression) {
    compileGeneralFunctionExpression(cur, expression);
}
exports.compileArrowFunctionExpression = compileArrowFunctionExpression;
function compileFunctionExpression(cur, expression) {
    compileGeneralFunctionExpression(cur, expression);
}
exports.compileFunctionExpression = compileFunctionExpression;
/** Compiles a function and returns a lazy sequence of instructions to reference the value locally */
function compileGeneralFunctionExpression(cur, expression) {
    const functionScopeInfo = cur.ctx.scopeAnalysis.scopes.get(expression) ?? (0, utils_1.unexpected)();
    if (functionScopeInfo.type !== 'FunctionScope' && functionScopeInfo.type)
        (0, utils_1.unexpected)();
    // Push reference to target
    addOp(cur, 'Literal', functionLiteralOperand(functionScopeInfo.ilFunctionId));
    // If the function does not need to be a closure, then the above literal
    // reference is sufficient. If the function needs to be a closure, we need to
    // bind the scope.
    if (functionScopeInfo.functionIsClosure) {
        if (functionScopeInfo.embeddedInParentSlot) {
            // Store the function pointer in the first closure slot, to make the
            // function properly executable.
            addOp(cur, 'StoreScoped', indexOperand(0));
            addOp(cur, 'LoadReg', nameOperand('closure'));
        }
        else {
            addOp(cur, 'ClosureNew');
        }
    }
    compileFunction(cur, expression);
}
/** Returns a LazyValue of the value current at the top of the stack */
function valueAtTopOfStack(cur) {
    const indexOfValue = cur.stackDepth - 1;
    return LazyValue(cur => addOp(cur, 'LoadVar', indexOperand(indexOfValue)));
}
function compileThisExpression(cur, expression) {
    const ref = cur.ctx.scopeAnalysis.references.get(expression) ?? (0, utils_1.unexpected)();
    getSlotAccessor(cur, ref.access, true, 'this').load(cur);
}
exports.compileThisExpression = compileThisExpression;
function compileConditionalExpression(cur, expression) {
    const consequent = predeclareBlock();
    const alternate = predeclareBlock();
    const after = predeclareBlock();
    // Expression leaves the test result at the top of the stack
    compileExpression(cur, expression.test);
    addOp(cur, 'Branch', labelOfBlock(consequent), labelOfBlock(alternate));
    // The -1 is because the branch instruction pops a value off the stack
    const consequentCur = createBlock(cur, consequent);
    compileExpression(consequentCur, expression.consequent);
    addOp(consequentCur, 'Jump', labelOfBlock(after));
    const alternateCur = createBlock(cur, alternate);
    compileExpression(alternateCur, expression.alternate);
    addOp(alternateCur, 'Jump', labelOfBlock(after));
    // The stack depth is the same as when we have the "test" result on the stack,
    // because the consequent and alternate paths both pop the test and push the
    // result.
    const afterCur = createBlock(alternateCur, after);
    moveCursor(cur, afterCur);
}
exports.compileConditionalExpression = compileConditionalExpression;
function compileArrayExpression(cur, expression) {
    const indexOfArrayInstance = cur.stackDepth;
    const op = addOp(cur, 'ArrayNew');
    op.staticInfo = {
        minCapacity: expression.elements.length
    };
    let endsInElision = false;
    for (const [i, element] of expression.elements.entries()) {
        if (!element) {
            endsInElision = true;
            // Missing elements are just elisions. It's safe not to assign them
            continue;
        }
        endsInElision = false;
        if (element.type === 'SpreadElement') {
            return (0, common_1.compileError)(cur, 'Spread syntax not supported');
        }
        addOp(cur, 'LoadVar', indexOperand(indexOfArrayInstance));
        addOp(cur, 'Literal', literalOperand(i));
        compileExpression(cur, element);
        compilingNode(cur, expression);
        addOp(cur, 'ObjectSet');
    }
    // If the array literal ends in an elision, then we need to update the length
    // manually.
    if (endsInElision) {
        addOp(cur, 'LoadVar', indexOperand(indexOfArrayInstance));
        addOp(cur, 'Literal', literalOperand('length'));
        addOp(cur, 'Literal', literalOperand(expression.elements.length));
        compilingNode(cur, expression);
        addOp(cur, 'ObjectSet');
    }
}
exports.compileArrayExpression = compileArrayExpression;
function compileObjectExpression(cur, expression) {
    addOp(cur, 'ObjectNew');
    const objectVariableIndex = cur.stackDepth - 1;
    for (const property of expression.properties) {
        if (property.type === 'SpreadElement') {
            return (0, common_1.compileError)(cur, 'Spread syntax not supported');
        }
        // TODO: It would be pretty easy to add support for methods and computed
        // property names here.
        if (property.type === 'ObjectMethod') {
            return (0, common_1.compileError)(cur, 'Object methods are not supported');
        }
        if (property.computed || property.key.type !== 'Identifier') {
            return (0, common_1.compileError)(cur, 'Object properties must be simple identifiers');
        }
        addOp(cur, 'LoadVar', indexOperand(objectVariableIndex));
        addOp(cur, 'Literal', literalOperand(property.key.name));
        if (!B.isExpression(property.value))
            return (0, utils_1.unexpected)();
        compileExpression(cur, property.value);
        addOp(cur, 'ObjectSet');
    }
}
exports.compileObjectExpression = compileObjectExpression;
function compileMemberExpression(cur, expression) {
    if (expression.object.type === 'Super') {
        return (0, common_1.compileError)(cur, 'Illegal use of reserved word "super" in this context');
    }
    compileExpression(cur, expression.object);
    if (expression.computed) { // Like `array[index]`
        const property = expression.property;
        if (property.type === 'PrivateName')
            return (0, common_1.featureNotSupported)(cur, 'Private names not supported');
        compileExpression(cur, property);
        addOp(cur, 'ObjectGet');
    }
    else {
        // Like `object.property`
        if (expression.property.type !== 'Identifier') {
            // I don't think his can be anything other than an identifier?
            return (0, common_1.compileError)(cur, 'Unexpected accessor form');
        }
        addOp(cur, 'Literal', literalOperand(expression.property.name));
        addOp(cur, 'ObjectGet');
    }
}
exports.compileMemberExpression = compileMemberExpression;
function compileNewExpression(cur, expression) {
    const callee = expression.callee;
    if (callee.type === 'Super') {
        return (0, common_1.compileError)(cur, 'Reserved word "super" invalid in this context');
    }
    if (callee.type === 'V8IntrinsicIdentifier') {
        return (0, common_1.compileError)(cur, 'Intrinsics not supported');
    }
    compileExpression(cur, callee);
    // Placeholder for `this`. The value is not used, but the `New` instruction
    // will use this slot for the constructed object
    addOp(cur, 'Literal', literalOperand(undefined));
    for (const arg of expression.arguments) {
        if (!B.isExpression(arg))
            (0, common_1.compileError)(cur, 'Argument must be an expression', arg);
        compileExpression(cur, arg);
    }
    addOp(cur, 'New', countOperand(expression.arguments.length + 1)); // +1 is for the object reference
}
exports.compileNewExpression = compileNewExpression;
function compileCallExpression(cur, expression, isVoidCall, isAwaitCall) {
    const callee = expression.callee;
    if (callee.type === 'Super') {
        return (0, common_1.compileError)(cur, 'Reserved word "super" invalid in this context');
    }
    // Where to put the result of the call
    const indexOfResult = cur.stackDepth;
    const finalStackLevel = cur.stackDepth + (isVoidCall ? 0 : 1);
    if (callee.type === 'MemberExpression') {
        // Reserve a slot for the function reference, since this needs to be first in a Call operation
        const indexOfFunctionReference = cur.stackDepth;
        addOp(cur, 'Literal', literalOperand(undefined));
        const indexOfObjectReference = cur.stackDepth;
        compileExpression(cur, callee.object); // The first IL parameter is the object instance
        // Fetch the property on the object that represents the function to be called
        compileDup(cur);
        if (callee.computed) {
            compileExpression(cur, callee.property);
        }
        else {
            const property = callee.property;
            // Since the callee property is not computed, I expect it to be an identifier
            if (property.type !== 'Identifier')
                (0, utils_1.unexpected)('Expected an identifier');
            addOp(cur, 'Literal', literalOperand(property.name));
        }
        addOp(cur, 'ObjectGet');
        addOp(cur, 'StoreVar', indexOperand(indexOfFunctionReference));
    }
    else {
        if (!B.isExpression(callee))
            return (0, utils_1.unexpected)();
        compileExpression(cur, callee);
        addOp(cur, 'Literal', literalOperand(undefined)); // Object reference is "undefined" if it's not a method call
    }
    for (const arg of expression.arguments) {
        compilingNode(cur, arg);
        if (arg.type === 'SpreadElement') {
            return (0, common_1.compileError)(cur, 'Unsupported syntax');
        }
        if (!B.isExpression(arg))
            return (0, utils_1.unexpected)();
        compileExpression(cur, arg);
    }
    const ilArgCount = expression.arguments.length + 1; // +1 is for the object reference
    if (isAwaitCall) {
        addOp(cur, 'AwaitCall', countOperand(ilArgCount));
    }
    else {
        addOp(cur, 'Call', countOperand(ilArgCount), flagOperand(isVoidCall));
    }
    if (cur.stackDepth > finalStackLevel) {
        // Await-calls can't be followed by anything else before the Await
        // instruction.
        (0, utils_1.hardAssert)(!isAwaitCall);
        if (!isVoidCall) {
            // Some things need to be popped off the stack, but we need the result to be underneath them
            addOp(cur, 'StoreVar', indexOperand(indexOfResult));
        }
        const remainingToPop = cur.stackDepth - finalStackLevel;
        if (remainingToPop) {
            addOp(cur, 'Pop', countOperand(remainingToPop));
        }
    }
}
exports.compileCallExpression = compileCallExpression;
function compileDup(cur) {
    addOp(cur, 'LoadVar', indexOperand(cur.stackDepth - 1));
}
function compileLogicalExpression(cur, expression) {
    if (expression.operator === '&&' || expression.operator === '||') {
        const rightBlock = predeclareBlock();
        const endBlock = predeclareBlock();
        compileExpression(cur, expression.left);
        compileDup(cur);
        if (expression.operator === '&&') {
            // Short circuit && -- if left is truthy, result is right, else result is left
            addOp(cur, 'Branch', labelOfBlock(rightBlock), labelOfBlock(endBlock));
        }
        else {
            // Short circuit || -- if left is truthy, result is left, else result is right
            addOp(cur, 'Branch', labelOfBlock(endBlock), labelOfBlock(rightBlock));
        }
        const rightCur = createBlock(cur, rightBlock);
        // If we get as far as evaluating the right, it means the result is not the
        // left, so pop the duplicate-left-value off the stack
        addOp(rightCur, 'Pop', countOperand(1));
        compileExpression(rightCur, expression.right);
        addOp(rightCur, 'Jump', labelOfBlock(endBlock));
        const endCur = createBlock(rightCur, endBlock);
        moveCursor(cur, endCur);
    }
    else if (expression.operator === '??') {
        // Note: an easy way to support this is by a transpiler plugin (https://babeljs.io/docs/en/babel-plugin-proposal-nullish-coalescing-operator)
        (0, common_1.featureNotSupported)(cur, 'Nullish coalescing operator', expression);
    }
    else {
        return (0, utils_1.assertUnreachable)(expression.operator);
    }
}
exports.compileLogicalExpression = compileLogicalExpression;
function compileAssignmentExpression(cur, expression) {
    if (expression.left.type === 'RestElement' ||
        expression.left.type === 'AssignmentPattern' ||
        expression.left.type === 'ArrayPattern' ||
        expression.left.type === 'ObjectPattern' ||
        expression.left.type === 'TSParameterProperty') {
        return (0, common_1.compileError)(cur, `Syntax not supported: ${expression.left.type}`);
    }
    if (expression.operator === '=') {
        const left = accessVariable(cur, expression.left);
        compileExpression(cur, expression.right);
        const value = valueAtTopOfStack(cur);
        left.store(cur, value);
    }
    else {
        const left = accessVariable(cur, expression.left);
        left.load(cur);
        compileExpression(cur, expression.right);
        const operator = getBinOpFromAssignmentExpression(cur, expression.operator);
        addOp(cur, 'BinOp', opOperand(operator));
        const value = valueAtTopOfStack(cur);
        left.store(cur, value);
    }
}
exports.compileAssignmentExpression = compileAssignmentExpression;
function getBinOpFromAssignmentExpression(cur, operator) {
    switch (operator) {
        case '=': return (0, utils_1.unexpected)();
        case '%=': return '%';
        case '&=': return '&';
        case '*=': return '*';
        case '+=': return '+';
        case '-=': return '-';
        case '/=': return '/';
        case '<<=': return '<<';
        case '>>=': return '>>';
        case '>>>=': return '>>>';
        case '^=': return '^';
        case '|=': return '|';
        default: (0, utils_1.notImplemented)(operator);
    }
}
function getObjectMemberAccessor(cur, object, property) {
    const propertyKey = typeof property === 'string'
        ? LazyValue(cur => addOp(cur, 'Literal', literalOperand(property)))
        : property;
    return {
        load(cur) {
            object.load(cur);
            propertyKey.load(cur);
            addOp(cur, 'ObjectGet');
        },
        store(cur, value) {
            object.load(cur);
            propertyKey.load(cur);
            value.load(cur);
            addOp(cur, 'ObjectSet');
        }
    };
}
/**
 * Returns an accessor for the given variable reference.
 *
 * For convenience, this also handles the case where the reference is a member
 * expression.
 *
 * Note: In the current design, all variables are referenced by an identifier
 * node in the AST. The instructions used to read or write to a variable change
 * depending on where it is accessed *from*, which is why this takes the
 * identifier referencing the node and not the variable node. In particular, the
 * `LoadScoped` and `StoreScoped` instructions for accessing closure variables
 * accept an index operand relative to the current frame.
 */
function accessVariable(cur, variableReference, opts) {
    if (variableReference.type === 'Identifier') {
        const reference = cur.ctx.scopeAnalysis.references.get(variableReference) ?? (0, utils_1.unexpected)();
        const resolvesTo = reference.resolvesTo;
        switch (resolvesTo.type) {
            case 'Binding': return getSlotAccessor(cur, reference.access, resolvesTo.binding.isDeclaredReadonly && !opts?.forInitialization, resolvesTo.binding.name);
            case 'FreeVariable': return getGlobalAccessor(resolvesTo.name);
            case 'RootLevelThis': return getConstantAccessor(undefined);
            default: (0, utils_1.assertUnreachable)(resolvesTo);
        }
    }
    if (variableReference.type === 'MemberExpression') {
        const object = LazyValue(cur => compileExpression(cur, variableReference.object));
        // Computed properties are like a[0], and are only used for array access within the context of Microvium
        if (variableReference.computed) {
            const property = LazyValue(cur => compileExpression(cur, variableReference.property));
            return getObjectMemberAccessor(cur, object, property);
        }
        else {
            if (variableReference.property.type !== 'Identifier') {
                return (0, common_1.compileError)(cur, 'Property names must be simple identifiers');
            }
            const propName = variableReference.property.name;
            const property = LazyValue(cur => addOp(cur, 'Literal', literalOperand(propName)));
            return getObjectMemberAccessor(cur, object, property);
        }
    }
    return (0, common_1.compileError)(cur, `Feature not supported: "${variableReference.type}"`);
}
function getGlobalAccessor(name) {
    return {
        load(cur) {
            addOp(cur, 'LoadGlobal', nameOperand(name));
        },
        store(cur, value) {
            value.load(cur);
            addOp(cur, 'StoreGlobal', nameOperand(name));
        }
    };
}
exports.getGlobalAccessor = getGlobalAccessor;
/** Given SlotAccessInfo, this produces a ValueAccessor that encapsulates the IL
 * sequences required to read or write to the given slot.  */
function getSlotAccessor(cur, slotAccess, readonly = false, nameHint) {
    switch (slotAccess.type) {
        case 'GlobalSlot': {
            return {
                load(cur) {
                    addOp(cur, 'LoadGlobal', nameOperand(slotAccess.name));
                },
                store(cur, value) {
                    if (readonly) {
                        return (0, common_1.compileError)(cur, 'Cannot assign to constant');
                    }
                    value.load(cur);
                    addOp(cur, 'StoreGlobal', nameOperand(slotAccess.name));
                }
            };
        }
        case 'ModuleImportExportSlot': {
            const object = getSlotAccessor(cur, slotAccess.moduleNamespaceObjectSlot);
            const propertyName = LazyValue(cur => addOp(cur, 'Literal', literalOperand(slotAccess.propertyName)));
            const propertySlot = getObjectMemberAccessor(cur, object, propertyName);
            return propertySlot;
        }
        case 'LocalSlot': {
            return {
                load(cur) {
                    (0, utils_1.hardAssert)(slotAccess.index < cur.stackDepth);
                    const op = addOp(cur, 'LoadVar', indexOperand(slotAccess.index));
                    op.nameHint = nameHint;
                },
                store(cur, value) {
                    (0, utils_1.hardAssert)(slotAccess.index < cur.stackDepth);
                    if (readonly) {
                        return (0, common_1.compileError)(cur, 'Cannot assign to constant');
                    }
                    value.load(cur);
                    const op = addOp(cur, 'StoreVar', indexOperand(slotAccess.index));
                    op.nameHint = nameHint;
                }
            };
        }
        case 'ClosureSlotAccess': {
            return {
                load(cur) {
                    const op = addOp(cur, 'LoadScoped', indexOperand(slotAccess.relativeIndex));
                    op.nameHint = nameHint;
                },
                store(cur, value) {
                    value.load(cur);
                    const op = addOp(cur, 'StoreScoped', indexOperand(slotAccess.relativeIndex));
                    op.nameHint = nameHint;
                }
            };
        }
        case 'ConstUndefinedAccess': return getConstantAccessor(undefined);
        case 'ArgumentSlot': {
            return {
                load(cur) {
                    const op = addOp(cur, 'LoadArg', indexOperand(slotAccess.argIndex));
                    op.nameHint = nameHint;
                },
                store: () => (0, utils_1.unexpected)()
            };
        }
        default: return (0, utils_1.assertUnreachable)(slotAccess);
    }
}
exports.getSlotAccessor = getSlotAccessor;
function getConstantAccessor(constant) {
    return {
        load(cur) {
            addOp(cur, 'Literal', literalOperand(constant));
        },
        store: () => (0, utils_1.unexpected)()
    };
}
function compileUnaryExpression(cur, expression) {
    if (!expression.prefix) {
        return (0, common_1.compileError)(cur, 'Not supported');
    }
    const operator = expression.operator;
    if (operator === 'throw') {
        // I don't even know what a `throw` unary expression is. We support a
        // ThrowStatement which is not an expression
        return (0, common_1.featureNotSupported)(cur, 'throw expression');
    }
    if (operator === "void" || operator === "delete") {
        return (0, common_1.compileError)(cur, `Operator not supported: "${operator}"`);
    }
    let unOpCode = operator;
    // Special case for negative numbers, we just fold the negative straight into the literal
    if (unOpCode === '-' && expression.argument.type === 'NumericLiteral') {
        return addOp(cur, 'Literal', literalOperand(-expression.argument.value));
    }
    compileExpression(cur, expression.argument);
    addOp(cur, 'UnOp', opOperand(unOpCode));
}
exports.compileUnaryExpression = compileUnaryExpression;
function compileUpdateExpression(cur, expression) {
    let updaterOp;
    switch (expression.operator) {
        case '++':
            updaterOp = cur => compileIncr(cur);
            break;
        case '--':
            updaterOp = cur => compileDecr(cur);
            break;
        default: updaterOp = (0, utils_1.assertUnreachable)(expression.operator);
    }
    let accessor;
    const argument = expression.argument;
    if (argument.type === 'Identifier') {
        // Simple variable increment like i++
        accessor = accessVariable(cur, argument);
    }
    else if (argument.type === 'MemberExpression') {
        // Member increment like `this.x.b.c++`
        // Note: this is implemented in a kinda "cheating" way because the whole
        // object expression is used twice. So I'm checking that it doesn't have any
        // side effects. Microvium doesn't support property getters and setters, so
        // the property access itself doesn't have side effects unless it's
        // computed, so this checks that there are no computed accesses. A more
        // general implementation that doesn't have this restriction would
        // necessarily involve using a temporary on the stack, but I don't have time
        // right now to deal with that and it probably won't add a ton of value to
        // the engine. It would be nice to deal with though,
        // TODO: it would be good to revisit this and get it working in the general
        // case. E.g. for cases like `x[i++]++`.
        if (argument.computed)
            (0, common_1.featureNotSupported)(cur, 'Member access with computed key', argument.property);
        // It doesn't make sense for a non-computed property to be anything but an identifier
        if (argument.property.type !== 'Identifier')
            (0, utils_1.unexpected)();
        const propertyName = argument.property.name;
        let object = argument.object;
        while (object.type === 'MemberExpression') {
            if (object.computed)
                (0, common_1.featureNotSupported)(cur, 'Member access with computed key', object.property);
            object = object.object;
        }
        // The LHS should bottom out at a variable or `this` access
        if (object.type !== 'Identifier' && object.type !== 'ThisExpression') {
            return (0, common_1.featureNotSupported)(cur, `Member access on computed expression`, object);
        }
        accessor = getObjectMemberAccessor(cur, LazyValue(cur => compileExpression(cur, argument.object)), LazyValue(cur => addOp(cur, 'Literal', literalOperand(propertyName))));
    }
    else {
        return (0, common_1.featureNotSupported)(cur, `Not supported as the target of an increment/decrement: ${argument.type}`, argument);
    }
    accessor.load(cur);
    if (expression.prefix) {
        // If used as a prefix operator, the result of the expression is the value *after* we increment it
        updaterOp(cur);
        const valueToStore = valueAtTopOfStack(cur);
        accessor.store(cur, valueToStore);
    }
    else {
        // If used as a suffix, the result of the expression is the value *before* we increment it
        compileDup(cur);
        updaterOp(cur);
        const valueToStore = valueAtTopOfStack(cur);
        accessor.store(cur, valueToStore);
        addOp(cur, 'Pop', countOperand(1));
    }
}
exports.compileUpdateExpression = compileUpdateExpression;
function compileIncr(cur) {
    // Note: this is not the JS ++ operator, it's just a sequence of operations
    // that increments the slot at the top of the stack
    addOp(cur, 'Literal', literalOperand(1));
    addOp(cur, 'BinOp', opOperand('+'));
}
function compileDecr(cur) {
    // Note: this is not the JS ++ operator, it's just a sequence of operations
    // that decrements the slot at the top of the stack
    addOp(cur, 'Literal', literalOperand(1));
    addOp(cur, 'BinOp', opOperand('-'));
}
function compileBinaryExpression(cur, expression) {
    const binOpCode = getBinOpCode(cur, expression.operator);
    // Special form for integer division `x / y | 0`
    if (binOpCode === '|'
        && expression.left.type === 'BinaryExpression'
        && expression.left.operator === '/'
        && expression.right.type === 'NumericLiteral'
        && expression.right.value === 0) {
        compileExpression(cur, expression.left.left);
        compileExpression(cur, expression.left.right);
        addOp(cur, 'BinOp', opOperand('DIVIDE_AND_TRUNC'));
        return;
    }
    compileExpression(cur, expression.left);
    compileExpression(cur, expression.right);
    addOp(cur, 'BinOp', opOperand(binOpCode));
}
exports.compileBinaryExpression = compileBinaryExpression;
function getBinOpCode(cur, operator) {
    if (operator === 'instanceof' || operator === 'in') {
        return (0, common_1.compileError)(cur, `Operator not supported: "${operator}"`);
    }
    if (operator === '==') {
        return (0, common_1.compileError)(cur, 'Use `===` instead of `==`');
    }
    if (operator === '!=') {
        return (0, common_1.compileError)(cur, 'Use `!==` instead of `!=`');
    }
    return operator;
}
function compileIdentifier(cur, expression) {
    // Undefined is treated as a special identifier in this language
    if (expression.name === 'undefined') {
        addOp(cur, 'Literal', literalOperand(undefined));
    }
    else {
        accessVariable(cur, expression).load(cur);
    }
}
exports.compileIdentifier = compileIdentifier;
// Note: the difference between visitingNode and compilingNode is that
// visitingNode can be called during analysis passes (e.g. scope analysis) that
// don't actually emit IL, whereas `compilingNode` should be called right before
// actual IL is emitted for the particular syntax construction. The
// `compilingNode` function accumulates the comments that will be "dumped" onto
// the next IL instruction to be emitted.
function compilingNode(cur, node) {
    // If it's already associated, we don't want to repeat the side effects of
    // this function (i.e. associating comments)
    if (cur.node === node) {
        return;
    }
    // Note: there can be multiple nodes that precede the generation of an
    // instruction, and this just uses the comment from the last node, which seems
    // "good enough"
    if (node.leadingComments) {
        cur.commentNext = node.leadingComments.map(c => c.value.trim());
    }
    (0, common_1.visitingNode)(cur, node);
}
exports.compilingNode = compilingNode;
function compilingEndOfNode(cur, node) {
    cur.node = node;
    cur.endOfNode = true;
}
exports.compilingEndOfNode = compilingEndOfNode;
function compileVariableDeclaration(cur, decl) {
    /*
    Note: variable declarations are non-compliant in Microvium. A declaration like
    ` var x = 5;` is compiled just `Literal(5)`, which leaves the value `5` at the
    top of the stack as the variable slot. This is non-compliant because it means
    local variable slots don't exist before their declaration (violates TDZ
    rules).
    */
    for (const d of decl.declarations) {
        compilingNode(cur, d);
        if (d.id.type !== 'Identifier') {
            return (0, common_1.compileError)(cur, 'Only simple variable declarations are supported.');
        }
        const slot = accessVariable(cur, d.id, { forInitialization: true });
        const initialValue = LazyValue(cur => d.init
            ? compileExpression(cur, d.init)
            : addOp(cur, 'Literal', literalOperand(undefined)));
        slot.store(cur, initialValue);
    }
}
exports.compileVariableDeclaration = compileVariableDeclaration;
function enterScope(cur, scope, opts) {
    const stackDepthAtEntry = cur.stackDepth;
    const helper = {
        leaveScope(cur, currentOperation) {
            if (currentOperation === 'normal') {
                scope.node && compilingEndOfNode(cur, scope.node);
            }
            if (cur.reachable) {
                // Expecting the stack to be balanced
                if (currentOperation !== 'return') {
                    (0, utils_1.hardAssert)(cur.stackDepth === stackDepthAfterProlog);
                }
                // Note: I'm only compiling the epilogue for block-level scopes because
                // function scopes already pop everything at runtime when they `return`,
                // except EndTry which should not be at the function level
                if (scope.type === 'BlockScope') {
                    compileBlockEpilogue(cur, scope, currentOperation);
                    // Check that we're back to the entry stack depth
                    if (currentOperation !== 'return') {
                        (0, utils_1.hardAssert)(cur.stackDepth === stackDepthAtEntry);
                    }
                }
                else {
                    (0, utils_1.hardAssert)(scope.epilogue.every(step => step.type !== 'EndTry'));
                }
            }
            else {
                // This is a hack. The end of the block is unreachable, so it shouldn't
                // matter what the stack depth is, but for all the things that are
                // checking for a balanced stack, they will care that we restore the
                // original stack depth
                cur.stackDepth = stackDepthAtEntry;
            }
            // Pop scope stack
            (0, utils_1.hardAssert)(cur.scopeStack === newScopeStack);
            cur.scopeStack = cur.scopeStack.parent;
        }
    };
    const newScopeStack = { helper, scope, parent: cur.scopeStack, catchTarget: opts?.catchTarget, stackDepth: opts?.stackDepth ?? cur.stackDepth };
    // Push scope stack
    cur.scopeStack = newScopeStack;
    scope.node && compilingNode(cur, scope.node);
    compilePrologue(cur, scope.prologue);
    const stackDepthAfterProlog = cur.stackDepth;
    return helper;
}
function computeMaximumStackDepth(func) {
    let maxStackDepth = 0;
    for (const [_blockID, block] of (0, utils_1.entries)(func.blocks)) {
        for (const op of block.operations) {
            if (op.stackDepthBefore > maxStackDepth)
                maxStackDepth = op.stackDepthBefore;
            if (op.stackDepthAfter && op.stackDepthAfter > maxStackDepth)
                maxStackDepth = op.stackDepthAfter;
        }
    }
    func.maxStackDepth = maxStackDepth;
}
exports.computeMaximumStackDepth = computeMaximumStackDepth;
function compileNopSpecialForm(cur, statement) {
    if (statement.type !== 'ExpressionStatement')
        return false;
    const expression = statement.expression;
    if (expression.type !== 'CallExpression')
        return false;
    const callee = expression.callee;
    const args = expression.arguments;
    if (callee.type != 'Identifier')
        return false;
    if (callee.name !== '$$MicroviumNopInstruction')
        return false;
    if (args.length !== 1)
        return false;
    const sizeArg = args[0];
    if (sizeArg.type !== 'NumericLiteral')
        return false;
    if (args.length !== 1)
        return false;
    const nopSize = sizeArg.value;
    if (!(0, runtime_types_1.isUInt16)(nopSize) || nopSize < 2) {
        return (0, common_1.compileError)(cur, 'Invalid NOP size: ' + nopSize);
    }
    addOp(cur, 'Nop', countOperand(nopSize));
    return true;
}
function getContainingFunction(cur) {
    let scope = cur.scopeStack;
    while (scope && scope.scope.type === 'BlockScope') {
        scope = scope.parent;
    }
    return scope?.scope;
}
function findAwaitPoints(unit) {
    const awaitPoints = [];
    for (const func of Object.values(unit.functions)) {
        for (const block of Object.values(func.blocks)) {
            for (const op of block.operations) {
                if (op.opcode === 'Await') {
                    awaitPoints.push(op);
                }
            }
        }
    }
    return awaitPoints;
}
//# sourceMappingURL=src-to-il.js.map