/* eslint-disable no-underscore-dangle */
import {
    /* Definitions */
    N_DefProgram,
    N_DefInteractiveProgram,
    N_DefProcedure,
    N_DefFunction,
    /* Statements */
    N_StmtBlock,
    N_StmtReturn,
    N_StmtIf,
    N_StmtRepeat,
    N_StmtForeach,
    N_StmtWhile,
    N_StmtSwitch,
    N_StmtAssignVariable,
    N_StmtAssignTuple,
    N_StmtProcedureCall,
    /* Patterns */
    N_PatternWildcard,
    N_PatternVariable,
    N_PatternNumber,
    N_PatternStructure,
    N_PatternTuple,
    N_PatternTimeout,
    /* Expressions */
    N_ExprVariable,
    N_ExprConstantNumber,
    N_ExprConstantString,
    N_ExprChoose,
    N_ExprMatching,
    N_ExprList,
    N_ExprRange,
    N_ExprTuple,
    N_ExprStructure,
    N_ExprStructureUpdate,
    N_ExprFunctionCall,
    /* Other */
    ASTDefProgram,
    SymbolTable,
    ASTMain,
    ASTDefInteractiveProgram,
    ASTDefProcedure,
    ASTDefFunction,
    ASTNode,
    ASTStmtBlock,
    ASTStmtReturn,
    ASTStmtIf,
    ASTStmtRepeat,
    ASTStmtForeach,
    ASTStmtWhile,
    ASTStmtSwitch,
    ASTStmtAssignVariable,
    ASTStmtAssignTuple,
    ASTStmtProcedureCall,
    ASTPatternVariable,
    ASTPatternWildcard,
    ASTPatternNumber,
    ASTPatternStructure,
    ASTPatternTuple,
    ASTPatternTimeout,
    ASTNodeWithBranches,
    ASTExpr,
    ASTExprVariable,
    ASTExprConstantNumber,
    ASTExprConstantString,
    ASTExprChoose,
    ASTExprMatching,
    ASTExprList,
    ASTMatchingBranch,
    ASTSwitchBranch,
    ASTPattern,
    ASTExprRange,
    ASTExprTuple,
    ASTExprStructure,
    ASTExprStructureUpdate,
    ASTExprFunctionCall,
    SourceReader
} from '@gobstones/gobstones-parser';
import {
    IPushInteger,
    IPushString,
    IPushVariable,
    ISetVariable,
    IUnsetVariable,
    ILabel,
    IJump,
    IJumpIfFalse,
    IJumpIfStructure,
    IJumpIfTuple,
    ICall,
    IReturn,
    IMakeTuple,
    IMakeList,
    IMakeStructure,
    IUpdateStructure,
    IReadTupleComponent,
    IReadStructureField,
    IReadStructureFieldPop,
    IDup,
    IPop,
    IPrimitiveCall,
    ISaveState,
    IRestoreState,
    ITypeCheck,
    Code,
    Instruction
} from './instruction';
import { TypeAny, TypeInteger, TypeTuple, TypeStructure, TypeList } from './value';
import { RuntimePrimitives } from './runtime';
import { i18n } from './i18n';

/*
 * A compiler receives a symbol table (instance of SymbolTable).
 *
 * The method this.compile(ast) receives an abstract syntax tree
 * (the output of a parser).
 *
 * The AST is expected to have been linted against the given symbol table.
 *
 * The compiler produces an instance of Code, representing code for the
 * virtual machine.
 *
 * Compiling a program should never throw an exception.
 * Exceptions thrown in this module correspond to assertions,
 * i.e. internal errors that should never occur.
 * - Static conditions should be checked beforehand during the
 *   parsing and linting phases.
 * - Runtime conditions are to be checked later, during execution.
 */
export class Compiler {
    private _symtable: SymbolTable;
    private _code: Code;
    private _nextLabel: number;
    private _nextVariable: number;
    private _primitives: RuntimePrimitives;

    public constructor(symtable: SymbolTable) {
        this._symtable = symtable;
        this._code = new Code([]);
        this._nextLabel = 0;
        this._nextVariable = 0;
        this._primitives = new RuntimePrimitives();
    }

    public compile(ast: ASTMain): Code {
        this._compileMain(ast);
        return this._code;
    }

    public _compileMain(ast: ASTMain): void {
        /* Accept the empty source */
        if (ast.definitions.length === 0) {
            this._produce(ast.startPos, ast.endPos, new IReturn());
            return;
        }

        /* Compile the program (or interactive program) */
        for (const definition of ast.definitions) {
            if (definition.tag === N_DefProgram) {
                this._compileDefProgram(definition as ASTDefProgram);
            } else if (definition.tag === N_DefInteractiveProgram) {
                this._compileDefInteractiveProgram(definition as ASTDefInteractiveProgram);
            }
        }

        /* Compile procedures and functions */
        for (const definition of ast.definitions) {
            if (definition.tag === N_DefProcedure) {
                this._compileDefProcedure(definition as ASTDefProcedure);
            } else if (definition.tag === N_DefFunction) {
                this._compileDefFunction(definition as ASTDefFunction);
            }
        }
    }

    public _compileDefProgram(definition: ASTDefProgram): void {
        this._compileStatement(definition.body);
        this._produce(definition.startPos, definition.endPos, new IReturn());
    }

    /* An interactive program is compiled as a switch statement
     * followed by a Return instruction. */
    public _compileDefInteractiveProgram(definition: ASTDefInteractiveProgram): void {
        this._compileMatchBranches(definition, false /* isMatching */);
        this._produce(definition.startPos, definition.endPos, new IReturn());
    }

    /* A procedure definition:
     *
     *   procedure P(x1, ..., xN) {
     *     <body>
     *   }
     *
     * is compiled as follows:
     *
     *   P:
     *     SetVariable x1
     *     ...
     *     SetVariable xN
     *     <body>
     *     Return
     */
    public _compileDefProcedure(definition: ASTDefProcedure): void {
        this._produce(definition.startPos, definition.endPos, new ILabel(definition.name.value));
        for (const parameter of definition.parameters) {
            const parameterName = parameter.value;
            this._produce(definition.startPos, definition.endPos, new ISetVariable(parameterName));
        }
        this._compileStatement(definition.body);
        this._produce(definition.startPos, definition.endPos, new IReturn());
    }

    /* A function definition:
     *
     *   function f(x1, ..., xN) {
     *     <body>
     *   }
     *
     * is compiled as follows:
     *
     *   f:
     *     SaveState
     *     SetVariable x1
     *     ...
     *     SetVariable xN
     *     <body>
     *     RestoreState
     *     Return
     */
    public _compileDefFunction(definition: ASTDefFunction): void {
        this._produceList(definition.startPos, definition.endPos, [
            new ILabel(definition.name.value),
            new ISaveState()
        ]);
        for (const parameter of definition.parameters) {
            const parameterName = parameter.value;
            this._produce(definition.startPos, definition.endPos, new ISetVariable(parameterName));
        }
        this._compileStatement(definition.body);
        this._produceList(definition.startPos, definition.endPos, [
            new IRestoreState(),
            new IReturn()
        ]);
    }

    /* Statements are compiled to VM instructions that start and end
     * with an empty local stack. The stack may grow and shrink during the
     * execution of a statement, but it should be empty by the end.
     *
     * The only exception to this rule is the "return" statement, which
     * pushes a single value on the stack.
     */
    public _compileStatement(statement: ASTNode): void {
        switch (statement.tag) {
            case N_StmtBlock:
                return this._compileStmtBlock(statement as ASTStmtBlock);
            case N_StmtReturn:
                return this._compileStmtReturn(statement as ASTStmtReturn);
            case N_StmtIf:
                return this._compileStmtIf(statement as ASTStmtIf);
            case N_StmtRepeat:
                return this._compileStmtRepeat(statement as ASTStmtRepeat);
            case N_StmtForeach:
                return this._compileStmtForeach(statement as ASTStmtForeach);
            case N_StmtWhile:
                return this._compileStmtWhile(statement as ASTStmtWhile);
            case N_StmtSwitch:
                return this._compileStmtSwitch(statement as ASTStmtSwitch);
            case N_StmtAssignVariable:
                return this._compileStmtAssignVariable(statement as ASTStmtAssignVariable);
            case N_StmtAssignTuple:
                return this._compileStmtAssignTuple(statement as ASTStmtAssignTuple);
            case N_StmtProcedureCall:
                return this._compileStmtProcedureCall(statement as ASTStmtProcedureCall);
            default:
                throw Error('Compiler: Statement not implemented: ' + Symbol.keyFor(statement.tag));
        }
    }

    public _compileStmtBlock(block: ASTStmtBlock): void {
        for (const statement of block.statements) {
            this._compileStatement(statement);
        }
    }

    /* Merely push the return value in the stack.
     * The "new IReturn()" instruction itself is produced by the
     * methods:
     *   _compileDefProgram
     *   _compileDefInteractiveProgram
     *   _compileDefProcedure
     *   _compileDefFunction
     * */
    public _compileStmtReturn(statement: ASTStmtReturn): void {
        return this._compileExpression(statement.result);
    }

    /*
     * If without else:
     *
     *   <condition>
     *   TypeCheck Bool
     *   JumpIfFalse labelElse
     *   <thenBranch>
     *   labelElse:
     *
     * If with else:
     *
     *   <condition>
     *   TypeCheck Bool
     *   JumpIfFalse labelElse
     *   <thenBranch>
     *   Jump labelEnd
     *   labelElse:
     *   <elseBranch>
     *   labelEnd:
     */
    public _compileStmtIf(statement: ASTStmtIf): void {
        this._compileExpression(statement.condition);
        this._produce(
            statement.condition.startPos,
            statement.condition.endPos,
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {}))
        );
        const labelElse = this._freshLabel();
        this._produce(statement.startPos, statement.endPos, new IJumpIfFalse(labelElse));
        this._compileStatement(statement.thenBlock);
        if (statement.elseBlock === undefined) {
            this._produce(statement.startPos, statement.endPos, new ILabel(labelElse));
        } else {
            const labelEnd = this._freshLabel();
            this._produceList(statement.startPos, statement.endPos, [
                new IJump(labelEnd),
                new ILabel(labelElse)
            ]);
            this._compileStatement(statement.elseBlock);
            this._produce(statement.startPos, statement.endPos, new ILabel(labelEnd));
        }
    }

    /* <times>
     * TypeCheck Integer
     * labelStart:
     *   Dup                     ;\
     *   PushInteger 0           ;| if not positive, end
     *   PrimitiveCall ">", 2    ;|
     *   JumpIfFalse labelEnd    ;/
     *   <body>
     *   PushInteger 1           ;\ subtract 1
     *   PrimitiveCall "-", 2    ;/
     * Jump labelStart
     * labelEnd:
     * Pop                       ; pop the remaining number
     */
    public _compileStmtRepeat(statement: ASTStmtRepeat): void {
        this._compileExpression(statement.times);
        this._produce(
            statement.times.startPos,
            statement.times.endPos,
            new ITypeCheck(new TypeInteger())
        );
        const labelStart = this._freshLabel();
        const labelEnd = this._freshLabel();
        this._produceList(statement.startPos, statement.endPos, [
            new ILabel(labelStart),
            new IDup(),
            new IPushInteger(0),
            new IPrimitiveCall('>', 2),
            new IJumpIfFalse(labelEnd)
        ]);
        this._compileStatement(statement.body);
        this._produceList(statement.startPos, statement.endPos, [
            new IPushInteger(1),
            new IPrimitiveCall('-', 2),
            new IJump(labelStart),
            new ILabel(labelEnd),
            new IPop()
        ]);
    }

    /* <range>                   ;\ _list = temporary variable
     * TypeCheck List(Any)       ;| holding the list we are ranging over
     * SetVariable _list         ;/
     *
     * PushVariable _list                    ;\ _n = temporary variable
     * PrimitiveCall "_unsafeListLength", 1  ;| holding the total length
     * SetVariable _n                        ;/ of the list
     *
     * PushInteger 0             ;\ _pos = temporary variable holding the
     * SetVariable _pos          ;/ current index inside the list
     *
     * labelStart:
     *   PushVariable _pos       ;\
     *   PushVariable _n         ;| if out of the bounds of the list, end
     *   PrimitiveCall "<", 2    ;|
     *   JumpIfFalse labelEnd    ;/
     *
     *   PushVariable _list                    ;\ get the `pos`-th element of the
     *   PushVariable _pos                     ;| list and match the value
     *   PrimitiveCall "_unsafeListNth", 2     ;| with the pattern of the foreach
     *   [match with the pattern or fail]      ;/
     *
     *   <body>
     *
     *   PushVariable _pos       ;\
     *   PushInteger 1           ;| add 1 to the current index
     *   PrimitiveCall "+", 2    ;|
     *   SetVariable _pos        ;/
     *
     * Jump labelStart
     * labelEnd:
     * UnsetVariable _list
     * UnsetVariable _n
     * UnsetVariable _pos
     * [unset all the variables bound by the pattern]
     */
    public _compileStmtForeach(statement: ASTStmtForeach): void {
        const labelStart = this._freshLabel();
        const labelEnd = this._freshLabel();
        const list = this._freshVariable();
        const pos = this._freshVariable();
        const n = this._freshVariable();

        this._compileExpression(statement.range);
        this._produceList(statement.range.startPos, statement.range.endPos, [
            new ITypeCheck(new TypeList(new TypeAny())),
            new ISetVariable(list),

            new IPushVariable(list),
            new IPrimitiveCall('_unsafeListLength', 1),
            new ISetVariable(n)
        ]);
        this._produceList(statement.startPos, statement.endPos, [
            new IPushInteger(0),
            new ISetVariable(pos),

            new ILabel(labelStart),
            new IPushVariable(pos),
            new IPushVariable(n),
            new IPrimitiveCall('<', 2),
            new IJumpIfFalse(labelEnd),

            new IPushVariable(list),
            new IPushVariable(pos),
            new IPrimitiveCall('_unsafeListNth', 2)
        ]);
        this._compileMatchForeachPatternOrFail(statement.pattern);
        this._compileStatement(statement.body);
        this._produceList(statement.startPos, statement.endPos, [
            new IPushVariable(pos),
            new IPushInteger(1),
            new IPrimitiveCall('+', 2),
            new ISetVariable(pos),

            new IJump(labelStart),

            new ILabel(labelEnd),
            new IUnsetVariable(list),
            new IUnsetVariable(n),
            new IUnsetVariable(pos)
        ]);
        this._compilePatternUnbind(statement.pattern as ASTPatternVariable);
    }

    /* Attempt to match the pattern against the top of the stack.
     * If the pattern matches, bind its variables.
     * Otherwise, issue an error message.
     * Always pops the top of the stack.
     */
    public _compileMatchForeachPatternOrFail(pattern: ASTPattern): void {
        switch (pattern.tag) {
            case N_PatternWildcard:
                this._produce(pattern.startPos, pattern.endPos, new IPop());
                return;
            case N_PatternVariable: {
                const patternVariable = pattern as ASTPatternVariable;
                this._produce(
                    pattern.startPos,
                    pattern.endPos,
                    new ISetVariable(patternVariable.variableName.value)
                );
                return;
            }
            default: {
                /* Attempt to match, issuing an error message if there is no match:
                 *
                 *   [if subject matches pattern, jump to L]
                 *   [error message: no match]
                 * L:
                 *   [bind pattern to subject]
                 *   [pop subject]
                 */
                const label = this._freshLabel();
                this._compilePatternCheck(pattern, label);
                this._produceList(pattern.startPos, pattern.endPos, [
                    new IPushString('foreach-pattern-does-not-match'),
                    new IPrimitiveCall('_FAIL', 1),
                    new ILabel(label)
                ]);
                this._compilePatternBind(pattern);
                this._produce(pattern.startPos, pattern.endPos, new IPop());
                return;
            }
        }
    }

    /* labelStart:
     * <condition>
     * TypeCheck Bool
     * JumpIfFalse labelEnd
     * <body>
     * Jump labelStart
     * labelEnd:
     */
    public _compileStmtWhile(statement: ASTStmtWhile): void {
        const labelStart = this._freshLabel();
        const labelEnd = this._freshLabel();
        this._produce(statement.startPos, statement.endPos, new ILabel(labelStart));
        this._compileExpression(statement.condition);
        this._produceList(statement.startPos, statement.endPos, [
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {})),
            new IJumpIfFalse(labelEnd)
        ]);
        this._compileStatement(statement.body);
        this._produceList(statement.startPos, statement.endPos, [
            new IJump(labelStart),
            new ILabel(labelEnd)
        ]);
    }

    /* If the branches of the switch are:
     *    pattern1 -> body1
     *    ...      -> ...
     *    patternN -> bodyN
     * the switch construction is compiled as follows:
     *
     * <subject>
     *   [if matches pattern1, jump to label1]
     *   ...
     *   [if matches patternN, jump to labelN]
     *   [error message: no match]
     *
     * label1:
     *   [bind parameters in pattern1]
     *   [pop subject]
     *   <body1>
     *   [unbind parameters in pattern1]
     *   Jump labelEnd
     * ...
     * labelN:
     *   [bind parameters in patternN]
     *   [pop subject]
     *   <bodyN>
     *   [unbind parameters in patternN]
     *   Jump labelEnd
     * labelEnd:
     */
    public _compileStmtSwitch(statement: ASTStmtSwitch): void {
        /* Compile the subject */
        this._compileExpression(statement.subject);
        this._compileMatchBranches(statement, false /* !isMatching */);
    }

    public _compileMatchBranches(statement: ASTNodeWithBranches, isMatching: boolean): void {
        const branchLabels = [];
        /* Attempt to match each pattern */
        for (const branch of statement.branches) {
            const label = this._freshLabel();
            branchLabels.push(label);
            this._compilePatternCheck(branch.pattern, label);
        }

        /* Issue an error message if there is no match */
        this._produceList(statement.startPos, statement.endPos, [
            new IPushString('switch-does-not-match'),
            new IPrimitiveCall('_FAIL', 1)
        ]);

        /* Compile each branch */
        const labelEnd = this._freshLabel();
        for (let i = 0; i < branchLabels.length; i++) {
            const branch = statement.branches[i];
            const label = branchLabels[i];
            this._produce(branch.startPos, branch.endPos, new ILabel(label));
            this._compilePatternBind(branch.pattern);
            this._produce(branch.startPos, branch.endPos, new IPop());
            if (isMatching) {
                this._compileExpression((branch as ASTMatchingBranch).body);
            } else {
                this._compileStatement((branch as ASTSwitchBranch).body);
            }
            this._compilePatternUnbind(branch.pattern as ASTPatternVariable);
            this._produce(branch.startPos, branch.endPos, new IJump(labelEnd));
        }
        this._produce(statement.startPos, statement.endPos, new ILabel(labelEnd));
    }

    public _compileStmtAssignVariable(statement: ASTStmtAssignVariable): void {
        this._compileExpression(statement.value);
        this._produce(
            statement.startPos,
            statement.endPos,
            new ISetVariable(statement.variable.value)
        );
    }

    public _compileStmtAssignTuple(statement: ASTStmtAssignTuple): void {
        this._compileExpression(statement.value);

        /* Check that the value is indeed a tuple of the expected length */
        const anys = [];
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const _variable of statement.variables) {
            anys.push(new TypeAny());
        }
        const expectedType = new TypeTuple(anys);
        this._produce(statement.startPos, statement.endPos, new ITypeCheck(expectedType));

        /* Assign each variable */
        for (let index = 0; index < statement.variables.length; index++) {
            this._produceList(statement.startPos, statement.endPos, [
                new IReadTupleComponent(index),
                new ISetVariable(statement.variables[index].value)
            ]);
        }

        /* Pop the tuple */
        this._produce(statement.startPos, statement.endPos, new IPop());
    }

    /* There are two cases:
     * (1) The procedure is a built-in primitive.
     * (2) The procedure is a user-defined procedure.
     */
    public _compileStmtProcedureCall(statement: ASTStmtProcedureCall): void {
        const procedureName = statement.procedureName.value;
        for (const argument of statement.args) {
            this._compileExpression(argument);
        }
        if (this._primitives.isProcedure(procedureName)) {
            this._compileStmtProcedureCallPrimitive(statement);
        } else if (this._symtable.isProcedure(procedureName)) {
            this._compileStmtProcedureCallUserDefined(statement);
        } else {
            throw Error('Compiler: ' + procedureName + ' is an undefined procedure.');
        }
    }

    public _compileStmtProcedureCallPrimitive(statement: ASTStmtProcedureCall): void {
        this._produce(
            statement.startPos,
            statement.endPos,
            new IPrimitiveCall(statement.procedureName.value, statement.args.length)
        );
    }

    public _compileStmtProcedureCallUserDefined(statement: ASTStmtProcedureCall): void {
        this._produce(
            statement.startPos,
            statement.endPos,
            new ICall(statement.procedureName.value, statement.args.length)
        );
    }

    /* Pattern checks are instructions that check whether the
     * top of the stack has the expected form (matching a given pattern)
     * and, in that case, branching to the given label.
     * The top of the stack is never popped.
     * The arguments of a pattern are not bound by this instruction.
     */
    public _compilePatternCheck(pattern: ASTPattern, targetLabel: string): void {
        switch (pattern.tag) {
            case N_PatternWildcard:
                return this._compilePatternCheckWildcard(
                    pattern as ASTPatternWildcard,
                    targetLabel
                );
            case N_PatternVariable:
                return this._compilePatternCheckVariable(
                    pattern as ASTPatternVariable,
                    targetLabel
                );
            case N_PatternNumber:
                return this._compilePatternCheckNumber(pattern as ASTPatternNumber, targetLabel);
            case N_PatternStructure:
                return this._compilePatternCheckStructure(
                    pattern as ASTPatternStructure,
                    targetLabel
                );
            case N_PatternTuple:
                return this._compilePatternCheckTuple(pattern as ASTPatternTuple, targetLabel);
            case N_PatternTimeout:
                return this._compilePatternCheckTimeout(pattern as ASTPatternTimeout, targetLabel);
            default:
                throw Error(
                    'Compiler: Pattern check not implemented: ' + Symbol.keyFor(pattern.tag)
                );
        }
    }

    public _compilePatternCheckWildcard(pattern: ASTPatternWildcard, targetLabel: string): void {
        this._produce(pattern.startPos, pattern.endPos, new IJump(targetLabel));
    }

    public _compilePatternCheckVariable(pattern: ASTPatternVariable, targetLabel: string): void {
        this._produce(pattern.startPos, pattern.endPos, new IJump(targetLabel));
    }

    public _compilePatternCheckNumber(pattern: ASTPatternNumber, targetLabel: string): void {
        this._produceList(pattern.startPos, pattern.endPos, [
            new IDup(),
            new ITypeCheck(new TypeInteger()),
            new IPushInteger(parseInt(pattern.number.value, 10)),
            new IPrimitiveCall('/=', 2),
            new IJumpIfFalse(targetLabel)
        ]);
    }

    public _compilePatternCheckStructure(pattern: ASTPatternStructure, targetLabel: string): void {
        /* Check that the type of the value coincides with the type
         * of the constructor */
        const constructorName = pattern.constructorName.value;
        const typeName = this._symtable.constructorType(constructorName);
        const expectedType = new TypeStructure(typeName, {});
        this._produce(pattern.startPos, pattern.endPos, new ITypeCheck(expectedType));

        /* Jump if the value matches */
        this._produce(
            pattern.startPos,
            pattern.endPos,
            new IJumpIfStructure(constructorName, targetLabel)
        );
    }

    public _compilePatternCheckTuple(pattern: ASTPatternTuple, targetLabel: string): void {
        /* Check that the type of the value coincides with the type
         * of the tuple */
        const anys = [];
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const _variable of pattern.boundVariables) {
            anys.push(new TypeAny());
        }
        const expectedType = new TypeTuple(anys);
        this._produce(pattern.startPos, pattern.endPos, new ITypeCheck(expectedType));

        /* Jump if the value matches */
        this._produce(
            pattern.startPos,
            pattern.endPos,
            new IJumpIfTuple(pattern.boundVariables.length, targetLabel)
        );
    }

    public _compilePatternCheckTimeout(pattern: ASTPatternTimeout, targetLabel: string): void {
        this._produce(
            pattern.startPos,
            pattern.endPos,
            new IJumpIfStructure(i18n('CONS:TIMEOUT'), targetLabel)
        );
    }

    /* Pattern binding are instructions that bind the parameters
     * of a pattern to the corresponding parts of the value currently
     * at the top of the stack. The value at the top of the stack
     * is never popped (it must be duplicated if necessary).
     */
    public _compilePatternBind(pattern: ASTPattern): void {
        switch (pattern.tag) {
            case N_PatternWildcard:
                return; /* No parameters to bind */
            case N_PatternVariable:
                this._compilePatternBindVariable(pattern as ASTPatternVariable);
                return;
            case N_PatternNumber:
                return; /* No parameters to bind */
            case N_PatternStructure:
                this._compilePatternBindStructure(pattern as ASTPatternStructure);
                return;
            case N_PatternTuple:
                this._compilePatternBindTuple(pattern as ASTPatternTuple);
                return;
            case N_PatternTimeout:
                return; /* No parameters to bind */
            default:
                throw Error(
                    'Compiler: Pattern binding not implemented: ' + Symbol.keyFor(pattern.tag)
                );
        }
    }

    public _compilePatternBindVariable(pattern: ASTPatternVariable): void {
        this._produceList(pattern.startPos, pattern.endPos, [
            new IDup(),
            new ISetVariable(pattern.variableName.value)
        ]);
    }

    public _compilePatternBindStructure(pattern: ASTPatternStructure): void {
        /* Allow structure pattern with no parameters, even if the constructor
         * has parameters */
        if (pattern.boundVariables.length === 0) {
            return;
        }

        const constructorName = pattern.constructorName.value;
        const fieldNames = this._symtable.constructorFields(constructorName);
        for (let i = 0; i < fieldNames.length; i++) {
            const variable = pattern.boundVariables[i];
            const fieldName = fieldNames[i];
            this._produceList(pattern.startPos, pattern.endPos, [
                new IReadStructureField(fieldName),
                new ISetVariable(variable.value)
            ]);
        }
    }

    public _compilePatternBindTuple(pattern: ASTPatternTuple): void {
        for (let index = 0; index < pattern.boundVariables.length; index++) {
            const variable = pattern.boundVariables[index];
            this._produceList(pattern.startPos, pattern.endPos, [
                new IReadTupleComponent(index),
                new ISetVariable(variable.value)
            ]);
        }
    }

    /* Pattern unbinding are instructions that unbind the parameters
     * of a pattern. */
    public _compilePatternUnbind(pattern: ASTPatternVariable): void {
        for (const variable of pattern.boundVariables) {
            this._produceList(pattern.startPos, pattern.endPos, [
                new IUnsetVariable(variable.value)
            ]);
        }
    }

    /* Expressions are compiled to instructions that make the size
     * of the local stack grow in exactly one.
     * The stack may grow and shrink during the evaluation of an
     * expression, but an expression should not consume values
     * that were present on the stack before its evaluation started.
     * In the end the stack should have exactly one more value than
     * at the start.
     */
    public _compileExpression(expression: ASTExpr): void {
        switch (expression.tag) {
            case N_ExprVariable:
                return this._compileExprVariable(expression as ASTExprVariable);
            case N_ExprConstantNumber:
                return this._compileExprConstantNumber(expression as ASTExprConstantNumber);
            case N_ExprConstantString:
                return this._compileExprConstantString(expression as ASTExprConstantString);
            case N_ExprChoose:
                return this._compileExprChoose(expression as ASTExprChoose);
            case N_ExprMatching:
                return this._compileExprMatching(expression as ASTExprMatching);
            case N_ExprList:
                return this._compileExprList(expression as ASTExprList);
            case N_ExprRange:
                return this._compileExprRange(expression as ASTExprRange);
            case N_ExprTuple:
                return this._compileExprTuple(expression as ASTExprTuple);
            case N_ExprStructure:
                return this._compileExprStructure(expression as ASTExprStructure);
            case N_ExprStructureUpdate:
                return this._compileExprStructureUpdate(expression as ASTExprStructureUpdate);
            case N_ExprFunctionCall:
                return this._compileExprFunctionCall(expression as ASTExprFunctionCall);
            default:
                throw Error(
                    'Compiler: Expression not implemented: ' + Symbol.keyFor(expression.tag)
                );
        }
    }

    public _compileExprVariable(expression: ASTExprVariable): void {
        this._produce(
            expression.startPos,
            expression.endPos,
            new IPushVariable(expression.variableName.value)
        );
    }

    public _compileExprConstantNumber(expression: ASTExprConstantNumber): void {
        this._produce(
            expression.startPos,
            expression.endPos,
            new IPushInteger(parseInt(expression.number.value, 10))
        );
    }

    public _compileExprConstantString(expression: ASTExprConstantString): void {
        this._produce(
            expression.startPos,
            expression.endPos,
            new IPushString(expression.string.value)
        );
    }

    /*
     * An expression of the form:
     *
     *   choose a when (cond) b otherwise
     *
     * is compiled similarly as a statement of the form:
     *
     *   if (cond) { a } else { b }
     *
     * Recall that a 'choose' with many branches:
     *
     *   choose a1 when (cond1)
     *          ...
     *          aN when (condN)
     *          b  otherwise
     *
     * is actually parsed as a sequence of nested binary choose
     * constructions:
     *
     *   choose a1 when (cond1)
     *          (
     *            ...
     *            choose aN when (condN)
     *                    b otherwise
     *            ...
     *          ) otherwise
     *
     */
    public _compileExprChoose(expression: ASTExprChoose): void {
        this._compileExpression(expression.condition);
        this._produce(
            expression.condition.startPos,
            expression.condition.endPos,
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {}))
        );
        const labelOtherwise = this._freshLabel();
        this._produce(expression.startPos, expression.endPos, new IJumpIfFalse(labelOtherwise));
        this._compileExpression(expression.trueExpr);
        const labelEnd = this._freshLabel();
        this._produceList(expression.startPos, expression.endPos, [
            new IJump(labelEnd),
            new ILabel(labelOtherwise)
        ]);
        this._compileExpression(expression.falseExpr);
        this._produce(expression.startPos, expression.endPos, new ILabel(labelEnd));
    }

    public _compileExprMatching(expression: ASTExprMatching): void {
        this._compileExpression(expression.subject);
        this._compileMatchBranches(expression, true /* isMatching */);
    }

    public _compileExprList(expression: ASTExprList): void {
        for (const element of expression.elements) {
            this._compileExpression(element);
        }
        this._produce(
            expression.startPos,
            expression.endPos,
            new IMakeList(expression.elements.length)
        );
    }

    /*
     * Range expresions [first..last] and [first,second..last]
     * are compiled by calling the primitive functions
     *   _makeRange
     *   _makeRangeWithSecond
     */
    public _compileExprRange(expression: ASTExprRange): void {
        this._compileExpression(expression.first);
        this._compileExpression(expression.last);
        if (expression.second === undefined) {
            this._produce(
                expression.startPos,
                expression.endPos,
                new IPrimitiveCall('_makeRange', 2)
            );
        } else {
            this._compileExpression(expression.second);
            this._produce(
                expression.startPos,
                expression.endPos,
                new IPrimitiveCall('_makeRangeWithSecond', 3)
            );
        }
    }

    public _compileExprTuple(expression: ASTExprTuple): void {
        for (const element of expression.elements) {
            this._compileExpression(element);
        }
        this._produce(
            expression.startPos,
            expression.endPos,
            new IMakeTuple(expression.elements.length)
        );
    }

    public _compileExprStructure(expression: ASTExprStructure): void {
        const fieldNames = [];
        for (const fieldBinding of expression.fieldBindings) {
            this._compileExpression(fieldBinding.value);
            fieldNames.push(fieldBinding.fieldName.value);
        }
        const constructorName = expression.constructorName.value;
        const typeName = this._symtable.constructorType(constructorName);
        this._produce(
            expression.startPos,
            expression.endPos,
            new IMakeStructure(typeName, constructorName, fieldNames)
        );
    }

    public _compileExprStructureUpdate(expression: ASTExprStructureUpdate): void {
        this._compileExpression(expression.original);
        const fieldNames = [];
        for (const fieldBinding of expression.fieldBindings) {
            this._compileExpression(fieldBinding.value);
            fieldNames.push(fieldBinding.fieldName.value);
        }
        const constructorName = expression.constructorName.value;
        const typeName = this._symtable.constructorType(constructorName);
        this._produce(
            expression.startPos,
            expression.endPos,
            new IUpdateStructure(typeName, constructorName, fieldNames)
        );
    }

    /* There are four cases:
     * (1) The function is '&&' or '||' which must be considered separately
     *     to account for short-circuting.
     * (2) The function is a built-in primitive.
     * (3) The function is a user-defined function.
     * (4) The function is an observer / field accessor.
     */
    public _compileExprFunctionCall(expression: ASTExprFunctionCall): void {
        const functionName = expression.functionName.value;
        if (functionName === '&&') {
            this._compileExprFunctionCallAnd(expression);
        } else if (functionName === '||') {
            this._compileExprFunctionCallOr(expression);
        } else {
            for (const argument of expression.args) {
                this._compileExpression(argument);
            }
            if (this._primitives.isFunction(functionName)) {
                this._compileExprFunctionCallPrimitive(expression);
            } else if (this._symtable.isFunction(functionName)) {
                this._compileExprFunctionCallUserDefined(expression);
            } else if (this._symtable.isField(functionName)) {
                this._compileExprFunctionCallFieldAccessor(expression);
            } else {
                throw Error('Compiler: ' + functionName + ' is an undefined function.');
            }
        }
    }

    /* <expr1>
     * TypeCheck Bool
     * JumpIfStructure 'False' labelEnd
     * Pop
     * <expr2>
     * TypeCheck Bool
     * labelEnd:
     */
    public _compileExprFunctionCallAnd(expression: ASTExprFunctionCall): void {
        const expr1 = expression.args[0];
        const expr2 = expression.args[1];
        const labelEnd = this._freshLabel();
        this._compileExpression(expr1);
        this._produceList(expression.startPos, expression.endPos, [
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {})),
            new IJumpIfStructure(i18n('CONS:False'), labelEnd),
            new IPop()
        ]);
        this._compileExpression(expr2);
        this._produceList(expression.startPos, expression.endPos, [
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {})),
            new ILabel(labelEnd)
        ]);
    }

    /* <expr1>
     * TypeCheck Bool
     * JumpIfStructure 'True' labelEnd
     * Pop
     * <expr2>
     * TypeCheck Bool
     * labelEnd:
     */
    public _compileExprFunctionCallOr(expression: ASTExprFunctionCall): void {
        const expr1 = expression.args[0];
        const expr2 = expression.args[1];
        const labelEnd = this._freshLabel();
        this._compileExpression(expr1);
        this._produceList(expression.startPos, expression.endPos, [
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {})),
            new IJumpIfStructure(i18n('CONS:True'), labelEnd),
            new IPop()
        ]);
        this._compileExpression(expr2);
        this._produceList(expression.startPos, expression.endPos, [
            new ITypeCheck(new TypeStructure(i18n('TYPE:Bool'), {})),
            new ILabel(labelEnd)
        ]);
    }

    public _compileExprFunctionCallPrimitive(expression: ASTExprFunctionCall): void {
        this._produce(
            expression.startPos,
            expression.endPos,
            new IPrimitiveCall(expression.functionName.value, expression.args.length)
        );
    }

    public _compileExprFunctionCallUserDefined(expression: ASTExprFunctionCall): void {
        this._produce(
            expression.startPos,
            expression.endPos,
            new ICall(expression.functionName.value, expression.args.length)
        );
    }

    public _compileExprFunctionCallFieldAccessor(expression: ASTExprFunctionCall): void {
        this._produceList(expression.startPos, expression.endPos, [
            new IReadStructureFieldPop(expression.functionName.value)
        ]);
    }

    /* Helpers */

    /* Produce the given instruction, setting its starting and ending
     * position to startPos and endPos respectively */
    public _produce(startPos: SourceReader, endPos: SourceReader, instruction: Instruction): void {
        instruction.startPos = startPos;
        instruction.endPos = endPos;
        this._code.produce(instruction);
    }

    public _produceList(
        startPos: SourceReader,
        endPos: SourceReader,
        instructions: Instruction[]
    ): void {
        for (const instruction of instructions) {
            this._produce(startPos, endPos, instruction);
        }
    }

    /* Create a fresh label name */
    public _freshLabel(): string {
        const label = '_l' + this._nextLabel.toString();
        this._nextLabel++;
        return label;
    }

    /* Create a fresh local variable name */
    public _freshVariable(): string {
        const v = '_v' + this._nextVariable.toString();
        this._nextVariable++;
        return v;
    }
}
