text:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <cmath>
#include <ctime>
#include <fstream>
#include <algorithm>
#include <random>

#include "memory.h"
#include "cpu.h"
#include "loader.h"
#include "image.h"
#include "assembler.h"
#include "keywords.h"
#include "functions.h"
#include "operators.h"
#include "linker.h"
#include "midi.h"

#define FONT_WIDTH   6
#define FONT_HEIGHT  8
#define MAPPING_SIZE 96

namespace Keywords
{
    enum LoadUsage {LoadType=0, LoadWave, LoadMidi, LoadImage, LoadSprite, LoadFont};
    enum InitTypes {InitTime, InitMidi, InitMidiV, InitUser};
    enum MidiTypes {MidiNone, Midi, MidiV, MidiId, MidiIdV};

    struct Gprintf
    {
        int codeLineIndex = 0;
        Assembler::Gprintf _gprintfAsm;
    };

    bool _constDimStrArray = false;
    int _numNumericGotosGosubs = 0;
    MidiTypes _midiType = MidiNone;
    std::string _userRoutine;

    std::map<std::string, Keyword> _keywords;
    std::map<std::string, std::string> _equalsKeywords;

    std::map<std::string, Keyword>& getKeywords(void)           {return _keywords;      }
    std::map<std::string, std::string>& getEqualsKeywords(void) {return _equalsKeywords;}

    std::vector<Gprintf> _gprintfs;

    void reset(void)
    {
        _midiType = MidiNone;
        _userRoutine = "";
        _gprintfs.clear();
    }

    void restart(void)
    {
        _constDimStrArray = false;
        _numNumericGotosGosubs = 0;
    }

    bool initialise(void)
    {
        restart();

        // Equals keywords
        _equalsKeywords["CONST" ] = "CONST";
        _equalsKeywords["DIM"   ] = "DIM";
        _equalsKeywords["DEF"   ] = "DEF";
        _equalsKeywords["FOR"   ] = "FOR";
        _equalsKeywords["IF"    ] = "IF";
        _equalsKeywords["ELSEIF"] = "ELSEIF";
        _equalsKeywords["WHILE" ] = "WHILE";
        _equalsKeywords["UNTIL" ] = "UNTIL";

        // Keywords
        _keywords["END"     ] = {"END",      END,     Compiler::SingleStatementParsed};
        _keywords["INC"     ] = {"INC",      INC,     Compiler::SingleStatementParsed};
        _keywords["DEC"     ] = {"DEC",      DEC,     Compiler::SingleStatementParsed};
        _keywords["ON"      ] = {"ON",       ON,      Compiler::SingleStatementParsed};
        _keywords["GOTO"    ] = {"GOTO",     GOTO,    Compiler::SingleStatementParsed};
        _keywords["GOSUB"   ] = {"GOSUB",    GOSUB,   Compiler::SingleStatementParsed};
        _keywords["RETURN"  ] = {"RETURN",   RETURN,  Compiler::SingleStatementParsed};
        _keywords["RET"     ] = {"RET",      RET,     Compiler::SingleStatementParsed};
        _keywords["CLS"     ] = {"CLS",      CLS,     Compiler::SingleStatementParsed};
        _keywords["?"       ] = {"?",        PRINT,   Compiler::SingleStatementParsed};
        _keywords["PRINT"   ] = {"PRINT",    PRINT,   Compiler::SingleStatementParsed};
        _keywords["INPUT"   ] = {"INPUT",    INPUT,   Compiler::SingleStatementParsed};
        _keywords["FOR"     ] = {"FOR",      FOR,     Compiler::SingleStatementParsed};
        _keywords["NEXT"    ] = {"NEXT",     NEXT,    Compiler::SingleStatementParsed};
        _keywords["IF"      ] = {"IF",       IF,      Compiler::MultiStatementParsed };
        _keywords["ELSEIF"  ] = {"ELSEIF",   ELSEIF,  Compiler::SingleStatementParsed};
        _keywords["ELSE"    ] = {"ELSE",     ELSE,    Compiler::SingleStatementParsed};
        _keywords["ENDIF"   ] = {"ENDIF",    ENDIF,   Compiler::SingleStatementParsed};
        _keywords["WHILE"   ] = {"WHILE",    WHILE,   Compiler::SingleStatementParsed};
        _keywords["WEND"    ] = {"WEND",     WEND,    Compiler::SingleStatementParsed};
        _keywords["REPEAT"  ] = {"REPEAT",   REPEAT,  Compiler::SingleStatementParsed};
        _keywords["UNTIL"   ] = {"UNTIL",    UNTIL,   Compiler::SingleStatementParsed};
        _keywords["FOREVER" ] = {"FOREVER",  FOREVER, Compiler::SingleStatementParsed};
        _keywords["&FOREVER"] = {"&FOREVER", FOREVER, Compiler::SingleStatementParsed};
        _keywords["AS"      ] = {"AS",       AS,      Compiler::SingleStatementParsed};
        _keywords["TYPE"    ] = {"TYPE",     TYPE,    Compiler::SingleStatementParsed};
        _keywords["CALL"    ] = {"CALL",     CALL,    Compiler::SingleStatementParsed};
        _keywords["PROC"    ] = {"PROC",     PROC,    Compiler::SingleStatementParsed};
        _keywords["ENDPROC" ] = {"ENDPROC",  ENDPROC, Compiler::SingleStatementParsed};
        _keywords["LOCAL"   ] = {"LOCAL",    LOCAL,   Compiler::SingleStatementParsed};
        _keywords["CONST"   ] = {"CONST",    CONST,   Compiler::SingleStatementParsed};
        _keywords["DIM"     ] = {"DIM",      DIM,     Compiler::SingleStatementParsed};
        _keywords["DEF"     ] = {"DEF",      DEF,     Compiler::SingleStatementParsed};
        _keywords["DATA"    ] = {"DATA",     DATA,    Compiler::SingleStatementParsed};
        _keywords["READ"    ] = {"READ",     READ,    Compiler::SingleStatementParsed};
        _keywords["RESTORE" ] = {"RESTORE",  RESTORE, Compiler::SingleStatementParsed};
        _keywords["ALLOC"   ] = {"ALLOC",    ALLOC,   Compiler::SingleStatementParsed};
        _keywords["FREE"    ] = {"FREE",     FREE,    Compiler::SingleStatementParsed};
        _keywords["AT"      ] = {"AT",       AT,      Compiler::SingleStatementParsed};
        _keywords["PUT"     ] = {"PUT",      PUT,     Compiler::SingleStatementParsed};
        _keywords["MODE"    ] = {"MODE",     MODE,    Compiler::SingleStatementParsed};
        _keywords["WAIT"    ] = {"WAIT",     WAIT,    Compiler::SingleStatementParsed};
        _keywords["PSET"    ] = {"PSET",     PSET,    Compiler::SingleStatementParsed};
        _keywords["LINE"    ] = {"LINE",     LINE,    Compiler::SingleStatementParsed};
        _keywords["HLINE"   ] = {"HLINE",    HLINE,   Compiler::SingleStatementParsed};
        _keywords["VLINE"   ] = {"VLINE",    VLINE,   Compiler::SingleStatementParsed};
        _keywords["CIRCLE"  ] = {"CIRCLE",   CIRCLE,  Compiler::SingleStatementParsed};
        _keywords["CIRCLEF" ] = {"CIRCLEF",  CIRCLEF, Compiler::SingleStatementParsed};
        _keywords["RECT"    ] = {"RECT",     RECT,    Compiler::SingleStatementParsed};
        _keywords["RECTF"   ] = {"RECTF",    RECTF,   Compiler::SingleStatementParsed};
        _keywords["POLY"    ] = {"POLY",     POLY,    Compiler::SingleStatementParsed};
        _keywords["POLYR"   ] = {"POLYR",    POLYR,   Compiler::SingleStatementParsed};
        _keywords["TCLIP"   ] = {"TCLIP",    TCLIP,   Compiler::SingleStatementParsed};
        _keywords["SCROLL"  ] = {"SCROLL",   SCROLL,  Compiler::SingleStatementParsed};
        _keywords["POKE"    ] = {"POKE",     POKE,    Compiler::SingleStatementParsed};
        _keywords["DOKE"    ] = {"DOKE",     DOKE,    Compiler::SingleStatementParsed};
        _keywords["INIT"    ] = {"INIT",     INIT,    Compiler::SingleStatementParsed};
        _keywords["TICK"    ] = {"TICK",     TICK,    Compiler::SingleStatementParsed};
        _keywords["PLAY"    ] = {"PLAY",     PLAY,    Compiler::SingleStatementParsed};
        _keywords["LOAD"    ] = {"LOAD",     LOAD,    Compiler::SingleStatementParsed};
        _keywords["SPRITE"  ] = {"SPRITE",   SPRITE,  Compiler::SingleStatementParsed};
        _keywords["SOUND"   ] = {"SOUND",    SOUND,   Compiler::SingleStatementParsed};
        _keywords["SET"     ] = {"SET",      SET,     Compiler::SingleStatementParsed};
        _keywords["ASM"     ] = {"ASM",      ASM,     Compiler::SingleStatementParsed};
        _keywords["ENDASM"  ] = {"ENDASM",   ENDASM,  Compiler::SingleStatementParsed};
        _keywords["BCDADD"  ] = {"BCDADD",   BCDADD,  Compiler::SingleStatementParsed};
        _keywords["BCDSUB"  ] = {"BCDSUB",   BCDSUB,  Compiler::SingleStatementParsed};
        _keywords["BCDINT"  ] = {"BCDINT",   BCDINT,  Compiler::SingleStatementParsed};
        _keywords["BCDCPY"  ] = {"BCDCPY",   BCDCPY,  Compiler::SingleStatementParsed};
        _keywords["GPRINTF" ] = {"GPRINTF",  GPRINTF, Compiler::SingleStatementParsed};
        _keywords["EXEC"    ] = {"EXEC",     EXEC,    Compiler::SingleStatementParsed};
        _keywords["OPEN"    ] = {"OPEN",     OPEN,    Compiler::SingleStatementParsed};

        return true;
    }

    bool findKeyword(std::string code, const std::string& keyword, size_t& foundPos)
    {
        Expression::strToUpper(code);
        foundPos = code.find(keyword);
        if(foundPos != std::string::npos)
        {
            foundPos += keyword.size();
            return true;
        }
        return false;
    }

    KeywordResult handleKeywords(Compiler::CodeLine& codeLine, const std::string& keyword, int codeLineIndex, int tokenIndex, KeywordFuncResult& result)
    {
        size_t foundPos;

        std::string key = keyword;
        Expression::strToUpper(key);
        if(_keywords.find(key) == _keywords.end()) return KeywordNotFound;

        // Handle keyword in code line
        if(findKeyword(key, _keywords[key]._name, foundPos)  &&  _keywords[key]._func)
        {
            // Line index taking into account modules
            int codeLineStart = Compiler::getCodeLineStart(codeLineIndex);

            // Keyword
            bool success = _keywords[key]._func(codeLine, codeLineIndex, codeLineStart, tokenIndex, foundPos, result);
            return (!success) ? KeywordError : KeywordFound;
        }

        return KeywordFound;
    }

#ifndef STAND_ALONE
    bool addGprintf(const std::string& lineToken, const std::string& formatText, const std::vector<std::string>& variables, uint16_t address, int codeLineIndex)
    {
        std::vector<Assembler::Gprintf::Var> vars;
        std::vector<std::string> subs;
        Assembler::parseGprintfFormat(formatText, variables, vars, subs);

        Gprintf gprintf = {codeLineIndex, {Compiler::getVasmPC(), codeLineIndex, lineToken, formatText, vars, subs}};
        for(int i=0; i<int(gprintf._gprintfAsm._vars.size()); i++)
        {
            gprintf._gprintfAsm._vars[i]._indirection = 2;
            gprintf._gprintfAsm._vars[i]._data = uint16_t(address  + i*2);
        }

        _gprintfs.push_back(gprintf);

        return true;
    }

    bool convertGprintGbasToGprintfAsm(void)
    {
        for(int i=0; i<int(_gprintfs.size()); i++)
        {
            int codeLineIndex = _gprintfs[i].codeLineIndex;
            const Compiler::CodeLine& codeLine = Compiler::getCodeLines()[codeLineIndex];
            const Compiler::VasmLine& vasmLine = codeLine._vasm.back();
            Assembler::Gprintf& gprintfAsm = _gprintfs[i]._gprintfAsm;
            uint16_t address = uint16_t(vasmLine._address);

            gprintfAsm._address = address;
            if(!Assembler::addGprintf(gprintfAsm, address))
            {
                fprintf(stderr, "Keywords::fixGprintfAddresses() : '%s:%d' : Assembler::addGprintf() at '0x%04x' already exists : %s\n", codeLine._moduleName.c_str(), codeLineIndex, address, codeLine._text.c_str());
                return false;
            }
        }

        return true;
    }
#endif

    // ********************************************************************************************
    // Keywords
    // ********************************************************************************************
    bool END(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);

        //std::string labelName = "_end_" + Expression::wordToHexString(Compiler::getVasmPC());
        //Compiler::emitVcpuAsm("BRA", labelName, false, codeLineIndex, labelName);
        Compiler::emitVcpuAsm("HALT", "", false, codeLineIndex);

        return true;
    }

    bool INC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        // Operand must be an integer var
        std::string varToken = codeLine._code.substr(foundPos);
        Expression::stripWhitespace(varToken);

        // Byte write configuration
        Expression::Int16Byte int16Byte = Expression::Int16Both;
        size_t dot = varToken.find('.');
        if(dot != std::string::npos)
        {
            std::string dotName = varToken.substr(dot);
            varToken = varToken.substr(0, dot);
            Expression::strToUpper(dotName);
            if(dotName == ".LO") int16Byte = Expression::Int16Low;
            if(dotName == ".HI") int16Byte = Expression::Int16High;
        }

        int varIndex = Compiler::findVar(varToken, false);
        if(varIndex < 0)
        {
            fprintf(stderr, "Keywords::INC() : '%s:%d' : syntax error, integer variable '%s' not found : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
            return false;
        }

        switch(int16Byte)
        {
            case Expression::Int16Low:  Compiler::emitVcpuAsm("INC", "_" + Compiler::getIntegerVars()[varIndex]._name,          false); break;
            case Expression::Int16High: Compiler::emitVcpuAsm("INC", "_" + Compiler::getIntegerVars()[varIndex]._name + " + 1", false); break;
            case Expression::Int16Both: Compiler::emitVcpuAsm("INC", "_" + Compiler::getIntegerVars()[varIndex]._name,          false); break;

            default: break;
        }

        return true;
    }

    bool DEC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        // Operand must be an integer var
        std::string varToken = codeLine._code.substr(foundPos);
        Expression::stripWhitespace(varToken);
        int varIndex = Compiler::findVar(varToken, false);
        if(varIndex < 0)
        {
            fprintf(stderr, "Keywords::DEC() : '%s:%d' : syntax error, integer variable '%s' not found : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
            return false;
        }

        Compiler::emitVcpuAsm("LDW",  "_" + Compiler::getIntegerVars()[varIndex]._name, false);
        Compiler::emitVcpuAsm("SUBI", "1", false);
        Compiler::emitVcpuAsm("STW",  "_" + Compiler::getIntegerVars()[varIndex]._name, false);

        return true;
    }

    bool ON(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::string code = codeLine._code;
        Expression::strToUpper(code);
        size_t gotoOffset = code.find("GOTO");
        size_t gosubOffset = code.find("GOSUB");
        if(gotoOffset == std::string::npos  &&  gosubOffset == std::string::npos)
        {
            fprintf(stderr, "Keywords::ON() : '%s:%d' : syntax error : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        size_t gSize = (gotoOffset != std::string::npos) ? 4 : 5;
        size_t gOffset = (gotoOffset != std::string::npos) ? gotoOffset : gosubOffset;

        // Parse ON field
        Expression::Numeric onValue;
        std::string onToken = codeLine._code.substr(foundPos, gOffset - (foundPos + 1));
        if(Compiler::parseExpression(codeLineIndex, onToken, onValue) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::ON() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, onToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "register0", false);

        // Parse labels
        std::vector<size_t> gOffsets;
        std::vector<std::string> gTokens = Expression::tokeniseOffsets(codeLine._code.substr(gOffset + gSize), ',', gOffsets, false);
        if(gTokens.size() < 1)
        {
            fprintf(stderr, "Keywords::ON() : '%s:%d' : syntax error, must have at least one label after GOTO/GOSUB : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Create on goto/gosub label LUT
        Compiler::getCodeLines()[codeLineIndex]._onGotoGosubLut._lut.clear();
        for(int i=0; i<int(gTokens.size()); i++)
        {
            std::string gLabel = gTokens[i];
            Expression::stripWhitespace(gLabel);

            // Optimised gosub has no PUSH, (i.e. leaf function, VBI handlers, ASM code, etc)
            bool usePush = true;
            if(gLabel[0] == '&')
            {
                usePush = false;
                gLabel.erase(0, 1);
            }

            int labelIndex = Compiler::findLabel(gLabel);
            if(labelIndex == -1)
            {
                fprintf(stderr, "Keywords::ON() : '%s:%d' : invalid label %s in slot %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, gLabel.c_str(), i, codeLine._text.c_str());
                Compiler::getCodeLines()[codeLineIndex]._onGotoGosubLut._lut.clear();
                return false;
            }

            // Only ON GOSUB needs a PUSH, (emitted in createVasmCode())
            if(gosubOffset != std::string::npos) Compiler::getLabels()[labelIndex]._gosub = usePush;

            // Create lookup table out of label addresses
            Compiler::getCodeLines()[codeLineIndex]._onGotoGosubLut._lut.push_back(labelIndex);
        }

        // Allocate giga memory for LUT
        int size = int(gTokens.size()) * 2;
        uint16_t address;
        if(!Memory::getFreeRAM(Memory::FitDescending, size, USER_CODE_START, Compiler::getRuntimeStart(), address))
        {
            fprintf(stderr, "Keywords::ON() : '%s:%d' : not enough RAM for onGotoGosub LUT of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, size, codeLine._text.c_str());
            return false;
        }
        Compiler::getCodeLines()[codeLineIndex]._onGotoGosubLut._address = address;
        Compiler::getCodeLines()[codeLineIndex]._onGotoGosubLut._name = "_lut_onAddrs_" + Expression::wordToHexString(address);

        Compiler::emitVcpuAsm("ADDW", "register0", false);
        Compiler::emitVcpuAsm("STW",  "register0", false);
        Compiler::emitVcpuAsm("LDWI", Compiler::getCodeLines()[codeLineIndex]._onGotoGosubLut._name, false);
        Compiler::emitVcpuAsm("ADDW", "register0", false);
        if(Compiler::getArrayIndiciesOne())
        {
            Compiler::emitVcpuAsm("SUBI", "2", false);  // enable this to start at 1 instead of 0
        }
        Compiler::emitVcpuAsm("DEEK", "", false);
        Compiler::emitVcpuAsm("CALL", "giga_vAC", false);

        return true;
    }

    bool GOTO(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Parse labels
        std::vector<size_t> gotoOffsets;
        std::vector<std::string> gotoTokens = Expression::tokeniseOffsets(codeLine._code.substr(foundPos), ',', gotoOffsets, false);
        if(gotoTokens.size() < 1  ||  gotoTokens.size() > 2)
        {
            fprintf(stderr, "Keywords::GOTO() : '%s:%d' : syntax error, must have one or two parameters, e.g. 'GOTO 200' or 'GOTO k+1,default' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Parse GOTO field
        Expression::Numeric gotoValue;
        std::string gotoToken = gotoTokens[0];
        Expression::stripWhitespace(gotoToken);

        bool useBRA = false;
        if(gotoToken[0] == '&')
        {
            useBRA = true;
            gotoToken.erase(0, 1);
        }

        int labelIndex = Compiler::findLabel(gotoToken);
        if(labelIndex == -1)
        {
            if(Expression::isNumber(gotoToken))
            {
                fprintf(stderr, "Keywords::GOTO() : '%s:%d' : numeric label '%s' does not exist : %s\n", codeLine._moduleName.c_str(), codeLineStart, gotoToken.c_str(), codeLine._text.c_str());
                return false;
            }
            if(++_numNumericGotosGosubs > Compiler::getNumNumericLabels())
            {
                fprintf(stderr, "Keywords::GOTO() : '%s:%d' : numeric label '%s' does not exist : %s\n", codeLine._moduleName.c_str(), codeLineStart, gotoToken.c_str(), codeLine._text.c_str());
                return false;
            }

            Compiler::setCreateNumericLabelLut(true);
            if(Compiler::parseExpression(codeLineIndex, gotoToken, gotoValue) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::GOTO() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, gotoToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "numericLabel", false);

            // Default label exists
            if(gotoTokens.size() == 2)
            {
                std::string defaultToken = gotoTokens[1];
                Expression::stripWhitespace(defaultToken);
                labelIndex = Compiler::findLabel(defaultToken);
                if(labelIndex == -1)
                {
                    fprintf(stderr, "Keywords::GOTO() : '%s:%d' : default label does not exist : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                Compiler::emitVcpuAsm("LDWI", "_" + Compiler::getLabels()[labelIndex]._name, false);
            }
            // No default label
            else
            {
                Compiler::emitVcpuAsm("LDI", "0", false);
            }
            Compiler::emitVcpuAsm("STW", "defaultLabel", false);

            // Call gotoNumericLabel
            Compiler::emitVcpuAsm("%GotoNumeric", "", false);

            return true;
        }

        // Within same page, (validation check on same page branch may fail after outputCode(), user will be warned)
        if(useBRA)
        {
            Compiler::emitVcpuAsm("BRA", "_" + gotoToken, false);
        }
        // Long jump
        else
        {
            if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
            {
                Compiler::emitVcpuAsm("CALLI", "_" + gotoToken, false);
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", "_" + gotoToken, false);
                Compiler::emitVcpuAsm("CALL", "giga_vAC",      false);
            }
        }

        return true;
    }

    bool GOSUB(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Parse labels
        std::vector<size_t> gosubOffsets;
        std::vector<std::string> gosubTokens = Expression::tokeniseOffsets(codeLine._code.substr(foundPos), ',', gosubOffsets, false);
        if(gosubTokens.size() < 1  ||  gosubTokens.size() > 2)
        {
            fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : syntax error, must have one or two parameters, e.g. 'GOSUB <label>' or 'GOSUB <expression>, <default label>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        if(gosubTokens[0].size() == 0)
        {
            fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : syntax error, invalid label : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Parse GOSUB field
        Expression::Numeric gosubValue;
        std::string gosubToken = gosubTokens[0];
        Expression::stripWhitespace(gosubToken);

        // Optimised gosub has no PUSH, (i.e. leaf function, VBI handlers, ASM code, etc)
        bool usePush = true;
        if(gosubToken[0] == '&')
        {
            usePush = false;
            gosubToken.erase(0, 1);
        }

        int labelIndex = Compiler::findLabel(gosubToken);
        if(labelIndex == -1)
        {
            if(Expression::isNumber(gosubToken))
            {
                fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : numeric label '%s' does not exist : %s\n", codeLine._moduleName.c_str(), codeLineStart, gosubToken.c_str(), codeLine._text.c_str());
                return false;
            }
            if(++_numNumericGotosGosubs > Compiler::getNumNumericLabels())
            {
                fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : numeric label '%s' does not exist : %s\n", codeLine._moduleName.c_str(), codeLineStart, gosubToken.c_str(), codeLine._text.c_str());
                return false;
            }
            if(!usePush)
            {
                fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : can't use optimised GOSUB with numeric labels : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }

            Compiler::setCreateNumericLabelLut(true);
            if(Compiler::parseExpression(codeLineIndex, gosubToken, gosubValue) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, gosubToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "numericLabel", false);

            // Default label exists
            if(gosubTokens.size() == 2)
            {
                std::string defaultToken = gosubTokens[1];
                Expression::stripWhitespace(defaultToken);
                labelIndex = Compiler::findLabel(defaultToken);
                if(labelIndex == -1)
                {
                    fprintf(stderr, "Keywords::GOSUB() : '%s:%d' : default label does not exist : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                Compiler::getLabels()[labelIndex]._gosub = true;
                Compiler::emitVcpuAsm("LDWI", "_" + Compiler::getLabels()[labelIndex]._name, false);
            }
            // No default label
            else
            {
                Compiler::emitVcpuAsm("LDI", "0", false);
            }
            Compiler::emitVcpuAsm("STW", "defaultLabel", false);

            // Call gosubNumericLabel
            Compiler::emitVcpuAsm("%GosubNumeric", "", false);

            return true;
        }

        // CALL label
        Compiler::getLabels()[labelIndex]._gosub = usePush;

        if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
        {
            Compiler::emitVcpuAsm("CALLI", "_" + gosubToken, false);
        }
        else
        {
            Compiler::emitVcpuAsm("LDWI", "_" + gosubToken, false);
            Compiler::emitVcpuAsm("CALL", "giga_vAC", false);
        }

        return true;
    }

    bool RETURN(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);

        // Use a macro instead of separate "POP" and "RET", otherwise page jumps could be inserted in between the "POP" and "RET" causing mayhem and havoc
        Compiler::emitVcpuAsm("%Return", "", false);

        return true;
    }

    bool RET(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);

        Compiler::emitVcpuAsm("RET", "", false);

        return true;
    }

    bool CLS(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() > 3)
        {
            fprintf(stderr, "Keywords::CLS() : '%s:%d' : syntax error, expected 'CLS INIT' or 'CLS <address>, <optional width>, <optional height>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Expression::Numeric param;
        if(tokens.size() == 1  &&  tokens[0].size())
        {
            std::string token = tokens[0];
            Expression::strToUpper(token);
            Expression::stripWhitespace(token);
            if(token == "INIT")
            {
                Compiler::emitVcpuAsm("%ResetVideoTable", "", false);
            }
            else
            {
                if(Compiler::parseExpression(codeLineIndex, tokens[0], param) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::CLS() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
                    return false;
                }
                if(param._varType == Expression::Number  &&  uint16_t(std::lround(param._value)) < DEFAULT_EXEC_ADDRESS)
                {
                    fprintf(stderr, "Keywords::CLS() : '%s:%d' : address field must be above &h%04x, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, DEFAULT_EXEC_ADDRESS, tokens[0].c_str(), codeLine._text.c_str());
                    return false;
                }
                Compiler::emitVcpuAsm("STW", "clsAddress", false);
                Compiler::emitVcpuAsm("%ClearScreen", "",  false);
            }
        }
        else if(tokens.size() > 1)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[0], param) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::CLS() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "clrAddress", false);
            if(Compiler::parseExpression(codeLineIndex, tokens[1], param) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::CLS() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[1].c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "clrWidth", false); // runtime uses clrWidth in arithmetic, so make sure all of it is valid

            if(tokens.size() == 2)
            {
                Compiler::emitVcpuAsm("LDI", "120", false);
            }
            else
            {
                if(Compiler::parseExpression(codeLineIndex, tokens[2], param) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::CLS() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[2].c_str(), codeLine._text.c_str());
                    return false;
                }
            }

            Compiler::emitVcpuAsm("STW", "clrLines", false); // runtime uses clrLines in arithmetic, so make sure all of it is valid
            Compiler::emitVcpuAsm("%ClearRect", "",  false);
        }
        else
        {
            Compiler::emitVcpuAsm("%ClearVertBlinds", "", false);
        }

        return true;
    }

    bool PRINT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineStart);

        // Parse print tokens
        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ';', false, false);

RESTART_PRINT:
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(tokens[i].size() == 0  ||  Expression::hasOnlyWhiteSpace(tokens[i]))
            {
                tokens.erase(tokens.begin() + i);
                goto RESTART_PRINT;
            }

            Expression::Numeric numeric;
            int varIndex = -1, constIndex = -1, strIndex = -1;
            uint32_t expressionType = Compiler::isExpression(tokens[i], varIndex, constIndex, strIndex);

#if 1
            if((expressionType & Expression::HasStringKeywords)  &&  (expressionType & Expression::HasOptimisedPrint))
            {
                // Prints text on the fly without creating strings
                Expression::setEnableOptimisedPrint(true);
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    Expression::setEnableOptimisedPrint(false);
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                Expression::setEnableOptimisedPrint(false);
            }
#else
            // TODO: Fix this, (checks for syntax errors)
            if((expressionType & Expression::HasStringKeywords))
            {
                if(expressionType & Expression::HasOptimisedPrint)
                {
                    // Prints text on the fly without creating strings
                    Expression::setEnableOptimisedPrint(true);
                    if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                    {
                        Expression::setEnableOptimisedPrint(false);
                        fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                        return false;
                    }
                    Expression::setEnableOptimisedPrint(false);
                }
                // Leading chars before a string function
                else
                {
                    //fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                    //return false;
                }
            }
#endif
            // Arrays are handled as functions
            else if(expressionType & Expression::HasFunctions)
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                if(numeric._varType == Expression::Number)
                {
                    Compiler::emitVcpuAsm("%PrintInt16", Expression::wordToHexString(int16_t(std::lround(numeric._value))), false);
                }
                else
                {
                    Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                    Compiler::emitVcpuAsm("%PrintAcInt16", "", false);
                }
            }
            else if((expressionType & Expression::HasStrVars)  &&  (expressionType & Expression::HasOperators))
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                if(numeric._varType == Expression::Number)
                {
                    Compiler::emitVcpuAsm("%PrintInt16", Expression::wordToHexString(int16_t(std::lround(numeric._value))), false);
                }
                else if(numeric._varType == Expression::Str2Var)
                {
                    Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                    Compiler::emitVcpuAsm("%PrintAcString", "", false);
                }
                else if(numeric._varType == Expression::TmpStrVar)
                {
                    Compiler::emitVcpuAsm("LDWI", Expression::wordToHexString(Compiler::getStrWorkArea()), false);
                    Compiler::emitVcpuAsm("%PrintAcString", "", false);
                }
                else
                {
                    Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                    Compiler::emitVcpuAsm("%PrintAcInt16", "", false);
                }
            }
            else if((expressionType & Expression::HasIntVars)  &&  (expressionType & Expression::HasOperators))
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                if(numeric._varType == Expression::Number)
                {
                    Compiler::emitVcpuAsm("%PrintInt16", Expression::wordToHexString(int16_t(std::lround(numeric._value))), false);
                }
                else
                {
                    if(numeric._varType != Expression::Str2Var)
                    {
                        Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                        Compiler::emitVcpuAsm("%PrintAcInt16", "", false);
                    }
                    // String array with variable index
                    else
                    {
                        Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                        Compiler::emitVcpuAsm("%PrintAcString", "", false);
                    }
                }
            }
            else if(expressionType & Expression::HasIntVars)
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                if(varIndex >= 0)
                {
                    if(numeric._varType != Expression::Str2Var)
                    {
                        switch(numeric._int16Byte)
                        {
                            case Expression::Int16Low:  Compiler::emitVcpuAsm("LD",  "_" + Compiler::getIntegerVars()[varIndex]._name,          false); break;
                            case Expression::Int16High: Compiler::emitVcpuAsm("LD",  "_" + Compiler::getIntegerVars()[varIndex]._name + " + 1", false); break;
                            case Expression::Int16Both: Compiler::emitVcpuAsm("LDW", "_" + Compiler::getIntegerVars()[varIndex]._name,          false); break;

                            default: break;
                        }

                        Compiler::emitVcpuAsm("%PrintAcInt16", "", false);
                    }
                    // String array with variable index
                    else
                    {
                        Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                        Compiler::emitVcpuAsm("%PrintAcString", "", false);
                    }
                }
                else
                {
                    Compiler::emitVcpuAsm("%PrintAcInt16", "", false);
                }
            }
            else if(expressionType & Expression::HasStrVars)
            {
                if(strIndex >= 0)
                {
                    if(Compiler::getStringVars()[strIndex]._varType != Compiler::VarStr2)
                    {
                        std::string strName = Compiler::getStringVars()[strIndex]._name;
                        Compiler::emitVcpuAsm("%PrintString", "_" + strName, false);
                    }
                    // String array with literal index
                    else
                    {
                        if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                        {
                            fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                            return false;
                        }
                        Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                        Compiler::emitVcpuAsm("%PrintAcString", "", false);
                    }
                }
            }
            else if(expressionType & Expression::HasKeywords)
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
                Compiler::emitVcpuAsm("%PrintAcInt16", "", false);
            }
            else if(expressionType & Expression::HasOperators)
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                Compiler::emitVcpuAsm("%PrintInt16", Expression::wordToHexString(int16_t(std::lround(numeric._value))), false);
            }
            else if(expressionType & Expression::HasStrings)
            {
                size_t lquote = tokens[i].find_first_of("\"");
                size_t rquote = tokens[i].find_last_of("\"");
#if 1
                // TODO: Test this thoroughly
                if(lquote > 0)
                {
                    // If there are leading chars that are not whitespace, then syntax error
                    for(size_t j=0; j<lquote; j++)
                    {
                        if(!isspace(tokens[i][j]))
                        {
                            std::string error = tokens[i].substr(0, lquote);
                            fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, error.c_str(), codeLine._text.c_str());
                            return false;
                        }
                    }
                }
                if(rquote < tokens[i].size() - 1)
                {
                    // If there are trailing chars left over and they are not whitespace, then syntax error
                    for(size_t j=rquote+1; j<tokens[i].size(); j++)
                    {
                        if(!isspace(tokens[i][j]))
                        {
                            std::string error = tokens[i].substr(rquote + 1);
                            fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, error.c_str(), codeLine._text.c_str());
                            return false;
                        }
                    }
                }
#endif
                if(lquote != std::string::npos  &&  rquote != std::string::npos)
                {
                    if(rquote == lquote + 1) continue; // skip empty strings
                    std::string str = tokens[i].substr(lquote + 1, rquote - (lquote + 1));

                    // Create string
                    std::string name;
                    uint16_t address;
                    if(Compiler::getOrCreateString(codeLine, codeLineIndex, str, name, address) == -1) return false;

                    // Print string
                    Compiler::emitVcpuAsm("%PrintString", "_" + name, false);
                }
            }
            else if(expressionType == Expression::HasStrConsts  &&  constIndex > -1)
            {
                // Print constant string
                std::string internalName = Compiler::getConstants()[constIndex]._internalName;
                Compiler::emitVcpuAsm("%PrintString", "_" + internalName, false);
            }
            else if(expressionType == Expression::HasIntConsts  &&  constIndex > -1)
            {
                // Print constant int
                int16_t data = Compiler::getConstants()[constIndex]._data;
                Compiler::emitVcpuAsm("%PrintInt16", Expression::wordToHexString(data), false);
            }
            else if(expressionType == Expression::HasNumbers)
            {
                if(!Expression::parse(tokens[i], codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::PRINT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                Compiler::emitVcpuAsm("%PrintInt16", Expression::wordToHexString(int16_t(std::lround(numeric._value))), false);
            }
        }

        // New line
        if(codeLine._code[codeLine._code.size() - 1] != ';'  &&  codeLine._code[codeLine._code.size() - 1] != ',')
        {
            Compiler::emitVcpuAsm("%NewLine", "", false);
        }

        return true;
    }

    bool INPUT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Tokenise string and vars
        std::vector<std::string> strings;
        std::string text = codeLine._code.substr(foundPos);
        Expression::stripNonStringWhitespace(text);
        std::vector<std::string> tokens = Expression::tokenise(text, ',', false, false);
        std::string code = Expression::stripStrings(text, strings, true);
        std::vector<std::string> varTokens = Expression::tokenise(code, ',', false, false);

        if(varTokens.size() < 1  ||  (strings.size() > varTokens.size() + 1))
        {
            fprintf(stderr, "Keywords::INPUT() : '%s:%d' : syntax error, use 'INPUT <heading>, <int/str var0>, <prompt0>, ... <int/str varN>, <promptN>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Print heading string
        bool foundHeadingString = false;
        if(tokens.size()  &&  Expression::isStringValid(tokens[0]))
        {
            size_t lquote = tokens[0].find_first_of("\"");
            size_t rquote = tokens[0].find_last_of("\"");
            if(lquote != std::string::npos  &&  rquote != std::string::npos)
            {
                // Skip empty strings
                if(rquote > lquote + 1)
                {
                    std::string str = tokens[0].substr(lquote + 1, rquote - (lquote + 1));

                    // Create string
                    std::string name;
                    uint16_t address;
                    if(Compiler::getOrCreateString(codeLine, codeLineIndex, str, name, address) == -1) return false;

                    // Print string
                    Compiler::emitVcpuAsm("%PrintString", "_" + name, false);
                    foundHeadingString = true;
                }
            }
        }

        // INPUT vars/strs/types LUTs, (extra 0x0000 delimiter used by VASM runtime)
        std::vector<uint16_t> varsLut(varTokens.size());
        std::vector<uint16_t> strsLut(varTokens.size());
        std::vector<uint16_t> typesLut(varTokens.size() + 1, 0x0000);

        // Loop through vars
        for(int i=0; i<int(varTokens.size()); i++)
        {
            // Int var exists
            bool isStrVar = false;
            int intVar = Compiler::findVar(varTokens[i]);
            if(intVar >= 0)
            {
                varsLut[i] = Compiler::getIntegerVars()[intVar]._address;
                typesLut[i] = Compiler::VarInt16;
                continue;
            }
            // Str var exists
            else
            {
                if(Expression::isStrNameValid(varTokens[i]))
                {
                    isStrVar = true;
                    int strIndex = Compiler::findStr(varTokens[i]);
                    if(strIndex >= 0)
                    {
                        varsLut[i] = Compiler::getStringVars()[strIndex]._address;
                        typesLut[i] = Compiler::VarStr;
                        continue;
                    }
                }
            }

            // Create int var
            int varIndex = 0;
            if(!isStrVar)
            {
                Compiler::createIntVar(varTokens[i], 0, 0, codeLine, codeLineIndex, false, varIndex);
                if(varIndex == -1)
                {
                    fprintf(stderr, "Keywords::INPUT() : '%s:%d' : couldn't create integer var '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varTokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                varsLut[i] = Compiler::getIntegerVars()[varIndex]._address;
                typesLut[i] = Compiler::VarInt16;
            }
            // Create str var
            else
            {
                uint16_t address;
                varIndex = getOrCreateString(codeLine, codeLineIndex, "", varTokens[i], address, USER_STR_SIZE, false);
                if(varIndex == -1)
                {
                    fprintf(stderr, "Keywords::INPUT() : '%s:%d' : couldn't create string var '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varTokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                varsLut[i] = Compiler::getStringVars()[varIndex]._address;
                typesLut[i] = Compiler::VarStr;
            }
        }

        // Loop through strs
        for(int i=0; i<int(varTokens.size()); i++)
        {
            // Create string
            std::string name;
            uint16_t address;
            int index = (foundHeadingString) ? i + 1 : i;
            std::string str = (index < int(strings.size())) ? strings[index] : "\"?\";;";
            size_t fquote = str.find_first_of('"');
            size_t lquote = str.find_last_of('"');

            // Semicolons
            if(str.size() > lquote + 1  &&  str[lquote + 1] != ';') typesLut[i] |= 0x40;
            if(str.size() > lquote + 1  &&  str[lquote + 1] == ';') str.erase(lquote + 1, 1);
            if(str.back() != ';') typesLut[i] |= 0x80;
            if(str.back() == ';') str.erase(str.size() - 1, 1);

            // Text length field
            uint8_t length = USER_STR_SIZE;
            if(str.size() > lquote + 1  &&  isdigit((unsigned char)str[lquote + 1]))
            {
                std::string field = str.substr(lquote + 1);
                if(!Expression::stringToU8(field, length))
                {
                    fprintf(stderr, "Keywords::INPUT() : '%s:%d' : syntax error in text size field of string '%s' of INPUT statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, str.c_str(), codeLine._text.c_str());
                    return false;
                }
                if(length > USER_STR_SIZE)
                {
                    fprintf(stderr, "Keywords::INPUT() : '%s:%d' : text size field > %d of string '%s' of INPUT statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, USER_STR_SIZE, str.c_str(), codeLine._text.c_str());
                    return false;
                }

                str.erase(lquote + 1, field.size());
            }
            typesLut[i] |= (length + 1) << 8; // increment length as INPUT VASM code counts cursor

            // Remove quotes, (remove last quote first)
            str.erase(lquote, 1);
            str.erase(fquote, 1);

            if(Compiler::getOrCreateString(codeLine, codeLineIndex, str, name, address) == -1) return false;
            strsLut[i] = address;
        }

        // Allocate memory for register work area if it hasn't been allocated already
        if(Compiler::getRegWorkArea() == 0x0000)
        {
            uint16_t regAddr;
            if(!Memory::getFreeRAM(Memory::FitDescending, REG_WORK_SIZE, USER_CODE_START, Compiler::getRuntimeStart(), regAddr))
            {
                fprintf(stderr, "Keywords::INPUT() : '%s:%d' : not enough RAM for register work area of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, REG_WORK_SIZE, codeLine._text.c_str());
                return false;
            }
            Compiler::setRegWorkArea(regAddr);
        }

        // INPUT LUTs
        const int lutSize = 3;
        uint16_t lutAddr, varsAddr, strsAddr, typesAddr;
        if(!Memory::getFreeRAM(Memory::FitDescending, lutSize*2, USER_CODE_START, Compiler::getRuntimeStart(), lutAddr))
        {
            fprintf(stderr, "Keywords::INPUT() : '%s:%d' : not enough RAM for INPUT LUT of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, lutSize*2, codeLine._text.c_str());
            return false;
        }
        if(!Memory::getFreeRAM(Memory::FitDescending, int(varsLut.size()*2), USER_CODE_START, Compiler::getRuntimeStart(), varsAddr))
        {
            fprintf(stderr, "Keywords::INPUT() : '%s:%d' : not enough RAM for INPUT Vars LUT of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(varsLut.size()*2), codeLine._text.c_str());
            return false;
        }
        if(!Memory::getFreeRAM(Memory::FitDescending, int(strsLut.size()*2), USER_CODE_START, Compiler::getRuntimeStart(), strsAddr))
        {
            fprintf(stderr, "Keywords::INPUT() : '%s:%d' : not enough RAM for INPUT Strings LUT of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(strsLut.size()*2), codeLine._text.c_str());
            return false;
        }
        if(!Memory::getFreeRAM(Memory::FitDescending, int(typesLut.size()*2), USER_CODE_START, Compiler::getRuntimeStart(), typesAddr))
        {
            fprintf(stderr, "Keywords::INPUT() : '%s:%d' : not enough RAM for INPUT Var Types LUT of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(typesLut.size()*2), codeLine._text.c_str());
            return false;
        }
        Compiler::getCodeLines()[codeLineIndex]._inputLut = {lutAddr, varsAddr, strsAddr, typesAddr, varsLut, strsLut, typesLut}; // save LUT in global codeLine not local copy
        Compiler::emitVcpuAsm("LDWI", Expression::wordToHexString(lutAddr), false);
        Compiler::emitVcpuAsm("%Input", "", false);

        return true;
    }

    bool FOR(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        bool optimise = true;
        bool varStart = false;
        int varIndex, constIndex, strIndex;
        uint32_t expressionType;

        // Parse first line of FOR loop
        std::string code = codeLine._code;
        Expression::strToUpper(code);
        size_t equals, to, step;
        if((equals = code.find("=")) == std::string::npos)
        {
            fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error, missing '=' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // TO uses INC/ADD, UPTO uses ADD, DOWNTO uses DEC/SUB; &TO/&UPTO/&DOWNTO are optimised BRA versions
        Compiler::ForNextType type = Compiler::AutoTo;
        type = (code.find("UPTO")   != std::string::npos) ? Compiler::UpTo   : type;
        type = (code.find("DOWNTO") != std::string::npos) ? Compiler::DownTo : type;
        bool farJump = (code.find("&TO") == std::string::npos)  &&  (code.find("&UPTO") == std::string::npos)  &&  (code.find("&DOWNTO") == std::string::npos);
        if((to = code.find("TO")) == std::string::npos)
        {
            fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error, missing 'TO' or 'DOWNTO' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        step = code.find("STEP");

        // Maximum of 4 nested loops
        if(Compiler::getForNextDataStack().size() == MAX_NESTED_LOOPS)
        {
            fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error, maximum nested loops is 4 : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Nested loops temporary variables
        uint16_t offset = uint16_t(Compiler::getForNextDataStack().size()) * LOOP_VARS_SIZE;
        uint16_t varEnd = LOOP_VAR_START + offset;
        uint16_t varStep = LOOP_VAR_START + offset + sizeof(uint16_t);

        // Adjust 'to' based on length of TO keyword
        int16_t loopStart = 0;
        int toOffset = (farJump) ? 0 : 0 - sizeof('&');
        toOffset = (type == Compiler::UpTo)   ? toOffset - (sizeof("UP")-1)   : toOffset;
        toOffset = (type == Compiler::DownTo) ? toOffset - (sizeof("DOWN")-1) : toOffset;

        // Loop start
        std::string startToken = codeLine._code.substr(equals + sizeof('='), to - (equals + sizeof('=')) + toOffset);
        expressionType = Compiler::isExpression(startToken, varIndex, constIndex, strIndex);
        if((expressionType & Expression::HasIntVars)  ||  (expressionType & Expression::HasKeywords)  ||  (expressionType & Expression::HasFunctions)) varStart = true;

        // Var counter, (create or update if being reused)
        std::string var = codeLine._code.substr(foundPos, equals - foundPos);
        Expression::stripWhitespace(var);
        int varCounter = Compiler::findVar(var);
        (varCounter < 0) ? Compiler::createIntVar(var, loopStart, 0, codeLine, codeLineIndex, false, varCounter) : Compiler::updateIntVar(loopStart, codeLine, varCounter, false);

        // Loop end
        int16_t loopEnd = 0;
        size_t end = (step == std::string::npos) ? codeLine._code.size() : step;
        std::string endToken = codeLine._code.substr(to + sizeof("TO")-1, end - (to + sizeof("TO")-1));
        expressionType = Compiler::isExpression(endToken, varIndex, constIndex, strIndex);
        if((expressionType & Expression::HasIntVars)  ||  (expressionType & Expression::HasKeywords)  ||  (expressionType & Expression::HasFunctions)) optimise = false;

        // Loop step
        int16_t loopStep = 1;
        std::string stepToken;
        if(step != std::string::npos)
        {
            end = codeLine._code.size();
            stepToken = codeLine._code.substr(step + sizeof("STEP")-1, end - (step + sizeof("STEP")-1));
            expressionType = Compiler::isExpression(stepToken, varIndex, constIndex, strIndex);
            if((expressionType & Expression::HasIntVars)  ||  (expressionType & Expression::HasKeywords)  ||  (expressionType & Expression::HasFunctions)) optimise = false;
        }

        Expression::Numeric startNumeric, endNumeric, stepNumeric;
        if(optimise)
        {
            // Parse start
            if(!Expression::parse(startToken, codeLineIndex, startNumeric))
            {
                fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in '%s'\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression());
                return false;
            }
            loopStart = int16_t(std::lround(startNumeric._value));

            // Parse end
            if(!Expression::parse(endToken, codeLineIndex, endNumeric))
            {
                fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in '%s'\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression());
                return false;
            }
            loopEnd = int16_t(std::lround(endNumeric._value));

            // Parse step
            if(stepToken.size())
            {
                if(!Expression::parse(stepToken, codeLineIndex, stepNumeric))
                {
                    fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in '%s'\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression());
                    return false;
                }
                loopStep = int16_t(std::lround(stepNumeric._value));
                if(loopStep < 1  ||  loopStep > 255) optimise = false;
            }

            // Variable start
            if(optimise  &&  varStart  &&  endNumeric._isValid  &&  loopEnd >= 0  &&  loopEnd <= 255)
            {
                if(Compiler::parseExpression(codeLineIndex, startToken, startNumeric) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, startToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                loopStart = int16_t(std::lround(startNumeric._value));
                Compiler::emitVcpuAsm("STW", "_" + Compiler::getIntegerVars()[varCounter]._name, false);
            }
            // 8bit constants
            else if(optimise  &&  startNumeric._isValid  &&  loopStart >= 0  &&  loopStart <= 255  &&  endNumeric._isValid  &&  loopEnd >= 0  &&  loopEnd <= 255)
            {
                Compiler::emitVcpuAsm("LDI", std::to_string(loopStart), false);
                Compiler::emitVcpuAsm("STW", "_" + Compiler::getIntegerVars()[varCounter]._name, false);
            }
            // 16bit constants require variables
            else
            {
                optimise = false;

                (loopStart >= 0  &&  loopStart <= 255) ? Compiler::emitVcpuAsm("LDI", std::to_string(loopStart), false) : Compiler::emitVcpuAsm("LDWI", std::to_string(loopStart), false);
                Compiler::emitVcpuAsm("STW", "_" + Compiler::getIntegerVars()[varCounter]._name, false);
                (loopEnd >= 0  &&  loopEnd <= 255) ? Compiler::emitVcpuAsm("LDI", std::to_string(loopEnd), false) : Compiler::emitVcpuAsm("LDWI", std::to_string(loopEnd), false);
                Compiler::emitVcpuAsm("STW", Expression::byteToHexString(uint8_t(varEnd)), false);
                (loopStep >= 0  &&  loopStep <= 255) ? Compiler::emitVcpuAsm("LDI", std::to_string(loopStep), false) : Compiler::emitVcpuAsm("LDWI", std::to_string(loopStep), false);
                Compiler::emitVcpuAsm("STW", Expression::byteToHexString(uint8_t(varStep)), false);
            }
        }
        else
        {
            // Parse start
            if(Compiler::parseExpression(codeLineIndex, startToken, startNumeric) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, startToken.c_str(), codeLine._text.c_str());
                return false;
            }
            loopStart = int16_t(std::lround(startNumeric._value));
            Compiler::emitVcpuAsm("STW", "_" + Compiler::getIntegerVars()[varCounter]._name, false);

            // Parse end
            if(Compiler::parseExpression(codeLineIndex, endToken, endNumeric) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, endToken.c_str(), codeLine._text.c_str());
                return false;
            }
            loopEnd = int16_t(std::lround(endNumeric._value));
            Compiler::emitVcpuAsm("STW", Expression::byteToHexString(uint8_t(varEnd)), false);

            // Parse step
            if(stepToken.size())
            {
                if(Compiler::parseExpression(codeLineIndex, stepToken, stepNumeric) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::FOR() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, stepToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                loopStep = int16_t(std::lround(stepNumeric._value));
            }
            else
            {
                loopStep = 1;
                Compiler::emitVcpuAsm("LDI", std::to_string(loopStep), false);
            }
            Compiler::emitVcpuAsm("STW", Expression::byteToHexString(uint8_t(varStep)), false);
        }

        // Label and stack
        Compiler::setNextInternalLabel("_next_" + Expression::wordToHexString(Compiler::getVasmPC()));
        Compiler::getForNextDataStack().push({varCounter, Compiler::getNextInternalLabel(), loopEnd, loopStep, varEnd, varStep, type, farJump, optimise, codeLineIndex});

        return true;
    }

    bool NEXT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        if(codeLine._tokens.size() != 2)
        {
            fprintf(stderr, "Keywords::NEXT() : '%s:%d' : syntax error, wrong number of tokens : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::string var = codeLine._code.substr(foundPos);
        int varIndex = Compiler::findVar(codeLine._tokens[1]);
        if(varIndex < 0)
        {
            fprintf(stderr, "Keywords::NEXT() : '%s:%d' : syntax error, bad var : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Pop stack for this nested loop
        if(Compiler::getForNextDataStack().empty())
        {
            fprintf(stderr, "Keywords::NEXT() : '%s:%d' : syntax error, missing FOR statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        Compiler::ForNextData forNextData = Compiler::getForNextDataStack().top();
        Compiler::getForNextDataStack().pop();

        if(varIndex != forNextData._varIndex)
        {
            fprintf(stderr, "Keywords::NEXT() : '%s:%d' : syntax error, wrong var : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::string varName = Compiler::getIntegerVars()[varIndex]._name;
        std::string labName = forNextData._labelName;
        int16_t loopEnd = forNextData._loopEnd;
        int16_t loopStep = forNextData._loopStep;
        uint16_t varEnd = forNextData._varEnd;
        uint16_t varStep = forNextData._varStep;
        Compiler::ForNextType type = forNextData._type;
        bool farJump = forNextData._farJump;
        bool optimise = forNextData._optimise;

        std::string forNextCmd;
        if(optimise)
        {
            if(abs(loopStep) == 1)
            {
                switch(type)
                {
                    case Compiler::AutoTo:
                    {
                        // Can't use INC when counting upto 0 or 255
                        if(loopEnd == 0  ||  loopEnd == 255)
                        {
                            forNextCmd = (farJump) ? "%ForNextFarAdd" : "%ForNextAdd";
                            Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName + " " + std::to_string(loopEnd) + " " + std::to_string(abs(loopStep)), false);
                        }
                        else
                        {
                            forNextCmd = (farJump) ? "%ForNextFarInc" : "%ForNextInc";
                            Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName + " " + std::to_string(loopEnd), false);
                        }
                    }
                    break;

                    case Compiler::UpTo:
                    {
                        forNextCmd = (farJump) ? "%ForNextFarAdd" : "%ForNextAdd";
                        Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName + " " + std::to_string(loopEnd) + " " + std::to_string(abs(loopStep)), false);
                    }
                    break;

                    case Compiler::DownTo:
                    {
                        // Decrement to 0, (if only we had a DJNZ instruction)
                        if(loopEnd == 0)
                        {
                            forNextCmd = (farJump) ? "%ForNextFarDecZero" : "%ForNextDecZero";
                            Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName, false);
                        }
                        else
                        {
                            forNextCmd = (farJump) ? "%ForNextFarDec" : "%ForNextDec";
                            Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName + " " + std::to_string(loopEnd), false);
                        }
                    }
                    break;

                    default: break;
                }
            }
            // Additive/subtractive step
            else
            {
                switch(type)
                {
                    case Compiler::AutoTo:
                    case Compiler::UpTo:   forNextCmd = (farJump) ? "%ForNextFarAdd" : "%ForNextAdd"; break;
                    case Compiler::DownTo: forNextCmd = (farJump) ? "%ForNextFarSub" : "%ForNextSub"; break;

                    default: break;
                }
                Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName + " " + std::to_string(loopEnd) + " " + std::to_string(abs(loopStep)), false);
            }
        }
        // Positive/negative variable step
        else
        {
            switch(type)
            {
                case Compiler::AutoTo:
                case Compiler::UpTo:   forNextCmd = (farJump) ? "%ForNextFarVarAdd" : "%ForNextVarAdd"; break;
                case Compiler::DownTo: forNextCmd = (farJump) ? "%ForNextFarVarSub" : "%ForNextVarSub"; break;

                default: break;
            }
            Compiler::emitVcpuAsm(forNextCmd, "_" + varName + " " + labName + " " + Expression::byteToHexString(uint8_t(varEnd)) + " " + Expression::byteToHexString(uint8_t(varStep)), false);
        }

        return true;
    }

    bool IF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        bool ifElseEndif = false;

        // IF
        std::string code = Compiler::getCodeLines()[codeLineIndex]._code;
        Expression::strToUpper(code);
        size_t offsetIF = code.find("IF");

        // THEN
        code = codeLine._code;
        Expression::strToUpper(code);
        size_t offsetTHEN = code.find("THEN");
        if(offsetTHEN == std::string::npos) ifElseEndif = true;

        // Condition
        Expression::Numeric condition;
        std::string conditionToken = codeLine._code.substr(foundPos, offsetTHEN - foundPos);
        if(Compiler::parseExpression(codeLineIndex, conditionToken, condition) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::IF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, conditionToken.c_str(), codeLine._text.c_str());
            return false;
        }
        if(condition._ccType == Expression::BooleanCC) Compiler::emitVcpuAsm("%JumpFalse", "", false); // Boolean condition requires this extra check
        int jmpIndex = int(Compiler::getCodeLines()[codeLineIndex]._vasm.size()) - 1;

        // Bail early as we assume this is an IF ELSE ENDIF block
        if(ifElseEndif)
        {
            std::stack<Compiler::EndIfData> endIfData; // stores endif label jumps per if/endif pair
            Compiler::getElseIfDataStack().push({jmpIndex, "", codeLineIndex, Compiler::IfBlock, condition._ccType, endIfData});
            return true;
        }

        // Action
        std::string actionToken = Compiler::getCodeLines()[codeLineIndex]._code.substr(offsetIF + offsetTHEN + 4);
        if(actionToken.size() == 0)
        {
            fprintf(stderr, "Keywords::IF() : '%s:%d' : syntax error, missing action in 'IF THEN <action>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        Expression::trimWhitespace(actionToken);
        std::string actionText = Expression::collapseWhitespaceNotStrings(actionToken);

        // Short circuit GOTO if action is a literal constant or a label
        uint16_t res = 0;
        if(Expression::stringToU16(actionText, res)  ||  Compiler::findLabel(actionText) != -1)
        {
            actionText = "GOTO " + actionText;
        }

        // Multi-statements
        int varIndex, strIndex;
        if(Compiler::parseMultiStatements(actionText, codeLineIndex, codeLineStart, varIndex, strIndex) == Compiler::StatementError) return false;

        // Create label on next line of vasm code
        Compiler::setNextInternalLabel("_else_" + Expression::wordToHexString(Compiler::getVasmPC()));
        std::string nextInternalLabel = Compiler::getNextInternalLabel() + " " + std::to_string(Compiler::getNextJumpFalseUniqueId());

        // Update if's jump to this new label
        Compiler::VasmLine* vasm = &Compiler::getCodeLines()[codeLineIndex]._vasm[jmpIndex];
        switch(condition._ccType)
        {
            case Expression::BooleanCC: vasm->_code = "JumpFalse" + std::string(OPCODE_TRUNC_SIZE - (sizeof("JumpFalse")-1), ' ') + nextInternalLabel; break;
            case Expression::NormalCC:  addLabelToJumpCC(Compiler::getCodeLines()[codeLineIndex]._vasm, nextInternalLabel);                            break;
            case Expression::FastCC:    addLabelToJumpCC(Compiler::getCodeLines()[codeLineIndex]._vasm, Compiler::getNextInternalLabel());             break;

            default: break;
        }

        return true;
    }

    bool ELSEIF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Check stack for this IF ELSE ENDIF block
        if(Compiler::getElseIfDataStack().empty())
        {
            fprintf(stderr, "Keywords::ELSEIF() : '%s:%d' : syntax error, missing IF statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::ElseIfData elseIfData = Compiler::getElseIfDataStack().top();
        int jmpIndex = elseIfData._jmpIndex;
        int codeIndex = elseIfData._codeLineIndex;
        Expression::CCType ccType = elseIfData._ccType;
        std::stack<Compiler::EndIfData> endIfData = elseIfData._endIfData; // add to the list of endif label jumps per if/endif pair
        Compiler::getElseIfDataStack().pop();

        if(elseIfData._ifElseEndType != Compiler::IfBlock  &&  elseIfData._ifElseEndType != Compiler::ElseIfBlock)
        {
            fprintf(stderr, "Keywords::ELSEIF() : '%s:%d' : syntax error, ELSEIF follows IF or ELSEIF : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Jump to endif for previous BASIC line
        if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
        {
            Compiler::emitVcpuAsm("CALLI", "CALLI_JUMP", false, codeLineIndex - 1);
            endIfData.push({int(Compiler::getCodeLines()[codeLineIndex - 1]._vasm.size()) - 1, codeLineIndex - 1, ccType});
        }
        else
        {
            // Validator checks for invalid page jumps
            if(ccType == Expression::FastCC)
            {
                Compiler::emitVcpuAsm("BRA", "BRA_JUMP", false, codeLineIndex - 1);
                endIfData.push({int(Compiler::getCodeLines()[codeLineIndex - 1]._vasm.size()) - 1, codeLineIndex - 1, ccType});
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", "LDWI_JUMP", false, codeLineIndex - 1);
                Compiler::emitVcpuAsm("CALL", "giga_vAC", false, codeLineIndex - 1);
                endIfData.push({int(Compiler::getCodeLines()[codeLineIndex - 1]._vasm.size()) - 2, codeLineIndex - 1, ccType});
            }
        }

        // Create label on next line of vasm code
        Compiler::setNextInternalLabel("_elseif_" + Expression::wordToHexString(Compiler::getVasmPC()));
        std::string nextInternalLabel = Compiler::getNextInternalLabel() + " " + std::to_string(Compiler::getNextJumpFalseUniqueId());

        // Update if's jump to this new label
        Compiler::VasmLine* vasm = &Compiler::getCodeLines()[codeIndex]._vasm[jmpIndex];
        switch(ccType)
        {
            case Expression::BooleanCC: vasm->_code = "JumpFalse" + std::string(OPCODE_TRUNC_SIZE - (sizeof("JumpFalse")-1), ' ') + nextInternalLabel; break;
            case Expression::NormalCC:  addLabelToJumpCC(Compiler::getCodeLines()[codeIndex]._vasm, nextInternalLabel);                                break;
            case Expression::FastCC:    addLabelToJumpCC(Compiler::getCodeLines()[codeIndex]._vasm, Compiler::getNextInternalLabel());                 break;

            default: break;
        }

        // Condition
        Expression::Numeric condition;
        std::string conditionToken = codeLine._code.substr(foundPos);
        if(Compiler::parseExpression(codeLineIndex, conditionToken, condition) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::ELSEIF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, conditionToken.c_str(), codeLine._text.c_str());
            return false;
        }
        if(condition._ccType == Expression::BooleanCC) Compiler::emitVcpuAsm("%JumpFalse", "", false); // Boolean condition requires this extra check
        jmpIndex = int(Compiler::getCodeLines()[codeLineIndex]._vasm.size()) - 1;

        Compiler::getElseIfDataStack().push({jmpIndex, "", codeLineIndex, Compiler::ElseIfBlock, condition._ccType, endIfData});

        return true;
    }

    bool ELSE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);

        if(codeLine._tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::ELSE() : '%s:%d' : syntax error, wrong number of tokens : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Check stack for this IF ELSE ENDIF block
        if(Compiler::getElseIfDataStack().empty())
        {
            fprintf(stderr, "Keywords::ELSE() : '%s:%d' : syntax error, missing IF statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::ElseIfData elseIfData = Compiler::getElseIfDataStack().top();
        int jmpIndex = elseIfData._jmpIndex;
        int codeIndex = elseIfData._codeLineIndex;
        Expression::CCType ccType = elseIfData._ccType;
        std::stack<Compiler::EndIfData> endIfData = elseIfData._endIfData; // add to the list of endif label jumps per if/endif pair
        Compiler::getElseIfDataStack().pop();

        // Jump to endif for previous BASIC line
        if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
        {
            Compiler::emitVcpuAsm("CALLI", "CALLI_JUMP", false, codeLineIndex - 1);
            endIfData.push({int(Compiler::getCodeLines()[codeLineIndex - 1]._vasm.size()) - 1, codeLineIndex - 1, ccType});
        }
        else
        {
            // Validator checks for invalid page jumps
            if(ccType == Expression::FastCC)
            {
                Compiler::emitVcpuAsm("BRA", "BRA_JUMP", false, codeLineIndex - 1);
                endIfData.push({int(Compiler::getCodeLines()[codeLineIndex - 1]._vasm.size()) - 1, codeLineIndex - 1, ccType});
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", "LDWI_JUMP", false, codeLineIndex - 1);
                Compiler::emitVcpuAsm("CALL", "giga_vAC", false, codeLineIndex - 1);
                endIfData.push({int(Compiler::getCodeLines()[codeLineIndex - 1]._vasm.size()) - 2, codeLineIndex - 1, ccType});
            }
        }

        // Create label on next line of vasm code
        Compiler::setNextInternalLabel("_else_" + Expression::wordToHexString(Compiler::getVasmPC()));
        std::string nextInternalLabel = Compiler::getNextInternalLabel() + " " + std::to_string(Compiler::getNextJumpFalseUniqueId());

        // Update if's or elseif's jump to this new label
        Compiler::VasmLine* vasm = &Compiler::getCodeLines()[codeIndex]._vasm[jmpIndex];
        switch(ccType)
        {
            case Expression::BooleanCC: vasm->_code = "JumpFalse" + std::string(OPCODE_TRUNC_SIZE - (sizeof("JumpFalse")-1), ' ') + nextInternalLabel; break;
            case Expression::NormalCC:  addLabelToJumpCC(Compiler::getCodeLines()[codeIndex]._vasm, nextInternalLabel);                                break;
            case Expression::FastCC:    addLabelToJumpCC(Compiler::getCodeLines()[codeIndex]._vasm, Compiler::getNextInternalLabel());                 break;

            default: break;
        }

        Compiler::getElseIfDataStack().push({jmpIndex, nextInternalLabel, codeIndex, Compiler::ElseBlock, ccType, endIfData});

        return true;
    }

    bool ENDIF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        if(codeLine._tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::ENDIF() : '%s:%d' : syntax error, wrong number of tokens : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Check stack for this IF ELSE ENDIF block
        if(Compiler::getElseIfDataStack().empty())
        {
            fprintf(stderr, "Keywords::ENDIF() : '%s:%d' : syntax error, missing IF statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::ElseIfData elseIfData = Compiler::getElseIfDataStack().top();
        int jmpIndex = elseIfData._jmpIndex;
        int codeIndex = elseIfData._codeLineIndex;
        Compiler::IfElseEndType ifElseEndType = elseIfData._ifElseEndType;
        Expression::CCType ccType = elseIfData._ccType;
        std::stack<Compiler::EndIfData>& endIfData = elseIfData._endIfData; // get the list of complete endif label jumps per if/endif pair
        Compiler::getElseIfDataStack().pop();

        // Create label on next line of vasm code
        Compiler::setNextInternalLabel("_endif_" + Expression::wordToHexString(Compiler::getVasmPC()));
        std::string nextInternalLabel = Compiler::getNextInternalLabel() + " " + std::to_string(Compiler::getNextJumpFalseUniqueId());

        // Update if's/elseif's jump to this new label
        if(ifElseEndType == Compiler::IfBlock  ||  ifElseEndType == Compiler::ElseIfBlock)
        {
            Compiler::VasmLine* vasm = &Compiler::getCodeLines()[codeIndex]._vasm[jmpIndex];
            switch(ccType)
            {
                case Expression::BooleanCC: vasm->_code = "JumpFalse" + std::string(OPCODE_TRUNC_SIZE - (sizeof("JumpFalse")-1), ' ') + nextInternalLabel; break;
                case Expression::NormalCC:  addLabelToJumpCC(Compiler::getCodeLines()[codeIndex]._vasm, nextInternalLabel);                                break;
                case Expression::FastCC:    addLabelToJumpCC(Compiler::getCodeLines()[codeIndex]._vasm, Compiler::getNextInternalLabel());                 break;

                default: break;
            }
        }

        // Traverse entire list of endif label jumps per if/endif pair and update elseif's and/or else's jumps to endif label
        while(!endIfData.empty())
        {
            codeIndex = endIfData.top()._codeLineIndex;
            jmpIndex = endIfData.top()._jmpIndex;
            ccType = endIfData.top()._ccType;
            Compiler::VasmLine* vasm = &Compiler::getCodeLines()[codeIndex]._vasm[jmpIndex];
            switch(ccType)
            {
                case Expression::BooleanCC:
                {
                    if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
                    {
                        vasm->_code = "CALLI" + std::string(OPCODE_TRUNC_SIZE - (sizeof("CALLI")-1), ' ') + Compiler::getNextInternalLabel();
                    }
                    else
                    {
                        vasm->_code = "LDWI" + std::string(OPCODE_TRUNC_SIZE - (sizeof("LDWI")-1), ' ') + Compiler::getNextInternalLabel();
                    }
                }
                break;

                case Expression::NormalCC:  addLabelToJump(Compiler::getCodeLines()[codeIndex]._vasm, Compiler::getNextInternalLabel()); break;
                case Expression::FastCC:    addLabelToJump(Compiler::getCodeLines()[codeIndex]._vasm, Compiler::getNextInternalLabel()); break;

                default: break;
            }

            endIfData.pop();
        }

        return true;
    }

    bool WHILE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineStart);

        Compiler::setNextInternalLabel("_while_" + Expression::wordToHexString(Compiler::getVasmPC()));
        Compiler::getWhileWendDataStack().push({0, Compiler::getNextInternalLabel(), codeLineIndex, Expression::BooleanCC});

        // Condition
        Expression::Numeric condition;
        std::string conditionToken = codeLine._code.substr(foundPos);
        if(Compiler::parseExpression(codeLineIndex, conditionToken, condition) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::WHILE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, conditionToken.c_str(), codeLine._text.c_str());
            return false;
        }
        if(condition._ccType == Expression::BooleanCC) Compiler::emitVcpuAsm("%JumpFalse", "", false); // Boolean condition requires this extra check
        Compiler::getWhileWendDataStack().top()._jmpIndex = int(Compiler::getCodeLines()[codeLineIndex]._vasm.size()) - 1;
        Compiler::getWhileWendDataStack().top()._ccType = condition._ccType;

        return true;
    }

    bool WEND(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        // Pop stack for this WHILE loop
        if(Compiler::getWhileWendDataStack().empty())
        {
            fprintf(stderr, "Keywords::WEND() : '%s:%d' : syntax error, missing WHILE statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        Compiler::WhileWendData whileWendData = Compiler::getWhileWendDataStack().top();
        Compiler::getWhileWendDataStack().pop();

        // Branch to WHILE and check condition again
        if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
        {
            Compiler::emitVcpuAsm("CALLI", whileWendData._labelName, false);
        }
        else
        {
            // There are no checks to see if this BRA's destination is in the same page, programmer discretion required when using this feature
            if(whileWendData._ccType == Expression::FastCC)
            {
                Compiler::emitVcpuAsm("BRA", whileWendData._labelName, false);
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", whileWendData._labelName, false);
                Compiler::emitVcpuAsm("CALL", "giga_vAC",      false);
            }
        }

        // Branch if condition false to instruction after WEND
        Compiler::setNextInternalLabel("_wend_" + Expression::wordToHexString(Compiler::getVasmPC()));
        Compiler::VasmLine* vasm = &Compiler::getCodeLines()[whileWendData._codeLineIndex]._vasm[whileWendData._jmpIndex];
        switch(whileWendData._ccType)
        {
            case Expression::BooleanCC:
            {
                vasm->_code = "JumpFalse" + std::string(OPCODE_TRUNC_SIZE - (sizeof("JumpFalse")-1), ' ') + Compiler::getNextInternalLabel() + " " +
                                                                                std::to_string(Compiler::getNextJumpFalseUniqueId());
            }
            break;

            case Expression::NormalCC:
            {
                addLabelToJumpCC(Compiler::getCodeLines()[whileWendData._codeLineIndex]._vasm, Compiler::getNextInternalLabel() + " " + std::to_string(Compiler::getNextJumpFalseUniqueId()));
            }
            break;

            case Expression::FastCC:
            {
                addLabelToJumpCC(Compiler::getCodeLines()[whileWendData._codeLineIndex]._vasm, Compiler::getNextInternalLabel());
            }
            break;

            default: break;
        }

        return true;
    }

    bool REPEAT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);

        Compiler::setNextInternalLabel("_repeat_" + Expression::wordToHexString(Compiler::getVasmPC()));
        Compiler::getRepeatUntilDataStack().push({Compiler::getNextInternalLabel(), codeLineIndex});

        return true;
    }

    bool UNTIL(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Pop stack for this REPEAT loop
        if(Compiler::getRepeatUntilDataStack().empty())
        {
            fprintf(stderr, "Keywords::UNTIL() : '%s:%d' : syntax error, missing REPEAT statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        Compiler::RepeatUntilData repeatUntilData = Compiler::getRepeatUntilDataStack().top();
        Compiler::getRepeatUntilDataStack().pop();

        // Condition
        Expression::Numeric condition;
        std::string conditionToken = codeLine._code.substr(foundPos);
        if(Compiler::parseExpression(codeLineIndex, conditionToken, condition) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::UNTIL() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, conditionToken.c_str(), codeLine._text.c_str());
            return false;
        }

        // Branch if condition false to instruction after REPEAT
        switch(condition._ccType)
        {
            case Expression::BooleanCC: Compiler::emitVcpuAsm("%JumpFalse", repeatUntilData._labelName + " " + std::to_string(Compiler::getNextJumpFalseUniqueId()), false);                      break;
            case Expression::NormalCC:  addLabelToJumpCC(Compiler::getCodeLines()[codeLineIndex]._vasm, repeatUntilData._labelName + " " + std::to_string(Compiler::getNextJumpFalseUniqueId())); break;
            case Expression::FastCC:    addLabelToJumpCC(Compiler::getCodeLines()[codeLineIndex]._vasm, repeatUntilData._labelName);                                                              break;

            default: break;
        }

        return true;
    }

    bool FOREVER(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(codeLineIndex);

        // Pop stack for this REPEAT loop
        if(Compiler::getRepeatUntilDataStack().empty())
        {
            fprintf(stderr, "Keywords::FOREVER() : '%s:%d' : syntax error, missing REPEAT statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        Compiler::RepeatUntilData repeatUntilData = Compiler::getRepeatUntilDataStack().top();
        Compiler::getRepeatUntilDataStack().pop();
        std::string gotoLabel = repeatUntilData._labelName;

        // Check for optimised branch
        bool useBRA = (codeLine._tokens[tokenIndex][0] == '&') ? true : false;

        // Within same page, (validation check on same page branch may fail after outputCode(), user will be warned)
        if(useBRA)
        {
            Compiler::emitVcpuAsm("BRA", repeatUntilData._labelName, false);
        }
        // Long jump
        else
        {
            if(Compiler::getCodeRomType() >= Cpu::ROMv5a)
            {
                Compiler::emitVcpuAsm("CALLI", repeatUntilData._labelName, false);
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", repeatUntilData._labelName, false);
                Compiler::emitVcpuAsm("CALL", "giga_vAC",      false);
            }
        }

        return true;
    }

    bool AS(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLineIndex);
        UNREFERENCED_PARAM(codeLine);

        return true;
    }

    bool typeHelper(const std::string& input, Compiler::CodeLine& codeLine, int codeLineStart, std::string& name, uint16_t& address, Compiler::TypeVarType& varType, int& paramNum)
    {
        name = input;
        address = 0x0000;
        Expression::stripWhitespace(name);

        std::string token = input;
        Expression::strToUpper(token);
        size_t dimPos = token.find("DIM ");
        if(dimPos == std::string::npos) return true;

        size_t lbra, rbra;
        token = input.substr(dimPos + 3);
        Expression::stripWhitespace(token);
        if(Expression::findMatchingBrackets(token, 0, lbra, rbra, '(', name, paramNum))
        {
            // Byte
            if(varType == Compiler::Byte  &&  (paramNum >= 1  &&  paramNum <= 3))
            {
                varType = Compiler::TypeVarType(Compiler::ArrayB + paramNum - 1);
            }
            // Word
            if(varType == Compiler::Word  &&  (paramNum >= 1  &&  paramNum <= 3))
            {
                varType = Compiler::TypeVarType(Compiler::ArrayW + paramNum - 1);
            }
            // String
            else if(varType == Compiler::String  &&  paramNum == 1)
            {
                varType = Compiler::ArrayS;
            }
            else
            {
                fprintf(stderr, "Keywords::TYPE() : '%s:%d' : syntax error, 'TYPE' var array does not have the correct number of dimensions : %s'\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }
        }

        return true;
    }
    bool TYPE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ' ', true);
        if(tokens.size() < 3  ||  tokens[1] != "=")
        {
            fprintf(stderr, "Keywords::TYPE() : '%s:%d' : syntax error, 'TYPE' requires the following format 'TYPE <NAME> = <varType1>, ... <varTypeN>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Type name
        std::string typeName = tokens[0];
        Expression::stripWhitespace(typeName);
        std::map<std::string, Compiler::TypeData>& typeDatas = Compiler::getTypeDatas();
        if(typeDatas.find(typeName) != typeDatas.end())
        {
            fprintf(stderr, "Keywords::TYPE() : '%s:%d' : type '%s' already exists : %s\n", codeLine._moduleName.c_str(), codeLineStart, typeName.c_str(), codeLine._text.c_str());
            return false;
        }

        // Variables
        size_t equals = codeLine._code.find_first_of('=');
        std::string vars = codeLine._code.substr(equals + 1);
        tokens = Expression::tokenise(vars, ',', true);
        if(tokens.size() == 0)
        {
            fprintf(stderr, "Keywords::TYPE() : '%s:%d' : missing variable types, 'TYPE' requires the following format 'TYPE <NAME> = <varType1>, ... <varTypeN>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Type data
        Compiler::TypeData typeData;
        for(int i=0; i<int(tokens.size()); i++)
        {
            int paramNum = 0;
            std::string varName;
            uint16_t varAddr = 0x0000;
            Compiler::TypeVarType varType;

            // Byte
            if(tokens[i].find('%') != std::string::npos)
            {
                varType = Compiler::Byte;
                if(!typeHelper(tokens[i], codeLine, codeLineStart, varName, varAddr, varType, paramNum)) return false;
            }
            // String
            else if(tokens[i].find('$') != std::string::npos)
            {
                varType = Compiler::String;
                if(!typeHelper(tokens[i], codeLine, codeLineStart, varName, varAddr, varType, paramNum)) return false;
            }
            // Word
            else
            {
                varType = Compiler::Word;
                if(!typeHelper(tokens[i], codeLine, codeLineStart, varName, varAddr, varType, paramNum)) return false;
            }

            if(typeData._vars.find(varName) != typeData._vars.end())
            {
                fprintf(stderr, "Keywords::TYPE() : '%s:%d' : var '%s' already exists : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }

            typeData._vars[varName] = {varAddr, varType};
        }

        typeDatas[typeName] = typeData;

        //// Constant string array
        //std::string token = tokens[0];
        //Expression::strToUpper(token);
        //size_t dimPos = token.find("DIM ");
        //if(dimPos != std::string::npos)
        //{
        //    size_t strPos = token.find("$", dimPos + 3);
        //    if(strPos != std::string::npos)
        //    {
        //        size_t lbra, rbra;
        //        if(Expression::findMatchingBrackets(token, strPos + 1, lbra, rbra))
        //        {
        //            _constDimStrArray = true;
        //            return DIM(codeLine, codeLineIndex, codeLineStart, tokenIndex, foundPos + dimPos + 3, result);
        //        }
        //    }
        //}

        return true;
    }

    bool callHelper(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, std::string& token, uint16_t localVarsAddr)
    {
        UNREFERENCED_PARAM(codeLine);

        // If valid expression
        Expression::Numeric numeric;
        if(!Expression::parse(token, codeLineIndex, numeric))
        {
            fprintf(stderr, "Keywords::CALL() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
            return false;
        }

        if(numeric._varType == Expression::Number)
        {
            int16_t value = int16_t(std::lround(numeric._value));
            (value >= 0  &&  value <= 255) ? Compiler::emitVcpuAsm("LDI", Expression::byteToHexString(uint8_t(value)), false) : Compiler::emitVcpuAsm("LDWI", Expression::wordToHexString(value), false);
        }
        else if(numeric._varType == Expression::IntVar16)
        {
            switch(numeric._int16Byte)
            {
                case Expression::Int16Low:  Compiler::emitVcpuAsm("LD",  "_" + Compiler::getIntegerVars()[numeric._index]._name,          false); break;
                case Expression::Int16High: Compiler::emitVcpuAsm("LD",  "_" + Compiler::getIntegerVars()[numeric._index]._name + " + 1", false); break;
                case Expression::Int16Both: Compiler::emitVcpuAsm("LDW", "_" + Compiler::getIntegerVars()[numeric._index]._name,          false); break;

                default: break;
            }
        }
        else
        {
            Compiler::emitVcpuAsm("LDW", Expression::byteToHexString(uint8_t(Compiler::getTempVarStart())), false);
        }

        Compiler::emitVcpuAsm("STW", Expression::byteToHexString(uint8_t(localVarsAddr)), false);

        return true;
    }
    bool CALL(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> callTokens = Expression::tokenise(codeLine._code.substr(foundPos + 1), ',', true);
        if(callTokens.size() == 0)
        {
            fprintf(stderr, "Keywords::CALL() : '%s:%d' : syntax error, 'CALL' requires a 'NAME' and optional parameters, 'CALL <NAME>, <param0, param1, ... paramN>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Proc name
        Expression::stripWhitespace(callTokens[0]);
        std::string procName = callTokens[0];

        // Params
        int numParams = int(callTokens.size()) - 1;
        uint16_t localVarsAddr = LOCAL_VAR_START;
        if(callTokens.size() > 1)
        {
            for(int i=1; i<int(callTokens.size()); i++)
            {
                if(localVarsAddr >= TEMP_VAR_START)
                {
                    fprintf(stderr, "Keywords::CALL() : '%s:%d' : syntax error, maximum number of parameters exceeded : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                if(!callHelper(codeLine, codeLineIndex, codeLineStart, callTokens[i], localVarsAddr))
                {
                    return false;
                }

                localVarsAddr += 2;
            }
        }

        // Save for later validation
        Compiler::CallData callData = {numParams, codeLineIndex, procName};
        Compiler::getCallDataMap()[procName] = callData;

        if(Compiler::getCodeRomType() < Cpu::ROMv5a)
        {
            Compiler::emitVcpuAsm("LDWI", "_" + procName, false);
            Compiler::emitVcpuAsm("CALL", "giga_vAC", false);
        }
        else
        {
            Compiler::emitVcpuAsm("CALLI", "_" + procName, false);
        }

        return true;
    }

    bool PROC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> procTokens = Expression::tokenise(codeLine._code.substr(foundPos + 1), ',', true);
        if(procTokens.size() == 0)
        {
            fprintf(stderr, "Keywords::PROC() : '%s:%d' : syntax error, 'PROC' requires a 'NAME' and optional parameters, 'PROC <NAME>, <param0, param1, ... paramN>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(!Compiler::getProcDataStack().empty())
        {
            fprintf(stderr, "Keywords::PROC() : '%s:%d' : syntax error, 'PROC' can NOT be nested : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::ProcData procData;
        uint16_t localVarsAddr = LOCAL_VAR_START;

        // Proc name
        Expression::stripWhitespace(procTokens[0]);
        procData._name = procTokens[0];
        procData._codeLineIndex = codeLineIndex;
        procData._numParams = int(procTokens.size()) - 1;

        // Params
        if(procTokens.size() > 1)
        {
            for(int i=1; i<int(procTokens.size()); i++)
            {
                if(localVarsAddr >= TEMP_VAR_START)
                {
                    fprintf(stderr, "Keywords::PROC() : '%s:%d' : syntax error, maximum number of parameters exceeded : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                Expression::stripWhitespace(procTokens[i]);
                if(Expression::isVarNameValid(procTokens[i]) == Expression::Invalid)
                {
                    fprintf(stderr, "Keywords::PROC() : '%s:%d' : syntax error, parameter types can only be integer : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                int localVarIndex = -1;
                std::string localVarName = procData._name + "_" + procTokens[i];
                Compiler::createProcIntVar(localVarName, 0, 0, codeLine, codeLineIndex, false, localVarsAddr, localVarIndex);
                if(localVarIndex == -1)
                {
                    fprintf(stderr, "Keywords::PROC() : '%s:%d' : can't create local integer var '%s'\n", codeLine._moduleName.c_str(), codeLineStart, localVarName.c_str());
                    return false;
                }

                // Accessing variables within a PROC requires a translation from local var name to source var name
                procData._localVarNameMap[procTokens[i]] = localVarName;

                localVarsAddr += 2;
            }
        }

        Compiler::getProcDataStack().push(procData);
        Compiler::getProcDataMap()[procData._name] = procData;

        Compiler::emitVcpuAsm("PUSH", "", false, codeLineIndex, "_" + procData._name);

        return true;
    }

    bool ENDPROC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        if(Compiler::getProcDataStack().empty())
        {
            fprintf(stderr, "Keywords::ENDPROC() : '%s:%d' : syntax error, missing PROC statement : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(Compiler::getProcDataStack().size() != 1)
        {
            fprintf(stderr, "Keywords::ENDPROC() : '%s:%d' : syntax error, 'PROC' can NOT be nested : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Pop stack for current PROC
        Compiler::getProcDataStack().pop();

        Compiler::emitVcpuAsm("%Return", "", false);

        return true;
    }

    bool LOCAL(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> localTokens = Expression::tokenise(codeLine._code.substr(foundPos + 1), ',', true);
        if(localTokens.size() < 1)
        {
            fprintf(stderr, "Keywords::LOCAL() : '%s:%d' : syntax error, 'LOCAL' requires at least one '<VAR>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(Compiler::getProcDataStack().empty())
        {
            fprintf(stderr, "Keywords::LOCAL() : '%s:%d' : syntax error, 'LOCAL' can only be used within a 'PROC/ENDPROC' pair : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(Compiler::getProcDataStack().size() != 1)
        {
            fprintf(stderr, "Keywords::LOCAL() : '%s:%d' : syntax error, 'LOCAL' can NOT be used in nested 'PROC's' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::ProcData procData = Compiler::getProcDataStack().top();
        procData._numLocals = int(localTokens.size());
        uint16_t localVarsAddr = LOCAL_VAR_START + uint16_t(procData._numParams)*2;

        // Local vars
        for(int i=0; i<int(localTokens.size()); i++)
        {
            if(localVarsAddr >= TEMP_VAR_START)
            {
                fprintf(stderr, "Keywords::LOCAL() : '%s:%d' : error, maximum number of local vars exceeded : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }

            Expression::stripWhitespace(localTokens[i]);
            if(Expression::isVarNameValid(localTokens[i]) == Expression::Invalid)
            {
                fprintf(stderr, "Keywords::LOCAL() : '%s:%d' : syntax error, local var types can only be integer : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }

            int localVarIndex = -1;
            std::string localVarName = procData._name + "_" + localTokens[i];
            Compiler::createProcIntVar(localVarName, 0, 0, codeLine, codeLineIndex, false, localVarsAddr, localVarIndex);
            if(localVarIndex == -1)
            {
                fprintf(stderr, "Keywords::LOCAL() : '%s:%d' : couldn't create local integer var '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, localVarName.c_str(), codeLine._text.c_str());
                return false;
            }

            // Accessing variables within a PROC requires a translation from local var name to source var name
            procData._localVarNameMap[localTokens[i]] = localVarName;

            localVarsAddr += 2;
        }

        Compiler::getProcDataStack().top() = procData;
        Compiler::getProcDataMap()[procData._name] = procData;

        return true;
    }

    bool CONST(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), '=', true);
        if(tokens.size() != 2)
        {
            fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error, use CONST a=50 or CONST a$=\"doggy\" or const dim arr$(2) = \"One\", \"Two\", \"Three\" : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Constant string array, (only string arrays can be constant, byte and integer arrays are excluded)
        std::string token = tokens[0];
        Expression::strToUpper(token);
        size_t dimPos = token.find("DIM ");
        if(dimPos != std::string::npos)
        {
            size_t strPos = token.find("$", dimPos + 3);
            if(strPos != std::string::npos)
            {
                size_t lbra, rbra;
                if(Expression::findMatchingBrackets(token, strPos + 1, lbra, rbra))
                {
                    _constDimStrArray = true;
                    return DIM(codeLine, codeLineIndex, codeLineStart, tokenIndex, foundPos + dimPos + 3, result);
                }
            }
        }

        // Variable string array
        Expression::stripWhitespace(tokens[0]);
        if(Expression::isVarNameValid(tokens[0]) == Expression::Invalid  &&  Expression::isStrNameValid(tokens[0]) == Expression::Invalid)
        {
            fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error, name MUST contain only alphanumerics and '$' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // String
        if(Expression::isStrNameValid(tokens[0]) == Expression::Variable)
        {
            // Strip whitespace
            Expression::stripNonStringWhitespace(tokens[1]);
            if(Expression::isStringValid(tokens[1]))
            {
                uint16_t address;
                std::string internalName;

                // Strip quotes
                tokens[1].erase(0, 1);
                tokens[1].erase(tokens[1].size()-1, 1);

                // Don't count escape char '\'
                int escCount = 0;
                int strLength = int(tokens[1].size());
                for(int i=0; i<strLength; i++)
                {
                    if(tokens[1][i] == '\\') escCount++;
                }
                strLength -= escCount;

                Compiler::getOrCreateString(codeLine, codeLineIndex, tokens[1], internalName, address);
                Compiler::getConstants().push_back({uint8_t(strLength), 0, address, tokens[1], tokens[0], internalName, Compiler::ConstStr});
            }
            // String keyword
            else
            {
                size_t lbra, rbra;
                if(!Expression::findMatchingBrackets(tokens[1], 0, lbra, rbra))
                {
                    fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error, invalid string or keyword : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                std::string funcToken = tokens[1].substr(0, lbra);
                std::string paramToken = tokens[1].substr(lbra + 1, rbra - (lbra + 1));
                Expression::strToUpper(funcToken);
                if(Functions::getStringFunctions().find(funcToken) == Functions::getStringFunctions().end())
                {
                    fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error, invalid string or keyword : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                int16_t param;
                if(!Expression::stringToI16(paramToken, param))
                {
                    fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error, keyword param must be a constant number : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                    return false;
                }

                // Create constant string
                int index;
                uint8_t length = 0;
                uint16_t address = 0x0000;
                if(funcToken == "CHR$")      {length = 1; address = Compiler::getOrCreateConstString(Compiler::StrChar, param, index);}
                else if(funcToken == "HEX$") {length = 4; address = Compiler::getOrCreateConstString(Compiler::StrHex,  param, index);}

                // Create constant
                if(address)
                {
                    std::string internalName = Compiler::getStringVars().back()._name;
                    Compiler::getConstants().push_back({length, 0, address, Compiler::getStringVars().back()._text, tokens[0], internalName, Compiler::ConstStr});
                }
            }
        }
        // Integer
        else
        {
            Expression::Numeric numeric(true); // true = allow static init
            if(!Expression::parse(tokens[1], codeLineIndex, numeric))
            {
                fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                return false;
            }
            if(tokens[1].size() == 0  ||  !numeric._isValid  ||  numeric._varType == Expression::TmpVar  ||  numeric._varType == Expression::IntVar16)
            {
                fprintf(stderr, "Keywords::CONST() : '%s:%d' : syntax error, invalid constant expression : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }

            Compiler::getConstants().push_back({2, int16_t(std::lround(numeric._value)), 0x0000, "", tokens[0], "_" + tokens[0], Compiler::ConstInt16});
        }

        return true;
    }

    bool initDIM(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, std::string& varName, int arrSizeTotal, int16_t& intInit, std::vector<int16_t>& intInits, bool& isInit)
    {
        std::string constName = varName;
        if(Compiler::findConst(constName) >= 0)
        {
            fprintf(stderr, "Keywords::initDIM() : '%s:%d' : const '%s' already exists : %s\n", codeLine._moduleName.c_str(), codeLineStart, varName.c_str(), codeLine._text.c_str());
            return false;
        }
        if(Compiler::findVar(varName) >= 0)
        {
            fprintf(stderr, "Keywords::initDIM() : '%s:%d' : var '%s' already exists : %s\n", codeLine._moduleName.c_str(), codeLineStart, varName.c_str(), codeLine._text.c_str());
            return false;
        }

        // Optional array int init values
        isInit = false;
        size_t equalsPos = codeLine._code.find("=");
        if(equalsPos != std::string::npos)
        {
            std::string initText = codeLine._code.substr(equalsPos + 1);
            Expression::stripWhitespace(initText);
            std::vector<std::string> initTokens = Expression::tokenise(initText, ',', true);
            if(initTokens.size() == 0)
            {
                fprintf(stderr, "Keywords::initDIM() : '%s:%d' : initial value must be a constant, found '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, initText.c_str(), codeLine._text.c_str());
                return false;
            }
            else if(initTokens.size() == 1)
            {
                std::string operand;
                Expression::Numeric numeric(true); // true = allow static init
                if(Compiler::parseStaticExpression(codeLineIndex, initTokens[0], operand, numeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::initDIM() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, initTokens[0].c_str(), codeLine._text.c_str());
                    return false;
                }
                intInit = int16_t(std::lround(numeric._value));
            }
            else if(int(initTokens.size()) > arrSizeTotal)
            {
                fprintf(stderr, "Keywords::initDIM() : '%s:%d' : too many initialisation values for size of array, found %d for a size of %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(initTokens.size()), arrSizeTotal, codeLine._text.c_str());
                return false;
            }

            // Multiple initialisation values, (if there are less init values than array size, then array is padded with last init value)
            std::string operand;
            intInits.resize(initTokens.size());
            std::vector<Expression::Numeric> funcParams(initTokens.size(), Expression::Numeric(true)); // true = allow static init
            for(int i=0; i<int(initTokens.size()); i++)
            {
                if(Compiler::parseStaticExpression(codeLineIndex, initTokens[i], operand, funcParams[i]) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::initDIM() : '%s:%d' : bad initialiser %s at index %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), i, codeLine._text.c_str());
                    return false;
                }
                intInits[i] = int16_t(std::lround(funcParams[i]._value));
            }
            intInit = intInits.back();

            isInit = true;
        }

        return true;
    }
    bool allocDIM(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, uint16_t& address, Compiler::VarType& varType, std::vector<uint16_t>& arrLut, std::vector<uint16_t>& arrSizes, std::vector<std::vector<uint16_t>>& arrAddrs)
    {
        UNREFERENCED_PARAM(codeLineIndex);

        int intSize = 0;
        switch(varType)
        {
            case Compiler::Var1Arr8:  intSize = 1; break;
            case Compiler::Var1Arr16: intSize = 2; break;

            default: fprintf(stderr, "Keywords::allocDIM() : '%s:%d' : unknown var type : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                     break;
        }

        // Allocate memory for k * j * i of intSize byte values
        int iSizeBytes = arrSizes[2] * intSize;
        for(int k=0; k<arrSizes[0]; k++)
        {
            for(int j=0; j<arrSizes[1]; j++)
            {
                if(!Memory::getFreeRAM(Memory::FitDescending, iSizeBytes, USER_CODE_START, Compiler::getArraysStart(), arrAddrs[k][j], false)) // arrays do not need to be contained within pages
                {
                    fprintf(stderr, "Keywords::allocDIM() : '%s:%d' : not enough RAM for int array of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, iSizeBytes, codeLine._text.c_str());
                    return false;
                }
            }
        }

        // 1D array
        if(arrSizes[0] == 1  &&  arrSizes[1] == 1)
        {
            address = arrAddrs[0][0];
            varType = (varType == Compiler::Var1Arr8) ? Compiler::Var1Arr8 : Compiler::Var1Arr16;
        }
        // 2D array
        else if(arrSizes[0] == 1)
        {
            int jSizeBytes = arrSizes[1] * 2;
            if(!Memory::getFreeRAM(Memory::FitDescending, jSizeBytes, USER_CODE_START, Compiler::getArraysStart(), address, false)) // arrays do not need to be contained within pages
            {
                fprintf(stderr, "Keywords::allocDIM() : '%s:%d' : not enough RAM for int array of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, jSizeBytes, codeLine._text.c_str());
                return false;
            }

            address = address;
            varType = (varType == Compiler::Var1Arr8) ? Compiler::Var2Arr8 : Compiler::Var2Arr16;

            // Enable system internal sub intialiser and mark system internal sub to be loaded, (init functions are not needed for ROMv5a and higher as CALLI is able to directly CALL them)
            if(intSize == 1)
            {
                if(Compiler::getCodeRomType() < Cpu::ROMv5a) Compiler::enableSysInitFunc("Init8Array2d");
                Linker::setInternalSubToLoad("convert8Arr2d");
            }
            else if(intSize == 2)
            {
                if(Compiler::getCodeRomType() < Cpu::ROMv5a) Compiler::enableSysInitFunc("Init16Array2d");
                Linker::setInternalSubToLoad("convert16Arr2d");
            }
        }
        // 3D array
        else
        {
            int jSizeBytes = arrSizes[1] * 2;
            for(int k=0; k<arrSizes[0]; k++)
            {
                if(!Memory::getFreeRAM(Memory::FitDescending, jSizeBytes, USER_CODE_START, Compiler::getArraysStart(), arrLut[k], false)) // arrays do not need to be contained within pages
                {
                    fprintf(stderr, "Keywords::allocDIM() : '%s:%d' : not enough RAM for int array of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, jSizeBytes, codeLine._text.c_str());
                    return false;
                }
            }

            int kSizeBytes = arrSizes[2] * 2;
            if(!Memory::getFreeRAM(Memory::FitDescending, kSizeBytes, USER_CODE_START, Compiler::getArraysStart(), address, false)) // arrays do not need to be contained within pages
            {
                fprintf(stderr, "Keywords::allocDIM() : '%s:%d' : not enough RAM for int array of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, kSizeBytes, codeLine._text.c_str());
                return false;
            }

            address = address;
            varType = (varType == Compiler::Var1Arr8) ? Compiler::Var3Arr8 : Compiler::Var3Arr16;

            // Enable system internal sub intialiser and mark system internal sub to be loaded, (init functions are not needed for ROMv5a and higher as CALLI is able to directly CALL them)
            if(intSize == 1)
            {
                if(Compiler::getCodeRomType() < Cpu::ROMv5a) Compiler::enableSysInitFunc("Init8Array3d");
                Linker::setInternalSubToLoad("convert8Arr3d");
            }
            else if(intSize == 2)
            {
                if(Compiler::getCodeRomType() < Cpu::ROMv5a) Compiler::enableSysInitFunc("Init16Array3d");
                Linker::setInternalSubToLoad("convert16Arr3d");
            }
        }

        return true;
    }
    bool DIM(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        size_t lbra, rbra;
        if(!Expression::findMatchingBrackets(codeLine._code, foundPos, lbra, rbra))
        {
            fprintf(stderr, "Keywords::DIM() : '%s:%d' : syntax error in DIM statement, must be DIM <var>(<n1>, <optional n2>) : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Dimensions
        std::vector<uint16_t> arrSizes;
        std::vector<std::string> sizeTokens = Expression::tokenise(codeLine._code.substr(lbra + 1, rbra - (lbra + 1)), ',', true);
        if(sizeTokens.size() > MAX_ARRAY_DIMS)
        {
            fprintf(stderr, "Keywords::DIM() : '%s:%d' : maximum of %d dimensions, found %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, MAX_ARRAY_DIMS, int(sizeTokens.size()), codeLine._text.c_str());
            return false;
        }

        int arrSizeTotal = 1;
        for(int i=0; i<int(sizeTokens.size()); i++)
        {
            int varIndex = -1, constIndex = -1, strIndex = -1;
            std::string sizeToken = sizeTokens[i];
            Expression::stripWhitespace(sizeToken);
            uint32_t expressionType = Compiler::isExpression(sizeToken, varIndex, constIndex, strIndex);

            // Constant dimension
            if((expressionType & Expression::HasIntConsts)  &&  constIndex > -1)
            {
                std::string operand;
                Expression::Numeric numeric(true); // true = allow static init
                if(Compiler::parseStaticExpression(codeLineIndex, sizeToken, operand, numeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DIM() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, sizeToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                int16_t data = int16_t(std::lround(numeric._value));
                arrSizes.push_back(data);
            }
            // Literal dimension
            else
            {
                uint16_t arrSize = 0;
                if(!Expression::stringToU16(sizeToken, arrSize)  ||  arrSize <= 0)
                {
                    fprintf(stderr, "Keywords::DIM() : '%s:%d' : array dimensions must be a positive constant, found '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, sizeToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                arrSizes.push_back(arrSize);
            }

            // Most BASIC's declared 0 to n elements, hence size = n + 1
            if(!Compiler::getArrayIndiciesOne())
            {
                arrSizes.back()++;
            }

            arrSizeTotal *= arrSizes.back();
        }

        std::string varName = codeLine._code.substr(foundPos, lbra - foundPos);
        Expression::stripWhitespace(varName);

        int16_t intInit = 0;
        bool isIntInit = false;
        std::vector<int16_t> intInits;
        std::vector<std::string> strInits;
        Compiler::VarType varType = Compiler::Var1Arr16;

        // Str array
        bool isStrInit = false;
        if(Expression::isStrNameValid(varName))
        {
            if(Compiler::findStr(varName) >= 0)
            {
                fprintf(stderr, "Keywords::DIM() : '%s:%d' : str %s already exists : %s\n", codeLine._moduleName.c_str(), codeLineStart, varName.c_str(), codeLine._text.c_str());
                return false;
            }
            if(arrSizes.size() != 1)
            {
                fprintf(stderr, "Keywords::DIM() : '%s:%d' : require 1 string dimension, found %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(arrSizes.size()), codeLine._text.c_str());
                return false;
            }

            // String array 2nd dimension is always USER_STR_SIZE, (actual arrSizes data structure looks like this, [n][USER_STR_SIZE])
            arrSizes.push_back(USER_STR_SIZE);

            // Optional array str init values
            size_t equalsPos = codeLine._code.find("=");
            if(equalsPos != std::string::npos)
            {
                std::string initText = codeLine._code.substr(equalsPos + 1);
                Expression::stripNonStringWhitespace(initText);
                std::vector<std::string> initTokens = Expression::tokenise(initText, ',', true);
                if(initTokens.size() == 0)
                {
                    fprintf(stderr, "Keywords::DIM() : '%s:%d' : initial value must be a string, found '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, initText.c_str(), codeLine._text.c_str());
                    return false;
                }
                else if(int(initTokens.size()) > arrSizes[0])
                {
                    fprintf(stderr, "Keywords::DIM() : '%s:%d' : too many initialisation strings for size of array, found %d for a size of %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(initTokens.size()), arrSizes[0], codeLine._text.c_str());
                    return false;
                }

                // Multiple initialisation values, (if there are less strings than array size, then array is padded with last string)
                for(int i=0; i<int(initTokens.size()); i++)
                {
                    if(initTokens[i].size() - 2 > USER_STR_SIZE)
                    {
                        fprintf(stderr, "Keywords::DIM() : '%s:%d' : initialisation string '%s' is larger than %d chars : %s\n", codeLine._moduleName.c_str(), codeLineStart, initTokens[i].c_str(), USER_STR_SIZE, codeLine._text.c_str());
                        return false;
                    }

                    if(!Expression::isStringValid(initTokens[i]))
                    {
                        fprintf(stderr, "Keywords::DIM() : '%s:%d' : invalid string initialiser, found '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, initTokens[i].c_str(), codeLine._text.c_str());
                        return false;
                    }

                    // Strip quotes
                    initTokens[i].erase(0, 1);
                    initTokens[i].erase(initTokens[i].size() - 1, 1);
                    strInits.push_back(initTokens[i]);
                }

                isStrInit = true;
            }

            varType = Compiler::VarStr2;
        }
        // Int8 array
        else if(varName.back() == '%')
        {
            // % is only used within DIM keyword
            varName.erase(varName.size()-1, 1);
            if(!initDIM(codeLine, codeLineIndex, codeLineStart, varName, arrSizeTotal, intInit, intInits, isIntInit)) return false;
            varType = Compiler::Var1Arr8;
        }
        // Int16 array
        else
        {
            if(!initDIM(codeLine, codeLineIndex, codeLineStart, varName, arrSizeTotal, intInit, intInits, isIntInit)) return false;
            varType = Compiler::Var1Arr16;
        }

        // Represent 1 or 2 dimensional arrays as a 3 dimensional array with dimensions of 1 for the missing dimensions
        while(arrSizes.size() != 3)
        {
            arrSizes.insert(arrSizes.begin(), 1);
        }

        // Array lut
        std::vector<uint16_t> arrLut;
        arrLut.resize(arrSizes[0]);

        // Array addresses
        std::vector<std::vector<uint16_t>> arrAddrs;
        arrAddrs.resize(arrSizes[0]);
        for(int i=0; i<arrSizes[0]; i++)
        {
            arrAddrs[i].resize(arrSizes[1]);
        }

        uint16_t address = 0x0000;

        switch(varType)
        {
            // Str arrays
            case Compiler::VarStr2:
            {
                std::vector<uint16_t> strAddrs;
                strAddrs.resize(arrSizes[1]);
                if(_constDimStrArray)
                {
                    // Constant array of strings
                    _constDimStrArray = false;
                    if(Compiler::createStringArray(codeLine, codeLineIndex, varName, 0, isStrInit, strInits, strAddrs) == -1) return false;
                }
                else
                {
                    // Variable array of strings
                    if(Compiler::createStringArray(codeLine, codeLineIndex, varName, USER_STR_SIZE, isStrInit, strInits, strAddrs) == -1) return false;
                }
            }
            break;

            // Int8 arrays, (allocDIM returns var type based on arrSizes)
            case Compiler::Var1Arr8:
            {
                int intIndex = -1;
                if(!allocDIM(codeLine, codeLineIndex, codeLineStart, address, varType, arrLut, arrSizes, arrAddrs)) return false;
                Compiler::createArrIntVar(varName, 0, intInit, codeLine, codeLineIndex, false, isIntInit, intIndex, varType, Compiler::Int8, address, arrSizes, intInits, arrAddrs, arrLut);
            }
            break;

            // Int16 arrays, (allocDIM returns var type based on arrSizes)
            case Compiler::Var1Arr16:
            {
                int intIndex = -1;
                if(!allocDIM(codeLine, codeLineIndex, codeLineStart, address, varType, arrLut, arrSizes, arrAddrs)) return false;
                Compiler::createArrIntVar(varName, 0, intInit, codeLine, codeLineIndex, false, isIntInit, intIndex, varType, Compiler::Int16, address, arrSizes, intInits, arrAddrs, arrLut);
            }
            break;

            default: fprintf(stderr, "Keywords::DIM() : '%s:%d' : unknown array type %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, varType, codeLine._text.c_str());
                     return false;
        }

        return true;
    }

    // Not used, implemented as a function, (Compiler::userFunc())
    bool FUNC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);

        size_t lbra, rbra;
        std::string fnText = codeLine._expression;
        Expression::strToUpper(fnText);
        size_t fnPos = fnText.find("FUNC");
        std::string funcText = codeLine._expression.substr(fnPos);
        if(!Expression::findMatchingBrackets(funcText, 0, lbra, rbra))
        {
            fprintf(stderr, "Keywords::FUNC() : '%s:%d' : syntax error, invalid parenthesis in FN : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        funcText = funcText.substr(0, rbra + 1);

        // Name
        std::string name = funcText.substr(sizeof("FUNC")-1, lbra - (sizeof("FUNC")-1));
        Expression::stripWhitespace(name);
        if(Compiler::getDefFunctions().find(name) == Compiler::getDefFunctions().end())
        {
            fprintf(stderr, "Keywords::FUNC() : '%s:%d' : syntax error, FN %s can't be found : %s\n", codeLine._moduleName.c_str(), codeLineStart, name.c_str(), codeLine._text.c_str());
            return false;
        }
        int varIndex = Compiler::findVar(name);
        if(varIndex >= 0)
        {
            fprintf(stderr, "Keywords::FUNC() : '%s:%d' : syntax error, name collision with var %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, name.c_str(), codeLine._text.c_str());
            return false;
        }

        // Params
        std::vector<std::string> params = Expression::tokenise(funcText.substr(lbra + 1, rbra - (lbra + 1)), ',', true);
        if(params.size() == 0)
        {
            fprintf(stderr, "Keywords::FUNC() : '%s:%d' : syntax error, need at least one parameter : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        int paramsSize = int(Compiler::getDefFunctions()[name]._params.size());
        if(paramsSize != int(params.size()))
        {
            fprintf(stderr, "Keywords::FUNC() : '%s:%d' : syntax error, wrong number of parameters, expecting %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, paramsSize, codeLine._text.c_str());
            return false;
        }
        std::string func = Compiler::getDefFunctions()[name]._function;
        for(int i=0; i<int(params.size()); i++)
        {
            Expression::stripWhitespace(params[i]);
            Expression::replaceText(func, Compiler::getDefFunctions()[name]._params[i], params[i]);
        }

        // Replace DEF FN with FUNC
        Expression::replaceText(codeLine._code,       funcText, func);
        Expression::replaceText(codeLine._text,       funcText, func);
        Expression::replaceText(codeLine._expression, funcText, func);
        Expression::replaceText(Compiler::getCodeLines()[codeLineIndex]._code,       funcText, func);
        Expression::replaceText(Compiler::getCodeLines()[codeLineIndex]._text,       funcText, func);
        Expression::replaceText(Compiler::getCodeLines()[codeLineIndex]._expression, funcText, func);
        std::vector<size_t> offsets;
        std::vector<std::string> tokens = Expression::tokeniseLineOffsets(Compiler::getCodeLines()[codeLineIndex]._code, " (),=", offsets);
        codeLine._tokens = tokens;
        codeLine._offsets = offsets;
        Compiler::getCodeLines()[codeLineIndex]._tokens = tokens;
        Compiler::getCodeLines()[codeLineIndex]._offsets = offsets;

        return true;
    }

    bool createDEFFN(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, std::string& defFunc)
    {
        UNREFERENCED_PARAM(codeLineIndex);

        size_t lbra, rbra;
        if(!Expression::findMatchingBrackets(defFunc, 0, lbra, rbra))
        {
            fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, invalid parenthesis : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Name
        if(lbra == 0)
        {
            fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, missing name : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        std::string name = defFunc.substr(0, lbra);
        Expression::stripWhitespace(name);
        Expression::strToUpper(name);
        int varIndex = Compiler::findVar(name);
        if(varIndex >= 0)
        {
            fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, name collision with var %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, name.c_str(), codeLine._text.c_str());
            return false;
        }

        // Function
        size_t equalsPos = defFunc.find("=", rbra + 1);
        if(equalsPos == std::string::npos)
        {
            fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, missing equals sign : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        std::string function = defFunc.substr(equalsPos + 1);
        Expression::trimWhitespace(function);

        // Params
        std::vector<std::string> params = Expression::tokenise(defFunc.substr(lbra + 1, rbra - (lbra + 1)), ',', true);
        if(params.size() == 0)
        {
            fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, need at least one parameter : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        for(int i=0; i<int(params.size()); i++)
        {
            Expression::stripWhitespace(params[i]);
            if(function.find(params[i]) == std::string::npos)
            {
                fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, parameter %s missing from function %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, params[i].c_str(), function.c_str(), codeLine._text.c_str());
                return false;
            }
        }

        Compiler::DefFunction defFunction = {name, function, params};
        if(Compiler::getDefFunctions().find(name) != Compiler::getDefFunctions().end())
        {
            fprintf(stderr, "Keywords::functionDEF() : '%s:%d' : syntax error, DEF FN %s has been defined more than once : %s\n", codeLine._moduleName.c_str(), codeLineStart, name.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::getDefFunctions()[name] = defFunction;

        return true;
    }
    bool DEF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::string defText = codeLine._code.substr(foundPos);
        std::string fnText = defText;
        Expression::strToUpper(fnText);

        // FUNC
        size_t defPos = std::string::npos;
        if((defPos = fnText.find("FN")) != std::string::npos)
        {
            std::string defFunc = defText.substr(defPos + sizeof("FN") - 1);
            return createDEFFN(codeLine, codeLineIndex, codeLineStart, defFunc);
        }

        // Equals
        size_t equalsPos = codeLine._code.find("=");
        if(equalsPos == std::string::npos)
        {
            // Integer var definition list
            std::vector<std::string> varTokens = Expression::tokenise(defText, ',', true);
            if(varTokens.size())
            {
                std::map<std::string, int> varMap;
                for(int i=0; i<int(varTokens.size()); i++)
                {
                    // Check for valid var name
                    Expression::stripWhitespace(varTokens[i]);
                    if(Expression::isVarNameValid(varTokens[i]) == Expression::Invalid)
                    {
                        fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in variable definition '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varTokens[i].c_str(), codeLine._text.c_str());
                        return false;
                    }

                    // Check for duplicate vars
                    if(varMap.find(varTokens[i]) != varMap.end())
                    {
                        fprintf(stderr, "Keywords::DEF() : '%s:%d' : duplicate variable definition '%s : %s'\n", codeLine._moduleName.c_str(), codeLineStart, varTokens[i].c_str(), codeLine._text.c_str());
                        return false;
                    }

                    // Create var, (no vASM code, i.e. uninitialised)
                    varMap[varTokens[i]] = i;
                    int varIndex = -1;
                    Compiler::createIntVar(varTokens[i], 0, 0, codeLine, codeLineIndex, false, varIndex);
                }

                return true;
            }

            fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error, missing equals sign : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Address field
        size_t typePos, lbra, rbra;
        uint16_t address = 0;
        bool foundAddress = false;
        bool foundLutGenerator = false;
        std::string addrText, operand;
        std::vector<std::string> addrTokens;
        Expression::Numeric addrNumeric(true);  // true = allow static init
        if(Expression::findMatchingBrackets(codeLine._code, foundPos, lbra, rbra))
        {
            // Check for LUT generator
            addrText = codeLine._code.substr(lbra + 1, rbra - (lbra + 1));
            addrTokens = Expression::tokenise(addrText, ',', true);
            if(addrTokens.size() >= 4)
            {
                foundLutGenerator = true;
            }

            // Parse address field
            if(addrTokens.size() == 0)
            {
                fprintf(stderr, "Keywords::DEF() : '%s:%d' : address field does not exist, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrText.c_str(), codeLine._text.c_str());
                return false;
            }
            if(Compiler::parseStaticExpression(codeLineIndex, addrTokens[0], operand, addrNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrTokens[0].c_str(), codeLine._text.c_str());
                return false;
            }
            address = uint16_t(std::lround(addrNumeric._value));
            typePos = lbra;
            foundAddress = true;
        }
        else
        {
            typePos = equalsPos;
        }

        // Type field
        std::string typeText = codeLine._code.substr(foundPos, typePos - foundPos);
        Expression::stripWhitespace(typeText);
        Expression::strToUpper(typeText);
        if(typeText != "BYTE"  &&  typeText != "WORD")
        {
            fprintf(stderr, "Keywords::DEF() : '%s:%d' : type field must be either BYTE or WORD, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, typeText.c_str(), codeLine._text.c_str());
            return false;
        }

        // Address offset field
        uint16_t addrOffset = 0;
        if(addrTokens.size() == 2  &&  !foundLutGenerator)
        {
            Expression::Numeric offsetNumeric(true);  // true = allow static init
            if(Compiler::parseStaticExpression(codeLineIndex, addrTokens[1], operand, offsetNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrTokens[1].c_str(), codeLine._text.c_str());
                return false;
            }
            addrOffset = uint16_t(std::lround(offsetNumeric._value));
        }

        // ************************************************************************************************************
        // LUT generator
        if(foundLutGenerator)
        {
            if(addrTokens.size() < 4  ||  addrTokens.size() > 6)
            {
                fprintf(stderr, "Keywords::DEF() : '%s:%d' : LUT generator must have 4 to 6 parameters, '(ADDR, <VAR>, START, STOP, SIZE, <OFFSET>)', (<VAR> and <OFFSET> are optional), found %d : %s\n",
                        codeLine._moduleName.c_str(), codeLineStart, int(addrTokens.size()), codeLine._text.c_str());
                return false;
            }

            for(int i=0; i<int(addrTokens.size()); i++) Expression::stripWhitespace(addrTokens[i]);

            std::string lutGenerator = codeLine._code.substr(equalsPos + 1);
            Expression::stripWhitespace(lutGenerator);
            if(lutGenerator.size() == 0)
            {
                fprintf(stderr, "Keywords::DEF() : '%s:%d' : LUT generator '%s' is invalid : %s\n", codeLine._moduleName.c_str(), codeLineStart, lutGenerator.c_str(), codeLine._text.c_str());
                return false;
            }

            // Parse LUT generator variable
            bool foundVar = false;
            std::string lutGenVar;
            size_t varPos = 0;
            std::vector<size_t> varPositions;
            if(addrTokens.size() >= 5)
            {
                lutGenVar = addrTokens[1];
                if(Expression::isVarNameValid(lutGenVar) == Expression::Variable)
                {
                    foundVar = true;
                    bool foundVarFirstTime = false;

                    for(;;)
                    {
                        varPos = lutGenerator.find(lutGenVar, varPos);
                        if(varPos == std::string::npos)
                        {
                            if(!foundVarFirstTime)
                            {
                                fprintf(stderr, "Keywords::DEF() : '%s:%d' : LUT generator variable '%s' invalid : %s\n", codeLine._moduleName.c_str(), codeLineStart, lutGenVar.c_str(), codeLine._text.c_str());
                                return false;
                            }

                            // Found all occurenced of LUT generator variable
                            break;
                        }
                        lutGenerator.erase(varPos, lutGenVar.size());
                        varPositions.push_back(varPos);
                        foundVarFirstTime = true;
                        varPos++;
                    }
                }
            }

            // Parse LUT generator parameters
            int paramsOffset = (foundVar) ? 2 : 1;
            std::vector<Expression::Numeric> lutGenParams = {Expression::Numeric(true), Expression::Numeric(true), Expression::Numeric(true)}; // true = allow static init
            for(int i=0; i<int(lutGenParams.size()); i++)
            {
                if(Compiler::parseStaticExpression(codeLineIndex, addrTokens[i + paramsOffset], operand, lutGenParams[i]) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrTokens[i + paramsOffset].c_str(), codeLine._text.c_str());
                    return false;
                }
            }
            if(lutGenParams[2]._value <= 0.0)
            {
                fprintf(stderr, "Keywords::DEF() : '%s:%d' : LUT size must be greater than zero, found '%d' : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(lutGenParams[2]._value), codeLine._text.c_str());
                return false;
            }

            // Parse LUT generator address offset, if it exists
            if(addrTokens.size() == 6  ||  (addrTokens.size() == 5  &&  !foundVar))
            {
                Expression::Numeric offsetNumeric;
                if(Compiler::parseStaticExpression(codeLineIndex, addrTokens[3 + paramsOffset], operand, offsetNumeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrTokens[3 + paramsOffset].c_str(), codeLine._text.c_str());
                    return false;
                }
                addrOffset = uint16_t(std::lround(offsetNumeric._value));
            }

            // Evaluate LUT generator
            double start = lutGenParams[0]._value;
            double end = lutGenParams[1]._value;
            double count = fabs(lutGenParams[2]._value);
            double step = (end - start) / count;
            std::vector<int16_t> lutGenData;
            for(double d=start; d<end; d+=step)
            {
                std::string var;
                if(foundVar)
                {
                    // Insert substitute values into var's positions
                    var = std::to_string(d);
                    for(int i=0; i<int(varPositions.size()); i++)
                    {
                        lutGenerator.insert(varPositions[i] + i*var.size(), var);
                    }
                }

                Expression::Numeric lutGenResult = Expression::Numeric(true); // true = allow static init
                if(Compiler::parseStaticExpression(codeLineIndex, lutGenerator, operand, lutGenResult) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, lutGenerator.c_str(), codeLine._text.c_str());
                    return false;
                }
                lutGenData.push_back(int16_t(std::lround(lutGenResult._value)));

                if(foundVar)
                {
                    // Erase every occurence of substitute values
                    for(int i=0; i<int(varPositions.size()); i++)
                    {
                        lutGenerator.erase(varPositions[i], var.size());
                    }
                }
            }

            // Allows for byte allocating and setting of interlaced memory, (audio memory is not allocated, but can still be set)
            if(typeText == "BYTE")
            {
                uint16_t addr = address;
                std::vector<uint8_t> dataBytes(int(count), 0);
                for(int i=0; i<int(dataBytes.size()); i++)
                {
                    dataBytes[i] = uint8_t(lutGenData[i]);
                    if(addrOffset != 0)
                    {
                        if(addr < RAM_AUDIO_START  ||  addr > RAM_AUDIO_END)
                        {
                            if(!Memory::takeFreeRAM(addr, 1)) return false;
                            addr += addrOffset;
                        }
                    }
                }
                if(addrOffset == 0)
                {
                    if(!Memory::takeFreeRAM(address, int(dataBytes.size()))) return false;
                }
                Compiler::getDefDataBytes().push_back({address, addrOffset, dataBytes});
            }
            // Allows for word allocating and setting of interlaced memory, (audio memory is not allocated, but can still be set)
            else if(typeText == "WORD")
            {
                uint16_t addr = address;
                std::vector<int16_t> dataWords(int(count), 0);
                for(int i=0; i<int(dataWords.size()); i++)
                {
                    dataWords[i] = int16_t(lutGenData[i]);
                    if(addrOffset != 0)
                    {
                        if(addr < RAM_AUDIO_START  ||  addr > RAM_AUDIO_END)
                        {
                            if(!Memory::takeFreeRAM(addr, 2)) return false;
                            addr += addrOffset * 2;
                        }
                    }
                }
                if(addrOffset == 0)
                {
                    if(!Memory::takeFreeRAM(address, int(dataWords.size()) * 2)) return false;
                }
                Compiler::getDefDataWords().push_back({address, addrOffset, dataWords});
            }

            return true;
        }

        // ************************************************************************************************************
        // Data fields
        std::vector<std::string> dataTokens = Expression::tokenise(codeLine._code.substr(equalsPos + 1), ',', true);
        if(dataTokens.size() == 0)
        {
            fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error, require at least one data field : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // BYTE data
        if(typeText == "BYTE")
        {
            // Parse byte fields
            std::vector<uint8_t> dataBytes;
            for(int i=0; i<int(dataTokens.size()); i++)
            {
                Expression::Numeric numeric(true); // true = allow static init
                if(Compiler::parseStaticExpression(codeLineIndex, dataTokens[i], operand, numeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, dataTokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                dataBytes.push_back(uint8_t(std::lround(numeric._value)));
            }

            // If no new address is found, then it is recalculated
            if(!foundAddress)
            {
                addrOffset = Compiler::getDefDataBytes().back()._offset;
                uint16_t offset = (addrOffset != 0) ? uint16_t(Compiler::getDefDataBytes().back()._data.size()) * addrOffset : uint16_t(Compiler::getDefDataBytes().back()._data.size());
                address = Compiler::getDefDataBytes().back()._address + offset;
            }

            // No address offset so take memory as one chunk
            if(addrOffset == 0)
            {
                if(address >= DEFAULT_EXEC_ADDRESS  &&  !Memory::takeFreeRAM(address, int(dataBytes.size())))
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : memory error, byte chunk allocation at '0x%04x of size '%d' failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, address, int(dataBytes.size()), codeLine._text.c_str());
                    return false;
                }
            }
            // Valid address offset, so take memory as individual bytes
            else
            {
                uint16_t addr = address;
                for(int i=0; i<int(dataBytes.size()); i++)
                {
                    if(addr < RAM_AUDIO_START  ||  addr > RAM_AUDIO_END)
                    {
                        if(!Memory::takeFreeRAM(addr, 1))
                        {
                            fprintf(stderr, "Keywords::DEF() : '%s:%d' : memory error, byte allocation at '0x%04x of size '1' failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, address, codeLine._text.c_str());
                            return false;
                        }
                        addr += addrOffset;
                    }
                }
            }
            Compiler::getDefDataBytes().push_back({address, addrOffset, dataBytes});
        }
        // WORD data
        else if(typeText == "WORD")
        {
            // Parse word fields
            std::vector<int16_t> dataWords;
            for(int i=0; i<int(dataTokens.size()); i++)
            {
                Expression::Numeric numeric(true); // true = allow static init
                if(Compiler::parseStaticExpression(codeLineIndex, dataTokens[i], operand, numeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, dataTokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                dataWords.push_back(int16_t(std::lround(numeric._value)));
            }

            // If no new address is found, then it is recalculated
            if(!foundAddress)
            {
                addrOffset = Compiler::getDefDataWords().back()._offset;
                uint16_t offset = (addrOffset != 0) ? uint16_t(Compiler::getDefDataWords().back()._data.size()) * addrOffset * 2 : uint16_t(Compiler::getDefDataWords().back()._data.size()) * 2;
                address = Compiler::getDefDataWords().back()._address + offset;
            }

            // No address offset so take memory as one chunk
            if(addrOffset == 0)
            {
                if(address >= DEFAULT_EXEC_ADDRESS  &&  !Memory::takeFreeRAM(address, int(dataWords.size()) * 2))
                {
                    fprintf(stderr, "Keywords::DEF() : '%s:%d' : memory error, word chunk allocation at '0x%04x of size '%d' failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, address, int(dataWords.size()) * 2, codeLine._text.c_str());
                    return false;
                }
            }
            // Valid address offset, so take memory as individual words
            else
            {
                uint16_t addr = address;
                for(int i=0; i<int(dataWords.size()); i++)
                {
                    if(addr < RAM_AUDIO_START  ||  addr > RAM_AUDIO_END)
                    {
                        if(!Memory::takeFreeRAM(addr, 2))
                        {
                            fprintf(stderr, "Keywords::DEF() : '%s:%d' : memory error, word allocation at '0x%04x of size '2' failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, address, codeLine._text.c_str());
                            return false;
                        }
                        addr += addrOffset * 2;
                    }
                }
            }
            Compiler::getDefDataWords().push_back({address, addrOffset, dataWords});
        }

        return true;
    }

    bool DATA(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Data fields
        std::vector<std::string> dataTokens = Expression::tokenise(codeLine._code.substr(foundPos + 1), ',', true);
        if(dataTokens.size() == 0)
        {
            fprintf(stderr, "Keywords::DATA() : '%s:%d' : syntax error, require at least one data field : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Parse data
        std::string operand;
        for(int i=0; i<int(dataTokens.size()); i++)
        {
            std::string dataToken = dataTokens[i];
            Expression::stripNonStringWhitespace(dataToken);
            if(Expression::isStringValid(dataToken))
            {
                // Strip quotes
                dataToken.erase(0, 1);
                dataToken.erase(dataToken.size()-1, 1);

                // Add str to list
                std::unique_ptr<Compiler::DataObject> pObject = std::make_unique<Compiler::DataStr>(dataToken);
                Compiler::getDataObjects().push_back(std::move(pObject));
            }
            else
            {
                // Parse and add int to list, (ints can be constants, complex expressions or functions that return statics, hence the parsing)
                Expression::Numeric numeric(true); // true = allow static init
                if(Compiler::parseStaticExpression(codeLineIndex, dataTokens[i], operand, numeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::DATA() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, dataTokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                int16_t data = int16_t(std::lround(numeric._value));
                std::unique_ptr<Compiler::DataObject> pObject = std::make_unique<Compiler::DataInt>(data);
                Compiler::getDataObjects().push_back(std::move(pObject));
            }
        }

        return true;
    }

    bool READ(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1)
        {
            fprintf(stderr, "Keywords::READ() : '%s:%d' : syntax error, use 'READ <var0, var1, var2...varN>', requires at least one variable : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Search for variables
        for(int i=0; i<int(tokens.size()); i++)
        {
            std::string varToken = tokens[i];
            Expression::stripWhitespace(varToken);

            int varIndex = Compiler::findVar(varToken);
            int strIndex = Compiler::findStr(varToken);

            size_t lbra, rbra;
            std::string arrText;
            Compiler::VarType varType = Compiler::VarInt16;
            if(varToken.find("$") != std::string::npos) varType = Compiler::VarStr;
            if(Expression::findMatchingBrackets(tokens[i], 0, lbra, rbra))
            {
                arrText = tokens[i].substr(lbra + 1, rbra - (lbra + 1));

                switch(varType)
                {
                    case Compiler::VarInt16: varType = Compiler::Var1Arr16; break;
                    case Compiler::VarStr:   varType = Compiler::VarStr2;   break;

                    default:
                    {
                        fprintf(stderr, "Keywords::READ() : '%s:%d' : syntax error, only single dimensional arrays are supported in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                        return false;
                    }
                    break;
                }
            }

            // Int array must have already been dimensioned
            if(varIndex == -1  &&  varType == Compiler::Var1Arr16)
            {
                fprintf(stderr, "Keywords::READ() : '%s:%d' : integer array is not dimensioned in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                return false;
            }

            // Str array must have already been dimensioned
            if(strIndex == -1  &&  varType == Compiler::VarStr2)
            {
                fprintf(stderr, "Keywords::READ() : '%s:%d' : string array is not dimensioned in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                return false;
            }

            // Create int var
            if(varIndex == -1  &&  varType == Compiler::VarInt16)
            {
                Compiler::createIntVar(varToken, 0, 0, codeLine, codeLineIndex, false, varIndex);
                if(varIndex == -1)
                {
                    fprintf(stderr, "Keywords::READ() : '%s:%d' : couldn't create integer var '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                    return false;
                }
            }
            // Create str var
            else if(strIndex == -1  &&  varType == Compiler::VarStr)
            {
                uint16_t address;
                strIndex = getOrCreateString(codeLine, codeLineIndex, "", varToken, address, USER_STR_SIZE, false);
                if(strIndex == -1)
                {
                    fprintf(stderr, "Keywords::READ() : '%s:%d' : couldn't create string var '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                    return false;
                }
            }

            // Copy read into int var
            if(varIndex >= 0)
            {
                Compiler::emitVcpuAsm("%ReadIntVar", "", false);
                Compiler::emitVcpuAsm("DEEK", "", false);

                if(varType == Compiler::VarInt16)
                {
                    Compiler::emitVcpuAsm("STW", "_" + Compiler::getIntegerVars()[varIndex]._name, false);
                }
                else if(varType == Compiler::Var1Arr16)
                {
                    Compiler::writeArrayVarNoAssign(codeLine, codeLineIndex, varIndex);
                }
                else
                {
                    fprintf(stderr, "Keywords::READ() : '%s:%d' : integer var '%s' is invalid : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                    return false;
                }
            }
            // Copy read into str var
            else if(strIndex >= 0)
            {
                if(varType == Compiler::VarStr)
                {
                    Compiler::emitVcpuAsm("LDWI", "_" + Compiler::getStringVars()[strIndex]._name, false);
                    Compiler::emitVcpuAsm("STW", "strDstAddr", false);
                    Compiler::emitVcpuAsm("%ReadStrVar", "", false);
                }
                else if(varType == Compiler::VarStr2)
                {
                    Compiler::writeArrayStrNoAssign(arrText, codeLineIndex, strIndex);
                    Compiler::emitVcpuAsm("STW", "strDstAddr", false);
                    Compiler::emitVcpuAsm("%ReadStrVar", "", false);
                }
                else
                {
                    fprintf(stderr, "Keywords::READ() : '%s:%d' : string var '%s' is invalid : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                    return false;
                }
            }
            else
            {
                fprintf(stderr, "Keywords::READ() : '%s:%d' : var '%s' is invalid : %s\n", codeLine._moduleName.c_str(), codeLineStart, varToken.c_str(), codeLine._text.c_str());
                return false;
            }
        }

        return true;
    }

    bool RESTORE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() > 1)
        {
            fprintf(stderr, "Keywords::RESTORE() : '%s:%d' : syntax error, use 'RESTORE <optional index>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::emitVcpuAsm("LDWI", "_dataIndex_", false);
        Compiler::emitVcpuAsm("STW", "memAddr",      false);

        if(tokens.size() == 1)
        {
            Expression::Numeric numeric;
            if(Compiler::parseExpression(codeLineIndex, tokens[0], numeric) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::RESTORE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
                return false;
            }
        }
        else
        {
            Compiler::emitVcpuAsm("LDI", "0", false);
        }

        Compiler::emitVcpuAsm("DOKE", "memAddr", false);

        return true;
    }

    bool ALLOC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  &&  tokens.size() > 4)
        {
            fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : syntax error, use 'ALLOC <address>, <optional size>, <optional count>, <optional offset=0x0100>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        int count = 1;
        uint16_t address, end, size = 0x0000, offset = 0x0100;
        std::string addrOperand, sizeOperand, countOperand, offsetOperand;
        Expression::Numeric addrNumeric(true), sizeNumeric(true), countNumeric(true), offsetNumeric(true);
        if(Compiler::parseStaticExpression(codeLineIndex, tokens[0], addrOperand, addrNumeric) == Compiler::OperandInvalid)
        {
            fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        if(tokens.size() >= 2)
        {
            if(Compiler::parseStaticExpression(codeLineIndex, tokens[1], sizeOperand, sizeNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[1].c_str(), codeLine._text.c_str());
                return false;
            }
            size = uint16_t(std::lround(sizeNumeric._value));
        }
        if(tokens.size() >= 3)
        {
            if(Compiler::parseStaticExpression(codeLineIndex, tokens[2], countOperand, countNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[2].c_str(), codeLine._text.c_str());
                return false;
            }
            count = std::lround(countNumeric._value);
        }
        if(tokens.size() >= 4)
        {
            if(Compiler::parseStaticExpression(codeLineIndex, tokens[3], offsetOperand, offsetNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[3].c_str(), codeLine._text.c_str());
                return false;
            }
            offset = uint16_t(std::lround(offsetNumeric._value));
            if(count == 0  ||  offset == 0)
            {
                fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : count and offset must both be non zero, found %d and 0x%04x : %s\n", codeLine._moduleName.c_str(), codeLineStart, count, offset, codeLine._text.c_str());
                return false;
            }
        }

        address = uint16_t(std::lround(addrNumeric._value));
        if(address < DEFAULT_EXEC_ADDRESS)
        {
            fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : address field must be above &h%04x, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, DEFAULT_EXEC_ADDRESS, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }

        end = (size == 0) ? 0xFFFF : address + size;
        for(int i=0; i<count; i++)
        {
            //fprintf(stderr, "0x%04x 0x%04x %d\n", address, end, end - address);
            for(uint16_t j=address; j<end; j++)
            {
                if(!Memory::takeFreeRAM(j, 1, false))
                {
                    fprintf(stderr, "Keywords::ALLOC() : '%s:%d' : trying to allocate already allocated memory at 0x%04x : %s\n", codeLine._moduleName.c_str(), codeLineStart, j, codeLine._text.c_str());
                    return false;
                }
            }
            address += offset;
            end += offset;
        }

        //Memory::printFreeRamList(Memory::NoSort);

        return true;
    }

    bool FREE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 2)
        {
            fprintf(stderr, "Keywords::FREE() : '%s:%d' : syntax error, use 'FREE <address>, <size> or FREE STRINGWORKAREA' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Free string work area
        if(tokens.size() == 1)
        {
            std::string token = Expression::strUpper(tokens[0]);
            Expression::stripWhitespace(token);
            if(token != "STRINGWORKAREA")
            {
                fprintf(stderr, "Keywords::FREE() : '%s:%d' : syntax error, expecting 'STRINGWORKAREA', found '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, token.c_str(), codeLine._text.c_str());
                return false;
            }

            for(int i=0; i<NUM_STR_WORK_AREAS; i++)
            {
                if(!Memory::giveFreeRAM(Compiler::getStrWorkArea(i), USER_STR_SIZE + 2))
                {
                    fprintf(stderr, "Keywords::FREE() : '%s:%d' : RAM at '0x%04x' is already free : %s\n", codeLine._moduleName.c_str(), codeLineStart, Compiler::getStrWorkArea(i), codeLine._text.c_str());
                    return false;
                }
            }

            return true;
        }

        Expression::Numeric addrNumeric(true), sizeNumeric(true);  // true = allow static init
        std::string addrOperand, sizeOperand;
        if(Compiler::parseStaticExpression(codeLineIndex, tokens[0], addrOperand, addrNumeric) == Compiler::OperandInvalid)
        {
            fprintf(stderr, "Keywords::FREE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        if(Compiler::parseStaticExpression(codeLineIndex, tokens[1], sizeOperand, sizeNumeric) == Compiler::OperandInvalid)
        {
            fprintf(stderr, "Keywords::FREE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[1].c_str(), codeLine._text.c_str());
            return false;
        }
        uint16_t address = uint16_t(std::lround(addrNumeric._value));
        uint16_t size = uint16_t(std::lround(sizeNumeric._value));

        //Memory::printFreeRamList(Memory::NoSort);
        if(!Memory::giveFreeRAM(address, size))
        {
            fprintf(stderr, "Keywords::FREE() : '%s:%d' : RAM at '0x%04x' is already free : %s\n", codeLine._moduleName.c_str(), codeLineStart, address, codeLine._text.c_str());
            return false;
        }
        //Memory::printFreeRamList(Memory::NoSort);

        return true;
    }

    bool AT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 2)
        {
            fprintf(stderr, "Keywords::AT() : '%s:%d' : syntax error, use 'AT <x>' or 'AT <x>, <y>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Expression::Numeric numeric;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], numeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::AT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("ST", "cursorXY", false);

        if(tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[1], numeric) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::AT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[1].c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "cursorXY + 1", false);
        }

        Compiler::emitVcpuAsm("%AtTextCursor", "", false);
        return true;
    }

    bool PUT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::PUT() : '%s:%d' : syntax error, use 'PUT <ascii>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Expression::Numeric numeric;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], numeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::PUT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("%PrintAcChr", "", false);

        return true;
    }

    bool MODE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        if(Compiler::getCodeRomType() < Cpu::ROMv2)
        {
            std::string romTypeStr;
            getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
            fprintf(stderr, "Keywords::MODE() : '%s:%d' : version error, 'MODE' requires ROMv2 or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
            return false;
        }

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::MODE() : '%s:%d' : syntax error, use 'MODE <0 - 3>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Expression::Numeric numeric;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], numeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::MODE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "graphicsMode", false);
        Compiler::emitVcpuAsm("%ScanlineMode", "",   false);

        return true;
    }

    bool WAIT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() > 1)
        {
            fprintf(stderr, "Keywords::WAIT() : '%s:%d' : syntax error, use 'WAIT <optional vblank count>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(tokens.size() == 0)
        {
            Compiler::emitVcpuAsm("%WaitVBlank", "", false);
            return true;
        }

        Expression::Numeric numeric;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], numeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::WAIT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "waitVBlankNum", false);
        Compiler::emitVcpuAsm("%WaitVBlanks", "",     false);

        return true;
    }

    bool PSET(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 2  ||  tokens.size() > 3)
        {
            fprintf(stderr, "Keywords::PSET() : '%s:%d' : syntax error, use 'PSET <x>, <y>' or 'PSET <x>, <y>, <colour>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Expression::Numeric numeric;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], numeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::PSET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("ST", "drawPixel_xy", false);
        if(Compiler::parseExpression(codeLineIndex, tokens[1], numeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::PSET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[1].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("ST", "drawPixel_xy + 1", false);

        if(tokens.size() == 3)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[2], numeric) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::PSET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[2].c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "fgbgColour + 1", false);
        }

        Compiler::emitVcpuAsm("%DrawPixel", "", false);
        return true;
    }

    bool LINE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 2  &&  tokens.size() != 4)
        {
            fprintf(stderr, "Keywords::LINE() : '%s:%d' : syntax error, use 'LINE <x>, <y>' or 'LINE <x1>, <y1>, <x2>, <y2>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(tokens.size() == 2)
        {
            std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric()};
            for(int i=0; i<int(tokens.size()); i++)
            {
                if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::LINE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                switch(i)
                {
                    case 0: Compiler::emitVcpuAsm("STW", "drawLine_x2", false); break;
                    case 1: Compiler::emitVcpuAsm("STW", "drawLine_y2", false); break;

                    default: break;
                }
            }

            Compiler::emitVcpuAsm("%AtLineCursor", "", false);
            Compiler::emitVcpuAsm("%DrawVTLine",   "", false);
        }
        else
        {
            std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
            for(int i=0; i<int(tokens.size()); i++)
            {
                if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::LINE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                    return false;
                }
                switch(i)
                {
                    case 0: Compiler::emitVcpuAsm("STW", "drawLine_x1", false); break;
                    case 1: Compiler::emitVcpuAsm("STW", "drawLine_y1", false); break;
                    case 2: Compiler::emitVcpuAsm("STW", "drawLine_x2", false); break;
                    case 3: Compiler::emitVcpuAsm("STW", "drawLine_y2", false); break;

                    default: break;
                }
            }

            Compiler::emitVcpuAsm("%DrawLine", "", false);
        }

        return true;
    }

    bool HLINE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::HLINE() : '%s:%d' : syntax error, use 'HLINE <x1>, <y>, <x2>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::HLINE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
            switch(i)
            {
                case 0: Compiler::emitVcpuAsm("STW", "drawHLine_x1", false); break;
                case 1: Compiler::emitVcpuAsm("STW", "drawHLine_y1", false); break;
                case 2: Compiler::emitVcpuAsm("STW", "drawHLine_x2", false); break;

                default: break;
            }
        }

        Compiler::emitVcpuAsm("%DrawHLine", "", false);

        return true;
    }

    bool VLINE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::VLINE() : '%s:%d' : syntax error, use 'VLINE <x>, <y1>, <y2>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::VLINE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
            switch(i)
            {
                case 0: Compiler::emitVcpuAsm("STW", "drawVLine_x1", false); break;
                case 1: Compiler::emitVcpuAsm("STW", "drawVLine_y1", false); break;
                case 2: Compiler::emitVcpuAsm("STW", "drawVLine_y2", false); break;

                default: break;
            }
        }

        Compiler::emitVcpuAsm("%DrawVLine", "", false);

        return true;
    }

    bool CIRCLE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::CIRCLE() : '%s:%d' : syntax error, use 'CIRCLE <x>, <y>, <radius>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::CIRCLE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
            switch(i)
            {
                case 0: Compiler::emitVcpuAsm("STW",  "drawCircle_cx", false);                                           break;
                case 1: Compiler::emitVcpuAsm("ADDI", "8", false); Compiler::emitVcpuAsm("STW", "drawCircle_cy", false); break;
                case 2: Compiler::emitVcpuAsm("STW",  "drawCircle_r",  false);                                           break;

                default: break;
            }
        }

        Compiler::emitVcpuAsm("%DrawCircle", "", false);

        return true;
    }

    bool CIRCLEF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::CIRCLEF() : '%s:%d' : syntax error, use 'CIRCLEF <x>, <y>, <radius>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::CIRCLEF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
            switch(i)
            {
                case 0: Compiler::emitVcpuAsm("STW", "drawCircleF_cx", false); break;
                case 1: Compiler::emitVcpuAsm("STW", "drawCircleF_cy", false); break;
                case 2: Compiler::emitVcpuAsm("STW", "drawCircleF_r",  false); break;

                default: break;
            }
        }

        Compiler::emitVcpuAsm("%DrawCircleF", "", false);

        return true;
    }

    bool RECT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 4)
        {
            fprintf(stderr, "Keywords::RECT() : '%s:%d' : syntax error, use 'RECT <x1>, <y1>, <x2>, <y2>'' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::RECT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
            switch(i)
            {
                case 0: Compiler::emitVcpuAsm("STW", "drawRect_x1", false); break;
                case 1: Compiler::emitVcpuAsm("STW", "drawRect_y1", false); break;
                case 2: Compiler::emitVcpuAsm("STW", "drawRect_x2", false); break;
                case 3: Compiler::emitVcpuAsm("STW", "drawRect_y2", false); break;

                default: break;
            }
        }

        Compiler::emitVcpuAsm("%DrawRect", "", false);

        return true;
    }

    bool RECTF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 4)
        {
            fprintf(stderr, "Keywords::RECTF() : '%s:%d' : syntax error, use 'RECTF <x1>, <y1>, <x2>, <y2>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<Expression::Numeric> params = {Expression::Numeric(), Expression::Numeric(), Expression::Numeric(), Expression::Numeric()};
        for(int i=0; i<int(tokens.size()); i++)
        {
            if(Compiler::parseExpression(codeLineIndex, tokens[i], params[i]) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::RECTF() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
            switch(i)
            {
                case 0: Compiler::emitVcpuAsm("STW", "drawRectF_x1", false); break;
                case 1: Compiler::emitVcpuAsm("STW", "drawRectF_y1", false); break;
                case 2: Compiler::emitVcpuAsm("STW", "drawRectF_x2", false); break;
                case 3: Compiler::emitVcpuAsm("STW", "drawRectF_y2", false); break;

                default: break;
            }
        }

        Compiler::emitVcpuAsm("%DrawRectF", "", false);

        return true;
    }

    bool POLY(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::POLY() : '%s:%d' : syntax error, use 'POLY <coords address>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Expression::Numeric param;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], param) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::POLY() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "drawPoly_addr", false);
        Compiler::emitVcpuAsm("%DrawPoly", "",        false);

        return true;
    }

    bool POLYR(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 2)
        {
            fprintf(stderr, "Keywords::POLYR() : '%s:%d' : syntax error, use 'POLYR <coords address> <optional FLIPX/FLIPY/FLIPXY>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Flip type
        if(tokens.size() == 2)
        {
            std::string flipToken = tokens[1];
            Expression::stripWhitespace(flipToken);
            Expression::strToUpper(flipToken);

            if(flipToken != "FLIPX"  &&  flipToken != "FLIPY"  &&  flipToken != "FLIPXY")
            {
                fprintf(stderr, "Keywords::POLYR() : '%s:%d' : syntax error, use one of the correct flip types, 'POLY <coords address> <optional FLIPX/FLIPY/FLIPXY>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }

             // SUBW mode
            Compiler::emitVcpuAsm("LDI", "0xB8", false);
            Compiler::emitVcpuAsm("ST", "drawPoly_mode", false);

            // FlipX
            if(flipToken == "FLIPX")
            {
                Compiler::emitVcpuAsm("%SetPolyRelFlipX", "", false);
            }
            // FlipY
            else if(flipToken == "FLIPY")
            {
                Compiler::emitVcpuAsm("%SetPolyRelFlipY", "", false);
            }
            // FlipXY
            else
            {
                Compiler::emitVcpuAsm("%SetPolyRelFlipX", "", false);
                Compiler::emitVcpuAsm("%SetPolyRelFlipY", "", false);
            }
        }

        Expression::Numeric param;
        if(Compiler::parseExpression(codeLineIndex, tokens[0], param) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::POLYR() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[0].c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "drawPoly_addr", false);
        Compiler::emitVcpuAsm("%DrawPolyRel", "", false);

        return true;
    }

    bool TCLIP(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ' ', false);
        if(tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::TCLIP() : '%s:%d' : syntax error, use 'TCLIP ON' or 'TCLIP OFF' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::string tclipToken = Expression::strToUpper(tokens[0]);
        Expression::stripWhitespace(tclipToken);
        if(tclipToken != "ON"  &&  tclipToken != "OFF")
        {
            fprintf(stderr, "Keywords::TCLIP() : '%s:%d' : syntax error, use 'TCLIP ON' or 'TCLIP OFF' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(tclipToken == "ON")
        {
            Compiler::emitVcpuAsm("LDWI", "0xFFFB",    false);
            Compiler::emitVcpuAsm("ANDW", "miscFlags", false);
        }
        else
        {
            Compiler::emitVcpuAsm("LDWI", "0x0004",   false);
            Compiler::emitVcpuAsm("ORW", "miscFlags", false);
        }
        Compiler::emitVcpuAsm("STW", "miscFlags", false);

        return true;
    }

    bool SCROLL(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ' ', false);
        if(tokens.size() != 1)
        {
            fprintf(stderr, "Keywords::SCROLL() : '%s:%d' : syntax error, use 'SCROLL ON' or 'SCROLL OFF' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::string scrollToken = Expression::strToUpper(tokens[0]);
        Expression::stripWhitespace(scrollToken);
        if(scrollToken != "ON"  &&  scrollToken != "OFF")
        {
            fprintf(stderr, "Keywords::SCROLL() : '%s:%d' : syntax error, use 'SCROLL ON' or 'SCROLL OFF'' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        if(scrollToken == "ON")
        {
            Compiler::emitVcpuAsm("LDWI", "0x0001",   false);
            Compiler::emitVcpuAsm("ORW", "miscFlags", false);
        }
        else
        {
            Compiler::emitVcpuAsm("LDWI", "0xFFFE",    false);
            Compiler::emitVcpuAsm("ANDW", "miscFlags", false);
        }
        Compiler::emitVcpuAsm("STW", "miscFlags", false);

        return true;
    }

    bool POKE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 2)
        {
            fprintf(stderr, "Keywords::POKE() : '%s:%d' : syntax error, use 'POKE <address>, <value>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<std::string> operands = {"", ""};
        std::vector<Expression::Numeric> numerics = {Expression::Numeric(), Expression::Numeric()};
        std::vector<Compiler::OperandType> operandTypes {Compiler::OperandConst, Compiler::OperandConst};

        for(int i=0; i<int(tokens.size()); i++)
        {
            operandTypes[i] = Compiler::parseStaticExpression(codeLineIndex, tokens[i], operands[i], numerics[i]);
            if(operandTypes[i] == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::POKE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
        }

        std::string opcode, operand;
        switch(numerics[1]._int16Byte)
        {
            case Expression::Int16Low:  opcode = "LD";  operand = "_" + operands[1];          break;
            case Expression::Int16High: opcode = "LD";  operand = "_" + operands[1] + " + 1"; break;
            case Expression::Int16Both: opcode = "LDW"; operand = "_" + operands[1];          break;

            default: break;
        }

        if((operandTypes[0] == Compiler::OperandVar  ||  operandTypes[0] == Compiler::OperandTemp)  &&  (operandTypes[1] == Compiler::OperandVar  ||
                                                                                operandTypes[1] == Compiler::OperandTemp))
        {
            (operandTypes[1] == Compiler::OperandVar) ? Compiler::emitVcpuAsm(opcode, operand, false, codeLineIndex) : Compiler::emitVcpuAsm("LDW", operands[1], false);
            (operandTypes[0] == Compiler::OperandVar) ? Compiler::emitVcpuAsm("POKE", "_" + operands[0], false, codeLineIndex) : Compiler::emitVcpuAsm("POKE", operands[0], false);
        }
        else if((operandTypes[0] == Compiler::OperandVar  ||  operandTypes[0] == Compiler::OperandTemp)  &&  operandTypes[1] == Compiler::OperandConst)
        {
            Compiler::emitVcpuAsm("LDI", operands[1], false);
            (operandTypes[0] == Compiler::OperandVar) ? Compiler::emitVcpuAsm("POKE", "_" + operands[0], false, codeLineIndex) : Compiler::emitVcpuAsm("POKE", operands[0], false);
        }
        else if(operandTypes[0] == Compiler::OperandConst  &&  (operandTypes[1] == Compiler::OperandVar  ||  operandTypes[1] == Compiler::OperandTemp))
        {
            uint16_t addr;
            if(Expression::stringToU16(operands[0], addr)  &&  addr < 0x0100)
            {
                (operandTypes[1] == Compiler::OperandVar) ? Compiler::emitVcpuAsm(opcode, operand, false, codeLineIndex) : Compiler::emitVcpuAsm("LDW", operands[1], false);
                Compiler::emitVcpuAsm("ST", operands[0], false);
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", operands[0], false);
                Compiler::emitVcpuAsm("STW", "register0", false);
                (operandTypes[1] == Compiler::OperandVar) ? Compiler::emitVcpuAsm(opcode, operand, false, codeLineIndex) : Compiler::emitVcpuAsm("LDW", operands[1], false);
                Compiler::emitVcpuAsm("POKE", "register0", false);
            }
        }
        else
        {
            // Optimise for page 0
            uint16_t addr;
            if(Expression::stringToU16(operands[0], addr)  &&  addr < 0x0100)
            {
                Compiler::emitVcpuAsm("LDI", operands[1], false);
                Compiler::emitVcpuAsm("ST",  operands[0], false);
            }
            // All other pages
            else
            {
                Compiler::emitVcpuAsm("LDWI", operands[0], false);
                Compiler::emitVcpuAsm("STW",  "register0", false);
                Compiler::emitVcpuAsm("LDI",  operands[1], false);
                Compiler::emitVcpuAsm("POKE", "register0", false);
            }
        }

        return true;
    }

    bool DOKE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 2)
        {
            fprintf(stderr, "Keywords::DOKE() : '%s:%d' : syntax error, use 'DOKE <address>, <value>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        std::vector<std::string> operands = {"", ""};
        std::vector<Expression::Numeric> numerics = {Expression::Numeric(), Expression::Numeric()};
        std::vector<Compiler::OperandType> operandTypes {Compiler::OperandConst, Compiler::OperandConst};

        for(int i=0; i<int(tokens.size()); i++)
        {
            operandTypes[i] = Compiler::parseStaticExpression(codeLineIndex, tokens[i], operands[i], numerics[i]);
            if(operandTypes[i] == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::DOKE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[i].c_str(), codeLine._text.c_str());
                return false;
            }
        }

        std::string opcode, operand;
        switch(numerics[1]._int16Byte)
        {
            case Expression::Int16Low:  opcode = "LD";  operand = "_" + operands[1];          break;
            case Expression::Int16High: opcode = "LD";  operand = "_" + operands[1] + " + 1"; break;
            case Expression::Int16Both: opcode = "LDW"; operand = "_" + operands[1];          break;

            default: break;
        }

        if((operandTypes[0] == Compiler::OperandVar  ||  operandTypes[0] == Compiler::OperandTemp)  &&  (operandTypes[1] == Compiler::OperandVar  ||
                                                                                operandTypes[1] == Compiler::OperandTemp))
        {
            (operandTypes[1] == Compiler::OperandVar) ? Compiler::emitVcpuAsm(opcode, operand, false, codeLineIndex) : Compiler::emitVcpuAsm("LDW", "" + operands[1], false);
            (operandTypes[0] == Compiler::OperandVar) ? Compiler::emitVcpuAsm("DOKE", "_" + operands[0], false, codeLineIndex) : Compiler::emitVcpuAsm("DOKE", "" + operands[0], false);
        }
        else if((operandTypes[0] == Compiler::OperandVar  ||  operandTypes[0] == Compiler::OperandTemp)  &&  operandTypes[1] == Compiler::OperandConst)
        {
            Compiler::emitVcpuAsm("LDWI", operands[1], false);
            (operandTypes[0] == Compiler::OperandVar) ? Compiler::emitVcpuAsm("DOKE", "_" + operands[0], false, codeLineIndex) : Compiler::emitVcpuAsm("DOKE", "" + operands[0], false);
        }
        else if(operandTypes[0] == Compiler::OperandConst  &&  (operandTypes[1] == Compiler::OperandVar  ||  operandTypes[1] == Compiler::OperandTemp))
        {
            uint16_t addr;
            if(Expression::stringToU16(operands[0], addr)  &&  addr < 0x0100)
            {
                (operandTypes[1] == Compiler::OperandVar) ? Compiler::emitVcpuAsm(opcode, operand, false, codeLineIndex) : Compiler::emitVcpuAsm("LDW", "" + operands[1], false);
                Compiler::emitVcpuAsm("STW", operands[0], false);
            }
            else
            {
                Compiler::emitVcpuAsm("LDWI", operands[0], false);
                Compiler::emitVcpuAsm("STW", "register0", false);
                (operandTypes[1] == Compiler::OperandVar) ? Compiler::emitVcpuAsm(opcode, operand, false, codeLineIndex) : Compiler::emitVcpuAsm("LDW", "" + operands[1], false);
                Compiler::emitVcpuAsm("DOKE", "register0", false);
            }
        }
        else
        {
            // Optimise for page 0
            uint16_t addr;
            if(Expression::stringToU16(operands[0], addr)  &&  addr < 0x0100)
            {
                Compiler::emitVcpuAsm("LDWI", operands[1], false);
                Compiler::emitVcpuAsm("STW",  operands[0], false);
            }
            // All other pages
            else
            {
                Compiler::emitVcpuAsm("LDWI", operands[0], false);
                Compiler::emitVcpuAsm("STW",  "register0", false);
                Compiler::emitVcpuAsm("LDWI", operands[1], false);
                Compiler::emitVcpuAsm("DOKE", "register0", false);
            }
        }

        return true;
    }

    void timeInit(void)
    {
        // Init time proc
        Compiler::setCreateTimeData(true);

        // ROM's 1 to 4
        if(Compiler::getCodeRomType() < Cpu::ROMv5a)
        {
            Compiler::emitVcpuAsm("LDI",  "0",               false);
            Compiler::emitVcpuAsm("STW",  "timerTick",       false);
            Compiler::emitVcpuAsm("LDI",  "giga_frameCount", false);
            Compiler::emitVcpuAsm("STW",  "timerPrev",       false);
        }
        // ROMv5a or higher
        else
        {
            Compiler::emitVcpuAsm("LDI",  "0",               false);
            Compiler::emitVcpuAsm("STW",  "timerTick",       false);
            Compiler::emitVcpuAsm("STW",  "timerPrev",       false);
        }
    }
    void midiInit(void)
    {
        _midiType = Midi;
    }
    void midivInit(void)
    {
        _midiType = MidiV;
    }
    void userInit(const std::string& label)
    {
        _userRoutine = label;
    }
    void timeAddr(int index)
    {
        Compiler::emitVcpuAsm("LDWI", "tickTime",                             false);
        Compiler::emitVcpuAsm("STW",  "realTimeProc" + std::to_string(index), false);
    }
    void midiAddr(int index)
    {
        Compiler::emitVcpuAsm("LDWI", "playMidi",                             false);
        Compiler::emitVcpuAsm("STW",  "realTimeProc" + std::to_string(index), false);
    }
    void midivAddr(int index)
    {
        Compiler::emitVcpuAsm("LDWI", "playMidiVol",                          false);
        Compiler::emitVcpuAsm("STW",  "realTimeProc" + std::to_string(index), false);
    }
    void userAddr(const std::string& label, int index)
    {
        Compiler::emitVcpuAsm("LDWI", "_" + label,                            false);
        Compiler::emitVcpuAsm("STW",  "realTimeProc" + std::to_string(index), false);
    }
    void usageINIT(Compiler::CodeLine& codeLine, int codeLineStart)
    {
        fprintf(stderr, "Keywords::INIT() : '%s:%d' : syntax error, use 'INIT VARS, <optional var address>, TIME, MIDI/MIDIV, <user proc>, NOUPDATE : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
    }
    bool INIT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        static std::map<std::string, InitTypes> initTypesMap = {{"TIME", InitTime}, {"MIDI", InitMidi}, {"MIDIV", InitMidiV}};

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 4)
        {
            usageINIT(codeLine, codeLineStart);
            return false;
        }

        if(tokens.size() == 1  ||  Compiler::getCodeRomType() > Cpu::ROMv4)
        {
            Expression::stripWhitespace(tokens[0]);
            std::string token = tokens[0];
            Expression::strToUpper(token);
            if(token == "NOUPDATE")
            {
                fprintf(stderr, "Keywords::INIT() : '%s:%d' : syntax error, 'NOUPDATE' must be used with 'INIT TIME, MIDI/MIDIV, <user proc>' and only on ROMv4 or lower : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }
        }

        // Search for init types and labels
        bool noUpdate = false;
        bool resetVars = false;
        std::string varsAddr;
        std::vector<InitTypes> initTypes;
        for(int i=0; i<int(tokens.size()); i++)
        {
            Expression::stripWhitespace(tokens[i]);
            std::string token = tokens[i];
            Expression::strToUpper(token);

            // Init type not found, so search for label
            if(initTypesMap.find(token) == initTypesMap.end())
            {
                if(token == "NOUPDATE")
                {
                    noUpdate = true;
                    continue;
                }

                size_t varsPos = token.find("VARS");
                if(varsPos != std::string::npos)
                {
                    // Check for starting var address
                    size_t addrPos = varsPos + sizeof("VARS")-1;
                    if(addrPos < tokens[i].size())
                    {
                        varsAddr = tokens[i].substr(addrPos);
                    }

                    resetVars = true;
                    continue;
                }

                int labIndex = Compiler::findLabel(tokens[i]);
                if(labIndex == -1)
                {
                    usageINIT(codeLine, codeLineStart);
                    return false;
                }
                else
                {
                    initTypes.push_back(InitUser);
                }
            }
            else
            {
                if(initTypesMap[token] == InitMidi  ||  initTypesMap[token] == InitMidiV)
                {
                    if(_midiType != MidiNone)
                    {
                        fprintf(stderr, "Keywords::INIT() : '%s:%d' : syntax error, can only init one instance of MIDI or MIDIV, use 'INIT TIME, MIDI/MIDIV, <user proc> : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                        return false;
                    }
                }
                initTypes.push_back(initTypesMap[token]);
            }
        }

        // Proc and addr init
        for(int i=0; i<int(initTypes.size()); i++)
        {
            switch(initTypes[i])
            {
                case InitTime:  timeInit();           timeAddr(i);            break;
                case InitMidi:  midiInit();           midiAddr(i);            break;
                case InitMidiV: midivInit();          midivAddr(i);           break;
                case InitUser:  userInit(tokens[i]);  userAddr(tokens[i], i); break;

                default: break;
            }
        }

        if(resetVars)
        {
            if(varsAddr.size())
            {
                Expression::Numeric numeric(true); // true = allow static init
                if(!Expression::parse(varsAddr, codeLineIndex, numeric))
                {
                    fprintf(stderr, "Keywords::INIT() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                    return false;
                }
                Compiler::emitVcpuAsm("LDI", Expression::byteToHexString(int8_t(std::lround(numeric._value))), false);
            }
            else
            {
                Compiler::emitVcpuAsm("LDI", "giga_User", false);
            }

            Compiler::emitVcpuAsm("STW", "varAddress", false);
            Compiler::emitVcpuAsm("%ResetVars", "", false);
        }

        if(initTypes.size())
        {
            // ROM's 1 to 4, (time sliced code)
            if(Compiler::getCodeRomType() < Cpu::ROMv5a)
            {
                // If 'NOUPDATE" is specified then user must call tick himself
                if(!noUpdate)
                {
                    Compiler::enableSysInitFunc("InitRealTimeStub");
                    Linker::setInternalSubToLoad("realTimeStub");
                    Compiler::emitVcpuPreProcessingCmd("%define TIME_SLICING");
                    Compiler::emitVcpuAsm("LDWI", "setRealTimeProc" + std::to_string(initTypes.size() - 1), false);
                    Compiler::emitVcpuAsm("CALL", "giga_vAC",                                               false);
                }
            }
            // ROMv5a and higher, (vertical blank interrupt)
            else
            {
                // Vertical blank interrupt uses 0x30-0x33 for vPC and vAC save/restore, so we must move any variables found there
                Compiler::moveVblankVars();

                // Build chain of vertical blank interrupt handlers, (up to 3)
                Compiler::emitVcpuAsm("CALLI", "setRealTimeProc" + std::to_string(initTypes.size() - 1), false);

                // Start vertical blank interrupt
                Compiler::emitVcpuAsm("LDI",  "0xFF"           , false);
                Compiler::emitVcpuAsm("ST",   "giga_frameCount", false);
                Compiler::emitVcpuAsm("LDWI", "giga_vblankProc", false);
                Compiler::emitVcpuAsm("STW",  "register0"      , false);
                Compiler::emitVcpuAsm("LDWI", "realTimeStub"   , false);
                Compiler::emitVcpuAsm("DOKE", "register0"      , false);
            }
        }

        return true;
    }

    void usageTICK(Compiler::CodeLine& codeLine, int codeLineStart)
    {
        fprintf(stderr, "Keywords::INIT() : '%s:%d' : syntax error, use one or more of, 'TICK TIME, MIDI, MIDIV, USER' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
    }
    bool TICK(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);

        if(Compiler::getCodeRomType() > Cpu::ROMv4)
        {
            std::string romTypeStr;
            getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
            fprintf(stderr, "Keywords::TICK() : '%s:%d' : version error, 'TICK' requires ROMv4 or lower, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
            return false;
        }

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 4)
        {
            usageTICK(codeLine, codeLineStart);
            return false;
        }

        std::map<std::string, InitTypes> initTypesMap;
        for(int i=0; i<int(tokens.size()); i++)
        {
            std::string token = Expression::strToUpper(tokens[i]);
            Expression::stripWhitespace(token);
            if(initTypesMap.find(token) == initTypesMap.end())
            {
                if(token == "TIME")
                {
                    if(!Compiler::getCreateTimeData())
                    {
                        fprintf(stderr, "Keywords::TICK() : '%s:%d' : TIME not initialised using INIT : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                        return false;
                    }

                    initTypesMap[token] = InitTime;
                    Compiler::emitVcpuAsm("%TickTime", "", false);
                }
                else if(token == "MIDI")
                {
                    if(_midiType != Midi)
                    {
                        fprintf(stderr, "Keywords::TICK() : '%s:%d' : MIDI not initialised using INIT : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                        return false;
                    }

                    initTypesMap[token] = InitMidi;
                    Compiler::emitVcpuAsm("%TickMidi", "", false);
                }
                else if(token == "MIDIV")
                {
                    if(_midiType != MidiV)
                    {
                        fprintf(stderr, "Keywords::TICK() : '%s:%d' : MIDIV not initialised using INIT : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                        return false;
                    }

                    initTypesMap[token] = InitMidiV;
                    Compiler::emitVcpuAsm("%TickMidiV", "", false);
                }
                else if(token == "USER")
                {
                    if(_userRoutine == "")
                    {
                        fprintf(stderr, "Keywords::TICK() : '%s:%d' : USER routine not initialised using INIT : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                        return false;
                    }

                    initTypesMap[token] = InitUser;
                    if(Compiler::getCodeRomType() < Cpu::ROMv5a)
                    {
                        Compiler::emitVcpuAsm("LDWI", "_" + _userRoutine, false);
                        Compiler::emitVcpuAsm("CALL", "giga_vAC",         false);
                    }
                    else
                    {
                        Compiler::emitVcpuAsm("CALLI", "_" + _userRoutine, false);
                    }
                }
                else
                {
                    usageTICK(codeLine, codeLineStart);
                    return false;
                }
            }
            else
            {
                usageTICK(codeLine, codeLineStart);
                return false;
            }
        }

        return true;
    }

    void usagePLAY(Compiler::CodeLine& codeLine, int codeLineStart)
    {
        fprintf(stderr, "Keywords::PLAY() : '%s:%d' : syntax error, use 'PLAY <TYPE>, <id/address>, <optional waveType>', where <TYPE> = 'MIDI', 'MIDID', 'MIDIV', 'MIDIDV' or 'MUSIC' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
    }
    bool PLAY(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 2  ||  tokens.size() > 3)
        {
            usagePLAY(codeLine, codeLineStart);
            return false;
        }

        // Default wave type
        if(tokens.size() == 2)
        {
            Compiler::emitVcpuAsm("LDI", "2",           false);
            Compiler::emitVcpuAsm("ST", "waveType + 1", false);
        }
        // Midi wave type, (optional)
        else if(tokens.size() == 3)
        {
            std::string waveTypeToken = tokens[2];
            Expression::Numeric waveTypeNumeric;
            if(Compiler::parseExpression(codeLineIndex, waveTypeToken, waveTypeNumeric) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::PLAY() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, waveTypeToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "waveType + 1", false);
        }

        // Midi stream address
        std::string addressToken = tokens[1];
        Expression::Numeric addressNumeric;
        if(Compiler::parseExpression(codeLineIndex, tokens[1], addressNumeric) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::PLAY() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, tokens[1].c_str(), codeLine._text.c_str());
            return false;
        }

        std::string midiToken = Expression::strToUpper(tokens[0]);
        Expression::stripWhitespace(midiToken);
        if(midiToken == "MIDI")
        {
            Compiler::emitVcpuAsm("%PlayMidi", "", false);
            return true;
        }

        if(midiToken == "MIDID")
        {
            Compiler::emitVcpuAsm("%SetMidiStream", "", false);
            Compiler::emitVcpuAsm("%PlayMidi", "",      false);
            return true;
        }

        if(midiToken == "MIDIV")
        {
            Compiler::emitVcpuAsm("%PlayMidiV", "", false);
            return true;
        }

        if(midiToken == "MIDIDV")
        {
            Compiler::emitVcpuAsm("%SetMidiStream", "", false);
            Compiler::emitVcpuAsm("%PlayMidiV", "",     false);
            return true;
        }

        if(midiToken == "MUSIC")
        {
            Compiler::emitVcpuAsm("%PlayMusic", "", false);
            return true;
        }

        usagePLAY(codeLine, codeLineStart);
        return false;
    }

    void loadUsage(int msgType, Compiler::CodeLine& codeLine, int codeLineStart)
    {
        switch(msgType)
        {
            case LoadType:   fprintf(stderr, "Keywords::LOAD() : '%s:%d' : syntax error, use 'LOAD <TYPE>, <filename>, where <TYPE> = 'IMAGE', 'SPRITE', 'FONT', 'MIDI', 'WAVE' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str()); break;
            case LoadWave:   fprintf(stderr, "Keywords::LOAD() : '%s:%d' : syntax error, use 'LOAD WAVE, <filename>, <optional address>, <optional address offset>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());             break;
            case LoadMidi:   fprintf(stderr, "Keywords::LOAD() : '%s:%d' : syntax error, use 'LOAD MIDI, <filename>, <id>, <optional loop count 1<->255, 0=forever>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());            break;
            case LoadImage:  fprintf(stderr, "Keywords::LOAD() : '%s:%d' : syntax error, use 'LOAD IMAGE, <filename>, <optional address>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());                                       break;
            case LoadSprite: fprintf(stderr, "Keywords::LOAD() : '%s:%d' : syntax error, use 'LOAD SPRITE, <filename>, <id>, <optional flip>, <optional overlap>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());               break;
            case LoadFont:   fprintf(stderr, "Keywords::LOAD() : '%s:%d' : syntax error, use 'LOAD FONT, <filename>, <id>, <optional 16 bit bg:fg colours>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());                     break;

            default: break;
        }
    }
    /********************************************************************************************************************************/
    /* Load Wave                                                                                */
    /********************************************************************************************************************************/
    bool loadWave(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, const std::vector<std::string>& tokens)
    {
        if(tokens.size() < 2  ||  tokens.size() > 4)
        {
            loadUsage(LoadWave, codeLine, codeLineStart);
            return false;
        }

        std::string filename = tokens[1];
        Expression::stripWhitespace(filename);
        std::string ext = filename;
        Expression::strToUpper(ext);
        if(ext.find(".GTWAV") != std::string::npos)
        {
            std::string filepath = Loader::getFilePath();
            size_t slash = filepath.find_last_of("\\/");
            filepath = (slash != std::string::npos) ? filepath.substr(0, slash) : ".";
            filename = filepath + "/" + filename;

            // Parse optional address
            uint16_t address = RAM_AUDIO_START;
            if(tokens.size() >= 3)
            {
                std::string addrToken = tokens[2];
                Expression::Numeric addrNumeric;
                std::string addrOperand;
                if(Compiler::parseStaticExpression(codeLineIndex, addrToken, addrOperand, addrNumeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::loadWave() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                address = uint16_t(std::lround(addrNumeric._value));
                if(address < DEFAULT_EXEC_ADDRESS)
                {
                    loadUsage(LoadWave, codeLine, codeLineStart);
                    fprintf(stderr, "Keywords::loadWave() : '%s:%d' : address field must be above &h%04x, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, DEFAULT_EXEC_ADDRESS, addrToken.c_str(), codeLine._text.c_str());
                    return false;
                }
            }

            // Parse optional address
            uint16_t addrOffset = 0;
            if(tokens.size() == 4)
            {
                std::string offsetToken = tokens[3];
                Expression::Numeric offsetNumeric;
                std::string offsetOperand;
                if(Compiler::parseStaticExpression(codeLineIndex, offsetToken, offsetOperand, offsetNumeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::loadWave() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, offsetToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                addrOffset = uint16_t(std::lround(offsetNumeric._value));
            }

            // Load wave file
            std::vector<uint8_t> dataBytes(64);
            std::ifstream infile(filename, std::ios::binary | std::ios::in);
            if(!infile.is_open())
            {
                loadUsage(LoadWave, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadWave() : '%s:%d' : failed to open file '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, filename.c_str(), codeLine._text.c_str());
                return false;
            }
            infile.read((char *)&dataBytes[0], 64);
            if(infile.eof() || infile.bad() || infile.fail())
            {
                loadUsage(LoadWave, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadWave() : '%s:%d' : failed to read file '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, filename.c_str(), codeLine._text.c_str());
                return false;
            }

            uint16_t addr = address;
            for(int i=0; i<int(dataBytes.size()); i++)
            {
                if(addrOffset != 0)
                {
                    if(addr < RAM_AUDIO_START  ||  addr > RAM_AUDIO_END)
                    {
                        if(!Memory::takeFreeRAM(addr, 1)) return false;
                        addr += addrOffset;
                    }
                }
            }
            if(addrOffset == 0)
            {
                if(!Memory::takeFreeRAM(address, int(dataBytes.size()))) return false;
            }
            Compiler::getDefDataBytes().push_back({address, addrOffset, dataBytes});
        }

        return true;
    }
    /********************************************************************************************************************************/
    /* Load Midi                                                                                */
    /********************************************************************************************************************************/
    bool loadMidi(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, const std::vector<std::string>& tokens)
    {
        if(tokens.size() < 3  ||  tokens.size() > 4)
        {
            loadUsage(LoadMidi, codeLine, codeLineStart);
            return false;
        }

        std::string filename = tokens[1];
        Expression::stripWhitespace(filename);
        std::string ext = filename;
        Expression::strToUpper(ext);
        if(ext.find(".GTMID") != std::string::npos)
        {
            std::string filepath = Loader::getFilePath();
            size_t slash = filepath.find_last_of("\\/");
            filepath = (slash != std::string::npos) ? filepath.substr(0, slash) : ".";
            filename = filepath + "/" + filename;

            // Unique midi ID
            std::string idToken = tokens[2];
            Expression::Numeric idNumeric;
            std::string idOperand;
            if(Compiler::parseStaticExpression(codeLineIndex, idToken, idOperand, idNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::loadMidi() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, idToken.c_str(), codeLine._text.c_str());
                return false;
            }
            int midiId = int(std::lround(idNumeric._value));
            if(Compiler::getDefDataMidis().find(midiId) != Compiler::getDefDataMidis().end())
            {
                loadUsage(LoadMidi, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadMidi() : '%s:%d' : MIDI id %d not unique : %s\n", codeLine._moduleName.c_str(), codeLineStart, midiId, codeLine._text.c_str());
                return false;
            }

            // Parse optional loop count
            uint16_t loops = 0;
            if(tokens.size() == 4)
            {
                std::string loopsToken = tokens[2];
                Expression::Numeric loopsNumeric;
                std::string loopsOperand;
                if(Compiler::parseStaticExpression(codeLineIndex, loopsToken, loopsOperand, loopsNumeric) == Compiler::OperandInvalid)
                {
                    fprintf(stderr, "Keywords::loadMidi() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, loopsToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                loops = uint16_t(std::lround(loopsNumeric._value));
                if(loops > 255)
                {
                    loadUsage(LoadMidi, codeLine, codeLineStart);
                    fprintf(stderr, "Keywords::loadMidi() : '%s:%d' : loops field must be between 0 and 255, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, loopsToken.c_str(), codeLine._text.c_str());
                    return false;
                }
            }

            // Load gtMID file
            int midiSize = 0;
            GtMidiHdr gtMidiHdr;
            std::vector<uint8_t> midiBuffer(MIDI_MAX_BUFFER_SIZE);
            if(!Midi::loadFile(filename, midiBuffer.data(), midiSize, &gtMidiHdr))
            {
                loadUsage(LoadMidi, codeLine, codeLineStart);
                return false;
            }
            midiBuffer.resize(midiSize);

            // Allocate memory for midi segments
            int byteIndex = 0;
            int segmentCount = 0;
            std::vector<uint16_t> segSizes;
            std::vector<uint16_t> segAddrs;
            bool hasVolume = bool(gtMidiHdr._hasVolume);
            while(midiSize)
            {
                uint16_t size = 0;
                uint16_t address = 0;
                if(!Memory::getFreeRAM(Memory::FitDescending, USER_CODE_START, Compiler::getRuntimeStart(), MIDI_MIN_SEGMENT_SIZE, address, uint16_t(midiSize + MIDI_CMD_JMP_SEG_SIZE), size, false))
                {
                    loadUsage(LoadMidi, codeLine, codeLineStart);
                    fprintf(stderr, "Keywords::loadMidi() : '%s:%d' : getting MIDI memory for segment %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, segmentCount, codeLine._text.c_str());
                    return false;
                }

                byteIndex += size;

                // Pad for jump segment command and adjust for start notes, start note can be either 3 bytes, (0x90, nn, vv) or 2 bytes, (0x90, nn), depending on hasVolume
                if(hasVolume  &&  (midiBuffer[byteIndex - 5] & 0xF0) == MIDI_CMD_START_NOTE) {byteIndex -= 2; size -= 2; Memory::giveFreeRAM(address + size-2, 2);} // landed on volume, (vv)
                else if((midiBuffer[byteIndex - 4] & 0xF0) == MIDI_CMD_START_NOTE)           {byteIndex -= 1; size -= 1; Memory::giveFreeRAM(address + size-1, 1);} // landed on note, (nn)
                                                                                {byteIndex -= 3; size -= 3;                                          } // jump segment, (0xD0, lo, hi)
                midiSize -= size;
                segSizes.push_back(size);
                segAddrs.push_back(address);
                segmentCount++;
            }

            Compiler::getDefDataMidis()[midiId] = {midiId, hasVolume, uint8_t(loops), midiBuffer, segSizes, segAddrs};
        }

        return true;
    }
    /********************************************************************************************************************************/
    /* Load Image                                                                                */
    /********************************************************************************************************************************/
    bool loadImageChunk(Compiler::CodeLine& codeLine, int codeLineStart, const std::vector<uint8_t>& data, int row, uint16_t width, uint16_t address, uint8_t chunkSize, uint16_t& chunkOffset, uint16_t& chunkAddr)
    {
        if(!Memory::getFreeRAM(Memory::FitDescending, chunkSize, USER_CODE_START, Compiler::getRuntimeStart(), chunkAddr))
        {
            loadUsage(LoadImage, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadImageChunk() : '%s:%d' : allocating RAM for offscreen pixel chunk on row %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, row, codeLine._text.c_str());
            return false;
        }

        std::vector<uint8_t> chunkData;
        for(int j=0; j<chunkSize; j++) chunkData.push_back(data[row*width + chunkOffset + j]);

        // Output loader image chunks copy code
        if(Compiler::getDefDataLoaderImageChunks().size() == 0)
        {
            if(Compiler::getCodeRomType() < Cpu::ROMv5a)
            {
                Compiler::emitVcpuAsm("LDWI", "copyLoaderImages", false);
                Compiler::emitVcpuAsm("CALL", "giga_vAC", false);
            }
            else
            {
                Compiler::emitVcpuAsm("CALLI", "copyLoaderImages", false);
            }
        }

        Compiler::DefDataLoaderImageChunk defDataLoaderImageChunk = {chunkAddr, uint16_t(address + chunkOffset), chunkSize, chunkData};
        Compiler::getDefDataLoaderImageChunks().push_back(defDataLoaderImageChunk);

        chunkOffset += chunkSize;

        return true;
    }
    bool loadImage(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, const std::vector<std::string>& tokens, const std::string& filename, const Image::TgaFile& tgaFile, const Image::GtRgbFile& gtRgbFile)
    {
        const uint16_t stride = 256;

        if(tokens.size() < 2  ||  tokens.size() > 3)
        {
            loadUsage(LoadImage, codeLine, codeLineStart);
            return false;
        }

        // Parse optional address
        uint16_t address = RAM_VIDEO_START;
        if(tokens.size() == 3)
        {
            std::string addrToken = tokens[2];
            Expression::Numeric addrNumeric;
            std::string addrOperand;
            if(Compiler::parseStaticExpression(codeLineIndex, addrToken, addrOperand, addrNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::loadImage() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, addrToken.c_str(), codeLine._text.c_str());
                return false;
            }
            address = uint16_t(std::lround(addrNumeric._value));
            if(address < DEFAULT_EXEC_ADDRESS)
            {
                loadUsage(LoadImage, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadImage() : '%s:%d' : address field must be above &h%04x, found %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, DEFAULT_EXEC_ADDRESS, addrToken.c_str(), codeLine._text.c_str());
                return false;
            }
        }

        if(gtRgbFile._header._width > stride  ||  gtRgbFile._header._width + (address & 0x00FF) > stride)
        {
            loadUsage(LoadImage, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadImage() : '%s:%d' : image width %d + starting address 0x%04x overflow, for %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, gtRgbFile._header._width, address, filename.c_str(), codeLine._text.c_str());
            return false;
        }

        Compiler::DefDataImage defDataImage = {address, tgaFile._header._width, tgaFile._header._height, stride, gtRgbFile._data};
        Compiler::getDefDataImages().push_back(defDataImage);

        int size = gtRgbFile._header._width;
        for(int y=0; y<gtRgbFile._header._height; y++)
        {
            // Take offscreen memory from compiler for images wider than visible screen resolution, or images in offscreen memory
            if(address >= RAM_VIDEO_START  &&  address <= RUN_TIME_START)
            {
                size = gtRgbFile._header._width + (address & 0x00FF) - RAM_SCANLINE_SIZE;
                if(size > 0)
                {
                    if(!Memory::takeFreeRAM((address & 0xFF00) + RAM_SCANLINE_SIZE, size))
                    {
                        loadUsage(LoadImage, codeLine, codeLineStart);
                        fprintf(stderr, "Keywords::loadImage() : '%s:%d' : allocating RAM for pixel row %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, y, codeLine._text.c_str());
                        return false;
                    }
                }
            }

            // 'Loader.gcl' is resident at these addresses when loading *.gt1 files, therefore you can overwrite these locations only AFTER the loading process has finished
            // Split up image scanlines into offscreen chunks and load the offscreen chunks into the correct onscreen memory locations after 'Loader.gcl' is done
            if((address >= LOADER_SCANLINE0_START  &&  address<= LOADER_SCANLINE0_END)  ||  (address >= LOADER_SCANLINE1_START  &&  address<= LOADER_SCANLINE1_END)  ||
                (address >= LOADER_SCANLINE2_START  &&  address<= LOADER_SCANLINE2_END))
            {
                uint16_t chunkOffset = 0x0000, chunkAddr = 0x0000;
                for(int i=0; i<gtRgbFile._header._width / RAM_SEGMENTS_SIZE; i++)
                {
                    loadImageChunk(codeLine, codeLineStart, gtRgbFile._data, y, gtRgbFile._header._width, address, RAM_SEGMENTS_SIZE, chunkOffset, chunkAddr);
                }
                loadImageChunk(codeLine, codeLineStart, gtRgbFile._data, y, gtRgbFile._header._width, address, gtRgbFile._header._width % RAM_SEGMENTS_SIZE, chunkOffset, chunkAddr);
            }

            // Next destination row
            address += stride;
        }

        return true;
    }
    /********************************************************************************************************************************/
    /* Load Sprite                                                                                */
    /********************************************************************************************************************************/
    bool loadSprite(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, const std::vector<std::string>& tokens, const std::string& filename, const Image::TgaFile& tgaFile, const Image::GtRgbFile& gtRgbFile)
    {
        if(Compiler::getCodeRomType() < Cpu::ROMv3)
        {
            std::string romTypeStr;
            getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
            loadUsage(LoadSprite, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : version error, 'LOAD SPRITE' requires ROMv3 or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
            return false;
        }

        if(tokens.size() < 3  ||  tokens.size() > 5)
        {
            loadUsage(LoadSprite, codeLine, codeLineStart);
            return false;
        }

        if(gtRgbFile._header._width % SPRITE_CHUNK_SIZE != 0)
        {
            loadUsage(LoadSprite, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : sprite width not a multiple of %d, (%d x %d), for %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, SPRITE_CHUNK_SIZE, gtRgbFile._header._width, gtRgbFile._header._height, filename.c_str(), codeLine._text.c_str());
            return false;
        }

        if(tokens.size() < 3)
        {
            loadUsage(LoadSprite, codeLine, codeLineStart);
            return false;
        }

        // Unique sprite ID
        std::string idToken = tokens[2];
        Expression::Numeric idNumeric;
        std::string idOperand;
        if(Compiler::parseStaticExpression(codeLineIndex, idToken, idOperand, idNumeric) == Compiler::OperandInvalid)
        {
            fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, idToken.c_str(), codeLine._text.c_str());
            return false;
        }
        int spriteId = int(std::lround(idNumeric._value));
        if(Compiler::getDefDataSprites().find(spriteId) != Compiler::getDefDataSprites().end())
        {
            loadUsage(LoadSprite, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : sprite id %d not unique : %s\n", codeLine._moduleName.c_str(), codeLineStart, spriteId, codeLine._text.c_str());
            return false;
        }

        // Parse optional sprite flip
        Compiler::SpriteFlipType flipType = Compiler::NoFlip;
        if(tokens.size() >= 4)
        {
            std::string flipToken = tokens[3];
            Expression::stripWhitespace(flipToken);
            Expression::strToUpper(flipToken);
            if(flipToken == "NOFLIP")      flipType = Compiler::NoFlip;
            else if(flipToken == "FLIPX")  flipType = Compiler::FlipX;
            else if(flipToken == "FLIPY")  flipType = Compiler::FlipY;
            else if(flipToken == "FLIPXY") flipType = Compiler::FlipXY;
            else
            {
                loadUsage(LoadSprite, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : unknown sprite flip type, %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, flipToken.c_str(), codeLine._text.c_str());
                fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : must use one of 'NOFLIP', 'FLIPX', 'FLIPY', 'FLIPXY' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
                return false;
            }
        }

        // Parse optional sprite overlap, (for last column)
        uint16_t overlap = 0;
        if(tokens.size() == 5)
        {
            std::string overlapToken = tokens[4];
            Expression::Numeric overlapNumeric;
            std::string overlapOperand;
            if(Compiler::parseStaticExpression(codeLineIndex, overlapToken, overlapOperand, overlapNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, overlapToken.c_str(), codeLine._text.c_str());
                return false;
            }
            overlap = uint16_t(std::lround(overlapNumeric._value));
        }

        // Build sprite data from image data
        uint16_t numColumns = gtRgbFile._header._width / SPRITE_CHUNK_SIZE;
        uint16_t remStripeChunks = gtRgbFile._header._height % Compiler::getSpriteStripeChunks();
        uint16_t numStripesPerCol = gtRgbFile._header._height / Compiler::getSpriteStripeChunks() + int(remStripeChunks > 0);
        uint16_t numStripeChunks = (numStripesPerCol == 1) ? gtRgbFile._header._height : Compiler::getSpriteStripeChunks();
        std::vector<uint16_t> stripeAddrs;
        std::vector<uint8_t> spriteData;

        if(numColumns == 1  &&  overlap)
        {
            loadUsage(LoadSprite, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : can't have a non zero overlap with a single column sprite : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Search sprite image for instancing
        int parentInstance = 0;
        bool isInstanced = false;
        for(auto it=Compiler::getDefDataSprites().begin(); it!=Compiler::getDefDataSprites().end(); ++it)
        {
            if(it->second._filename == filename)
            {
                spriteData = it->second._data;
                parentInstance = it->first;
                isInstanced = true;
                break;
            }
        }

        // Allocate sprite memory
        int addrIndex = 0;
        uint16_t address = 0x0000;
        for(int i=0; i<numColumns; i++)
        {
            // One stripe per column
            if(numStripesPerCol == 1)
            {
                if(isInstanced)
                {
                    address = Compiler::getDefDataSprites()[parentInstance]._stripeAddrs[addrIndex];
                    addrIndex += 2;
                }
                else
                {
                    if(!Memory::getFreeRAM(Compiler::getSpriteStripeFitType(), numStripeChunks*SPRITE_CHUNK_SIZE + 1, Compiler::getSpriteStripeMinAddress(), Compiler::getRuntimeStart(), address))
                    {
                        loadUsage(LoadSprite, codeLine, codeLineStart);
                        fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : getting sprite memory for stripe %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(stripeAddrs.size()/2 + 1), codeLine._text.c_str());
                        return false;
                    }
                }
                stripeAddrs.push_back(address);

                // Destination offsets
                switch(flipType)
                {
                    case Compiler::NoFlip: stripeAddrs.push_back(uint16_t(0 + i*6));
                                           if(i == numColumns - 1) stripeAddrs.back() -= overlap;  // push last column closer to all other columns
                    break;

                    case Compiler::FlipX: stripeAddrs.push_back(uint16_t(0 + (numColumns-1-i)*6));
                                          if(i != numColumns - 1) stripeAddrs.back() -= overlap;  // push all other columns closer to last column
                    break;

                    case Compiler::FlipY: stripeAddrs.push_back(uint16_t((numStripeChunks-1)*256 + i*6));
                                          if(i == numColumns - 1) stripeAddrs.back() -= overlap;  // push last column closer to all other columns
                    break;

                    case Compiler::FlipXY: stripeAddrs.push_back(uint16_t((numStripeChunks-1)*256 + (numColumns-1-i)*6));
                                           if(i != numColumns - 1) stripeAddrs.back() -= overlap;  // push all other columns closer to last column
                    break;

                    default: break;
                }

                // Copy sprite data and delimiter
                for(int j=0; j<numStripeChunks; j++)
                {
                    for(int k=0; k<SPRITE_CHUNK_SIZE; k++)
                    {
                        spriteData.push_back(gtRgbFile._data[i*SPRITE_CHUNK_SIZE + j*SPRITE_CHUNK_SIZE*numColumns + k]);
                    }
                }
                spriteData.push_back(uint8_t(-gtRgbFile._header._height));
            }
            // Multiple stripes per column
            else
            {
                // MAX_SPRITE_CHUNKS_PER_STRIPE stripes
                for(int j=0; j<numStripesPerCol-1; j++)
                {
                    if(isInstanced)
                    {
                        address = Compiler::getDefDataSprites()[parentInstance]._stripeAddrs[addrIndex];
                        addrIndex += 2;
                    }
                    else
                    {
                        if(!Memory::getFreeRAM(Compiler::getSpriteStripeFitType(), numStripeChunks*SPRITE_CHUNK_SIZE + 1, Compiler::getSpriteStripeMinAddress(), Compiler::getRuntimeStart(), address))
                        {
                            loadUsage(LoadSprite, codeLine, codeLineStart);
                            fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : getting sprite memory failed for stripe %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(stripeAddrs.size()/2 + 1), codeLine._text.c_str());
                            return false;
                        }
                    }
                    stripeAddrs.push_back(address);

                    // Destination offsets
                    switch(flipType)
                    {
                        case Compiler::NoFlip: stripeAddrs.push_back(uint16_t(j*numStripeChunks*256 + i*6));
                                               if(i == numColumns - 1) stripeAddrs.back() -= overlap;  // push last column closer to all other columns
                        break;

                        case Compiler::FlipX: stripeAddrs.push_back(uint16_t(j*numStripeChunks*256 + (numColumns-1-i)*6));
                                              if(i != numColumns - 1) stripeAddrs.back() -= overlap;  // push all other columns closer to last column
                        break;

                        case Compiler::FlipY: stripeAddrs.push_back(uint16_t(((numStripesPerCol-1-j)*numStripeChunks+remStripeChunks-1)*256 + i*6));
                                              if(i == numColumns - 1) stripeAddrs.back() -= overlap;  // push last column closer to all other columns
                        break;

                        case Compiler::FlipXY: stripeAddrs.push_back(uint16_t(((numStripesPerCol-1-j)*numStripeChunks+remStripeChunks-1)*256 + (numColumns-1-i)*6));
                                               if(i != numColumns - 1) stripeAddrs.back() -= overlap;  // push all other columns closer to last column
                        break;

                        default: break;
                    }

                    // Copy sprite data and delimiter
                    for(int k=0; k<numStripeChunks; k++)
                    {
                        for(int l=0; l<SPRITE_CHUNK_SIZE; l++)
                        {
                            spriteData.push_back(gtRgbFile._data[i*SPRITE_CHUNK_SIZE + j*numStripeChunks*SPRITE_CHUNK_SIZE*numColumns + k*SPRITE_CHUNK_SIZE*numColumns + l]);
                        }
                    }
                    spriteData.push_back(255);
                }

                // Remainder stripe
                if(isInstanced)
                {
                    address = Compiler::getDefDataSprites()[parentInstance]._stripeAddrs[addrIndex];
                    addrIndex += 2;
                }
                else
                {
                    if(!Memory::getFreeRAM(Compiler::getSpriteStripeFitType(), remStripeChunks*SPRITE_CHUNK_SIZE + 1, Compiler::getSpriteStripeMinAddress(), Compiler::getRuntimeStart(), address))
                    {
                        loadUsage(LoadSprite, codeLine, codeLineStart);
                        fprintf(stderr, "Keywords::loadSprite() : '%s:%d' : getting sprite memory failed for stripe %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(stripeAddrs.size()/2 + 1), codeLine._text.c_str());
                        return false;
                    }
                }
                stripeAddrs.push_back(address);

                // Destination offsets
                switch(flipType)
                {
                    case Compiler::NoFlip: stripeAddrs.push_back(uint16_t((numStripesPerCol-1)*numStripeChunks*256 + i*6));
                                           if(i == numColumns - 1) stripeAddrs.back() -= overlap;  // push last column closer to all other columns
                    break;

                    case Compiler::FlipX: stripeAddrs.push_back(uint16_t((numStripesPerCol-1)*numStripeChunks*256 + (numColumns-1-i)*6));
                                          if(i != numColumns - 1) stripeAddrs.back() -= overlap;  // push all other columns closer to last column
                    break;

                    case Compiler::FlipY: stripeAddrs.push_back(uint16_t((remStripeChunks-1)*256 + i*6));
                                          if(i == numColumns - 1) stripeAddrs.back() -= overlap;  // push last column closer to all other columns
                    break;

                    case Compiler::FlipXY: stripeAddrs.push_back(uint16_t((remStripeChunks-1)*256 + (numColumns-1-i)*6));
                                           if(i != numColumns - 1) stripeAddrs.back() -= overlap;  // push all other columns closer to last column
                    break;

                    default: break;
                }

                // Copy sprite data and delimiter
                for(int j=0; j<remStripeChunks; j++)
                {
                    for(int k=0; k<SPRITE_CHUNK_SIZE; k++)
                    {
                        spriteData.push_back(gtRgbFile._data[i*SPRITE_CHUNK_SIZE + (numStripesPerCol-1)*numStripeChunks*SPRITE_CHUNK_SIZE*numColumns + j*SPRITE_CHUNK_SIZE*numColumns + k]);
                    }
                }
                spriteData.push_back(255);
            }
        }

        Compiler::getDefDataSprites()[spriteId] = {spriteId, filename, tgaFile._header._width, tgaFile._header._height, numColumns, numStripesPerCol, numStripeChunks, remStripeChunks, stripeAddrs, spriteData, flipType, isInstanced};

        return true;
    }
    /********************************************************************************************************************************/
    /* Load font                                                                                */
    /********************************************************************************************************************************/
    bool loadFont(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, const std::vector<std::string>& tokens, const std::string& filename, const Image::TgaFile& tgaFile, const Image::GtRgbFile& gtRgbFile)
    {
        if(Compiler::getCodeRomType() < Cpu::ROMv3)
        {
            std::string romTypeStr;
            getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
            loadUsage(LoadFont, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : version error, 'LOAD FONT' requires ROMv3 or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
            return false;
        }

        if(tokens.size() < 3  ||  tokens.size() > 4)
        {
            loadUsage(LoadFont, codeLine, codeLineStart);
            return false;
        }

        // Unique font ID
        std::string idToken = tokens[2];
        Expression::Numeric idNumeric;
        std::string idOperand;
        if(Compiler::parseStaticExpression(codeLineIndex, idToken, idOperand, idNumeric) == Compiler::OperandInvalid)
        {
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, idToken.c_str(), codeLine._text.c_str());
            return false;
        }
        int fontId = int(std::lround(idNumeric._value));
        if(Compiler::getDefDataFonts().find(fontId) != Compiler::getDefDataFonts().end())
        {
            loadUsage(LoadFont, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : font id %d not unique : %s\n", codeLine._moduleName.c_str(), codeLineStart, fontId, codeLine._text.c_str());
            return false;
        }

        // Foreground/background colours
        uint16_t fgbgColour = 0x0000;
        if(tokens.size() == 4)
        {
            std::string fgbgToken = tokens[3];
            Expression::Numeric fgbgNumeric;
            std::string fgbgOperand;
            if(Compiler::parseStaticExpression(codeLineIndex, fgbgToken, fgbgOperand, fgbgNumeric) == Compiler::OperandInvalid)
            {
                fprintf(stderr, "Keywords::loadFont() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, fgbgToken.c_str(), codeLine._text.c_str());
                return false;
            }
            fgbgColour = uint16_t(std::lround(fgbgNumeric._value));
        }

        // Width
        if(gtRgbFile._header._width % FONT_WIDTH != 0)
        {
            loadUsage(LoadFont, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : font width %d is not a multiple of %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, gtRgbFile._header._width, FONT_WIDTH, codeLine._text.c_str());
            return false;
        }

        // Height
        if(gtRgbFile._header._height % FONT_HEIGHT != 0)
        {
            loadUsage(LoadFont, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : font height %d is not a multiple of %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, gtRgbFile._header._height, FONT_HEIGHT, codeLine._text.c_str());
            return false;
        }

        // Load font mapping file
        bool foundMapFile = true;
        size_t nameSuffix = filename.find_last_of(".");
        std::string filenameMap = filename.substr(0, nameSuffix) + ".map";
        std::ifstream infile(filenameMap, std::ios::in);
        if(!infile.is_open())
        {
            foundMapFile = false;
        }

        // Parse font mapping file
        int maxIndex = -1;
        uint16_t mapAddr = 0x0000;
        std::vector<uint8_t> mapping(MAPPING_SIZE);
        if(foundMapFile)
        {
            int ascii, index, line = 0;
            while(!infile.eof())
            {
                infile >> ascii >> index;
                if(index > maxIndex) maxIndex = index;
                if(!infile.good() && !infile.eof())
                {
                    loadUsage(LoadFont, codeLine, codeLineStart);
                    fprintf(stderr, "Keywords::loadFont() : '%s:%d' : error in mapping file %s on line %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, filenameMap.c_str(), line + 1, codeLine._text.c_str());
                    return false;
                }

                if(line >= MAPPING_SIZE) break;
                mapping[line++] = uint8_t(index);
            }

            if(line != MAPPING_SIZE)
            {
                loadUsage(LoadFont, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadFont() : '%s:%d' : warning, found an incorrect number of map entries %d for file %s, should be %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, line - 1, filenameMap.c_str(), MAPPING_SIZE, codeLine._text.c_str());
                return false;
            }

            if(!Memory::getFreeRAM(Memory::FitDescending, MAPPING_SIZE, USER_CODE_START, Compiler::getRuntimeStart(), mapAddr))
            {
                loadUsage(LoadFont, codeLine, codeLineStart);
                fprintf(stderr, "Keywords::loadFont() : '%s:%d' : getting mapping memory for map size of %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, MAPPING_SIZE, codeLine._text.c_str());
                return false;
            }
        }

        // 8th line is implemented as a separate sprite call, to save memory and allow for more efficient memory packing
        const int kCharHeight = FONT_HEIGHT-1;

        // Copy font data and create delimiter
        std::vector<uint8_t> charData;
        std::vector<uint16_t> charAddrs;
        std::vector<std::vector<uint8_t>> fontData;
        for(int j=0; j<tgaFile._header._height; j+=FONT_HEIGHT)
        {
            for(int i=0; i<tgaFile._header._width; i+=FONT_WIDTH)
            {
                for(int l=0; l<kCharHeight; l++)
                {
                    for(int k=0; k<FONT_WIDTH; k++)
                    {
                        uint8_t pixel = gtRgbFile._data[j*tgaFile._header._width + i + l*tgaFile._header._width + k];
                        if(fgbgColour)
                        {
                            if(pixel == 0x00) pixel = fgbgColour & 0x00FF;
                            if(pixel == 0x3F) pixel = fgbgColour >> 8;
                        }
                        charData.push_back(pixel);
                    }
                }
                charData.push_back(uint8_t(-(kCharHeight)));
                fontData.push_back(charData);
                charData.clear();

                uint16_t address = 0x0000;
                if(!Memory::getFreeRAM(Memory::FitDescending, (kCharHeight)*FONT_WIDTH + 1, USER_CODE_START, Compiler::getRuntimeStart(), address))
                {
                    loadUsage(LoadFont, codeLine, codeLineStart);
                    fprintf(stderr, "Keywords::loadFont() : '%s:%d' : getting font memory for char %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(fontData.size() - 1), codeLine._text.c_str());
                    return false;
                }

                charAddrs.push_back(address);
            }
        }

        if(foundMapFile  &&  maxIndex + 1 != int(fontData.size()))
        {
            loadUsage(LoadFont, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : font mapping table does not match font data, found a mapping count of %d and a chars count of %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, maxIndex + 1, int(fontData.size()), codeLine._text.c_str());
            return false;
        }

        // Create baseline for all chars in each font
        uint16_t baseAddr = 0x0000;
        if(!Memory::getFreeRAM(Memory::FitDescending, FONT_WIDTH + 1, USER_CODE_START, Compiler::getRuntimeStart(), baseAddr))
        {
            loadUsage(LoadFont, codeLine, codeLineStart);
            fprintf(stderr, "Keywords::loadFont() : '%s:%d' : getting font memory for char %d failed : %s\n", codeLine._moduleName.c_str(), codeLineStart, int(fontData.size() - 1), codeLine._text.c_str());
            return false;
        }

        Compiler::getDefDataFonts()[fontId] = {fontId, filenameMap, tgaFile._header._width, tgaFile._header._height, charAddrs, fontData, mapAddr, mapping, baseAddr, fgbgColour};

        Linker::enableFontLinking();

        return true;
    }
    bool LOAD(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 2  ||  tokens.size() > 5)
        {
            loadUsage(LoadType, codeLine, codeLineStart);
            return false;
        }

        // Type
        Expression::strToUpper(tokens[0]);
        Expression::stripWhitespace(tokens[0]);

        // Load WAVE
        if(tokens[0] == "WAVE")
        {
            return loadWave(codeLine, codeLineIndex, codeLineStart, tokens);
        }

        // Load Midi
        if(tokens[0] == "MIDI")
        {
            return loadMidi(codeLine, codeLineIndex, codeLineStart, tokens);
        }

        // Load Image, Sprite and Font
        if(tokens[0] == "IMAGE"  ||  tokens[0] == "SPRITE"  ||  tokens[0] == "FONT")
        {
            std::string filename = tokens[1];
            Expression::stripWhitespace(filename);
            std::string ext = filename;
            Expression::strToUpper(ext);
            if(ext.find(".TGA") != std::string::npos)
            {
                std::string filepath = Loader::getFilePath();
                size_t slash = filepath.find_last_of("\\/");
                filepath = (slash != std::string::npos) ? filepath.substr(0, slash) : ".";
                filename = filepath + "/" + filename;
                Image::TgaFile tgaFile;

                //fprintf(stderr, "\nKeywords::LOAD() : %s\n", filename.c_str());

                if(!Image::loadTgaFile(filename, tgaFile))
                {
                    fprintf(stderr, "Keywords::LOAD() : '%s:%d' : file '%s' failed to load : %s\n", codeLine._moduleName.c_str(), codeLineStart, filename.c_str(), codeLine._text.c_str());
                    return false;
                }

                // Load image/sprite/font
                std::vector<uint8_t> data;
                std::vector<uint16_t> optional;
                Image::GtRgbFile gtRgbFile{GTRGB_IDENTIFIER, Image::GT_RGB_222, tgaFile._header._width, tgaFile._header._height, data, optional};
                Image::convertRGB8toRGB2(tgaFile._data, gtRgbFile._data, tgaFile._header._width, tgaFile._header._height, tgaFile._imageOrigin);

                // Image
                if(tokens[0] == "IMAGE")
                {
                    return loadImage(codeLine, codeLineIndex, codeLineStart, tokens, filename, tgaFile, gtRgbFile);
                }

                // Sprite
                if(tokens[0] == "SPRITE")
                {
                    return loadSprite(codeLine, codeLineIndex, codeLineStart, tokens, filename, tgaFile, gtRgbFile);
                }

                // Font
                if(tokens[0] == "FONT")
                {
                    return loadFont(codeLine, codeLineIndex, codeLineStart, tokens, filename, tgaFile, gtRgbFile);
                }
            }
        }

        loadUsage(LoadType, codeLine, codeLineStart);
        return false;
    }

    bool SPRITE(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        if(Compiler::getCodeRomType() < Cpu::ROMv3)
        {
            std::string romTypeStr;
            getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
            fprintf(stderr, "Keywords::SPRITE() : '%s:%d' : version error, 'SPRITE' requires ROMv3 or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
            return false;
        }

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 4)
        {
            fprintf(stderr, "Keywords::SPRITE() : '%s:%d' : syntax error, use 'SPRITE <NOFLIP/FLIPX/FLIPY/FLIPXY>, <id>, <x pos>, <y pos>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Flip type
        static std::map<std::string, Compiler::SpriteFlipType> flipType = {{"NOFLIP", Compiler::NoFlip}, {"FLIPX", Compiler::FlipX}, {"FLIPY", Compiler::FlipY}, {"FLIPXY", Compiler::FlipXY}};
        std::string flipToken = tokens[0];
        Expression::stripWhitespace(flipToken);
        Expression::strToUpper(flipToken);
        if(flipType.find(flipToken) == flipType.end())
        {
            fprintf(stderr, "Keywords::SPRITE() : '%s:%d' : syntax error, use one of the correct flip types, 'SPRITE <NOFLIP/FLIPX/FLIPY/FLIPXY>, <id>, <x pos>, <y pos>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Sprite identifier
        std::string idToken = tokens[1];
        Expression::Numeric idParam;
        if(Compiler::parseExpression(codeLineIndex, idToken, idParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::SPRITE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, idToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "spriteId", false);

        // Sprite X position
        std::string xposToken = tokens[2];
        Expression::Numeric xposParam;
        if(Compiler::parseExpression(codeLineIndex, xposToken, xposParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::SPRITE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, xposToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("ST", "spriteXY", false);

        // Sprite Y position
        std::string yposToken = tokens[3];
        Expression::Numeric yposParam;
        if(Compiler::parseExpression(codeLineIndex, yposToken, yposParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::SPRITE() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, yposToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("ADDI", "8", false);
        Compiler::emitVcpuAsm("ST", "spriteXY + 1", false);

        // Draw sprite
        switch(flipType[flipToken])
        {
            case Compiler::NoFlip: Compiler::emitVcpuAsm("%DrawSprite",   "", false); break;
            case Compiler::FlipX:  Compiler::emitVcpuAsm("%DrawSpriteX",  "", false); break;
            case Compiler::FlipY:  Compiler::emitVcpuAsm("%DrawSpriteY",  "", false); break;
            case Compiler::FlipXY: Compiler::emitVcpuAsm("%DrawSpriteXY", "", false); break;
        }

        return true;
    }

    void usageSOUND(int msgType, Compiler::CodeLine& codeLine, int codeLineStart)
    {
        switch(msgType)
        {
            case 0: fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error, use 'SOUND <TYPE>, <params>, where <TYPE> = 'ON', 'MOD' or 'OFF' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());               break;
            case 1: fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error, use 'SOUND ON, <channel>, <frequency>, <optional volume>, <optional waveform>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str()); break;
            case 2: fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error, use 'SOUND MOD, <channel>, <wavX>, <optional wavA>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());                            break;
            case 3: fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error, use 'SOUND OFF, <optional channel>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());                                            break;

            default: break;
        }
    }
    bool SOUND(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 5)
        {
            usageSOUND(0, codeLine, codeLineStart);
            return false;
        }

        // Sound state
        std::string stateToken = tokens[0];
        Expression::stripWhitespace(stateToken);
        Expression::strToUpper(stateToken);

        // Sound channel, (has to be between 1 and 4, saves an ADD/INC, no checking done)
        if(tokens.size() >= 2)
        {
            std::string chanToken = tokens[1];
            Expression::Numeric chanParam;
            if(Compiler::parseExpression(codeLineIndex, chanToken, chanParam) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, chanToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "sndChannel + 1", false);
        }

        // Sound channels on
        if(stateToken == "ON")
        {
            if(tokens.size() < 3)
            {
                usageSOUND(1, codeLine, codeLineStart);
                return false;
            }

            std::string freqToken = tokens[2];
            Expression::Numeric freqParam;
            if(Compiler::parseExpression(codeLineIndex, freqToken, freqParam) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, freqToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "sndFrequency", false);

            if(tokens.size() == 3)
            {
                Compiler::emitVcpuAsm("%SoundOn", "", false);
                return true;
            }

            std::string volToken = tokens[3];
            Expression::Numeric volParam;
            if(Compiler::parseExpression(codeLineIndex, volToken, volParam) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, volToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "sndVolume", false);

            if(tokens.size() == 4)
            {
                Compiler::emitVcpuAsm("LDI", "2",           false);
                Compiler::emitVcpuAsm("STW", "sndWaveType", false);
                Compiler::emitVcpuAsm("%SoundOnV", "",      false);
                return true;
            }

            std::string wavToken = tokens[4];
            Expression::Numeric wavParam;
            if(Compiler::parseExpression(codeLineIndex, wavToken, wavParam) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, wavToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "sndWaveType", false);
            Compiler::emitVcpuAsm("%SoundOnV", "",      false);

            return true;
        }

        // Sound channels modulation
        if(stateToken == "MOD")
        {
            if(tokens.size() < 3  ||  tokens.size() > 4)
            {
                usageSOUND(2, codeLine, codeLineStart);
                return false;
            }

            std::string waveXToken = tokens[2];
            Expression::Numeric waveXParam;
            if(Compiler::parseExpression(codeLineIndex, waveXToken, waveXParam) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, waveXToken.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "sndWaveType + 1", false);

            if(tokens.size() == 4)
            {
                std::string waveAToken = tokens[3];
                Expression::Numeric waveAParam;
                if(Compiler::parseExpression(codeLineIndex, waveAToken, waveAParam) == Expression::IsInvalid)
                {
                    fprintf(stderr, "Keywords::SOUND() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, waveAToken.c_str(), codeLine._text.c_str());
                    return false;
                }
                Compiler::emitVcpuAsm("ST", "sndWaveType", false);
            }
            // Reset waveA
            else
            {
                Compiler::emitVcpuAsm("LDI", "0",          false);
                Compiler::emitVcpuAsm("ST", "sndWaveType", false);
            }

            Compiler::emitVcpuAsm("%SoundMod", "", false);
            return true;
        }

        // Sound channels off
        if(stateToken == "OFF")
        {
            if(tokens.size() > 2)
            {
                usageSOUND(3, codeLine, codeLineStart);
                return false;
            }

            // All sound channels off
            if(tokens.size() == 1)
            {
                Compiler::emitVcpuAsm("%SoundAllOff", "", false);
                return true;
            }
            // Single channel off
            else
            {
                Compiler::emitVcpuAsm("%SoundOff", "", false);
                return true;
            }
        }

        usageSOUND(0, codeLine, codeLineStart);
        return false;
    }

    void usageSET(Compiler::CodeLine& codeLine, int codeLineStart)
    {
        fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error, use 'SET <VAR NAME>, <PARAM>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
    }
    bool SET(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 3)
        {
            usageSET(codeLine, codeLineStart);
            return false;
        }

        // System variable to set
        std::string sysVarName = tokens[0];
        Expression::stripWhitespace(sysVarName);
        Expression::strToUpper(sysVarName);

        // First parameter after system var name
        std::string token1;
        Expression::Numeric param1;
        if(tokens.size() >= 2) token1 = tokens[1];

        // Second parameter after system var name
        std::string token2;
        Expression::Numeric param2;
        if(tokens.size() >= 3) token2 = tokens[2];

        // Font id variable
        if(sysVarName == "FONT_ID"  &&  tokens.size() == 2)
        {
            if(Compiler::getCodeRomType() < Cpu::ROMv3)
            {
                std::string romTypeStr;
                getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
                fprintf(stderr, "Keywords::SET() : '%s:%d' : version error, 'SET FONTID' requires ROMv3 or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
                return false;
            }

            Compiler::emitVcpuAsm("LDWI", "_fontId_", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "TIME_MODE")
        {
            Compiler::emitVcpuAsm("LDWI", "handleT_mode + 1", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "TIME_EPOCH")
        {
            Compiler::emitVcpuAsm("LDWI", "handleT_epoch + 1", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "TIME_S")
        {
            Compiler::emitVcpuAsm("LDWI", "_timeArray_ + 0", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "TIME_M")
        {
            Compiler::emitVcpuAsm("LDWI", "_timeArray_ + 1", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "TIME_H")
        {
            Compiler::emitVcpuAsm("LDWI", "_timeArray_ + 2", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "TIMER")
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "timerTick", false);
            return true;
        }
        else if(sysVarName == "VBLANK_PROC")
        {
            if(Compiler::getCodeRomType() < Cpu::ROMv5a)
            {
                std::string romTypeStr;
                getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
                fprintf(stderr, "Keywords::SET() : '%s:%d' : version error, 'SET VBLANK_PROC' requires ROMv5a or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
                return false;
            }

            Compiler::emitVcpuAsm("LDWI", Expression::wordToHexString(VBLANK_PROC), false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("DOKE", "register0", false);
            return true;
        }
        else if(sysVarName == "VBLANK_FREQ")
        {
            if(Compiler::getCodeRomType() < Cpu::ROMv5a)
            {
                std::string romTypeStr;
                getRomTypeStr(Compiler::getCodeRomType(), romTypeStr);
                fprintf(stderr, "Keywords::SET() : '%s:%d' : version error, 'SET VBLANK_FREQ' requires ROMv5a or higher, you are trying to link against '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, romTypeStr.c_str(), codeLine._text.c_str());
                return false;
            }

#if 0
            // TODO: Fix this
            // (256 - n) = vblank interrupt frequency, where n = 1 to 255
            Compiler::emitVcpuAsm("LDWI", "realTS_rti + 2", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "register1", false);
            Compiler::emitVcpuAsm("LDWI", "256", false);
            Compiler::emitVcpuAsm("SUBW", "register1", false);
            Compiler::emitVcpuAsm("POKE", "register0", false);
#endif
            return true;
        }
        else if(sysVarName == "CURSOR_X"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "cursorXY", false);
            return true;
        }
        else if(sysVarName == "CURSOR_Y"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "cursorXY + 1", false);
            return true;
        }
        else if(sysVarName == "CURSOR_XY"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "cursorXY", false);
            return true;
        }
        else if(sysVarName == "FG_COLOUR"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "fgbgColour + 1", false);
            return true;
        }
        else if(sysVarName == "BG_COLOUR"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "fgbgColour", false);
            return true;
        }
        else if(sysVarName == "FGBG_COLOUR"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "fgbgColour", false);
            return true;
        }
        else if(sysVarName == "MIDI_STREAM"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("STW", "midiStream", false);
            return true;
        }
        else if(sysVarName == "VIDEO_TOP"  &&  tokens.size() == 2)
        {
            Compiler::emitVcpuAsm("LDWI", "giga_videoTop", false);
            Compiler::emitVcpuAsm("STW", "register0", false);
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("POKE", "register0", false);
            return true;
        }
        else if(sysVarName == "LED_TEMPO"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_ledTempo", false);
            return true;
        }
        else if(sysVarName == "LED_STATE"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_ledState", false);
            return true;
        }
        else if(sysVarName == "SOUND_TIMER"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_soundTimer", false);
            return true;
        }
        else if(sysVarName == "CHANNEL_MASK"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_channelMask", false);
            return true;
        }
        else if(sysVarName == "XOUT_MASK"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_xoutMask", false);
            return true;
        }
        else if(sysVarName == "BUTTON_STATE"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_buttonState", false);
            return true;
        }
        else if(sysVarName == "FRAME_COUNT"  &&  tokens.size() == 2)
        {
            if(Compiler::parseExpression(codeLineIndex, token1, param1) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::SET() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, token1.c_str(), codeLine._text.c_str());
                return false;
            }
            Compiler::emitVcpuAsm("ST", "giga_frameCount", false);
            return true;
        }

        usageSET(codeLine, codeLineStart);
        return false;
    }

    bool ASM(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);

        // If ASM is attached to a label, propagate it to the generated vCPU code, (if sub/proc/func already has a PUSH, then label is already valid, so ignore)
        if(codeLine._labelIndex > -1  &&  !Compiler::getLabels()[codeLine._labelIndex]._gosub)
        {
            Compiler::setNextInternalLabel("_" + Compiler::getLabels()[codeLine._labelIndex]._name);
        }

        Compiler::setCodeIsAsm(true);

        return true;
    }

    bool ENDASM(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);

        Compiler::setCodeIsAsm(false);

        return true;
    }

    bool BCDADD(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::BCDADD() : '%s:%d' : syntax error, use 'BCDADD <src bcd address>, <dst bcd address>, <length>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // BCD src address
        std::string srcToken = tokens[0];
        Expression::Numeric srcParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, srcToken, srcParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDADD() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, srcToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdSrcAddr", false);

        // BCD dst address
        std::string dstToken = tokens[1];
        Expression::Numeric dstParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, dstToken, dstParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDADD() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, dstToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdDstAddr", false);

        // BCD length
        std::string lenToken = tokens[2];
        Expression::Numeric lenParam;
        if(Compiler::parseExpression(codeLineIndex, lenToken, lenParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDADD() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, lenToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("%BcdAdd", "", false);

        return true;
    }

    bool BCDSUB(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::BCDSUB() : '%s:%d' : syntax error, use 'BCDSUB <src bcd address>, <dst bcd address>, <length>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // BCD src address
        std::string srcToken = tokens[0];
        Expression::Numeric srcParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, srcToken, srcParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDSUB() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, srcToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdSrcAddr", false);

        // BCD dst address
        std::string dstToken = tokens[1];
        Expression::Numeric dstParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, dstToken, dstParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDSUB() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, dstToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdDstAddr", false);

        // BCD length
        std::string lenToken = tokens[2];
        Expression::Numeric lenParam;
        if(Compiler::parseExpression(codeLineIndex, lenToken, lenParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDSUB() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, lenToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("%BcdSub", "", false);

        return true;
    }

    bool BCDINT(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 2)
        {
            fprintf(stderr, "Keywords::BCDINT() : '%s:%d' : syntax error, use 'BCDINT <dst bcd address>, <int>' bcd value MUST contain at least 5 digits : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // BCD dst address
        std::string srcToken = tokens[0];
        Expression::Numeric srcParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, srcToken, srcParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDINT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, srcToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdDstAddr", false);

        // Integer value, must be +ve, max value 42767, (32767 + 10000 because of how vASM sub Numeric::bcdInt works)
        std::string intToken = tokens[1];
        Expression::Numeric intParam;
        if(Compiler::parseExpression(codeLineIndex, intToken, intParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDINT() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, intToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("%BcdInt", "", false);

        return true;
    }

    bool BCDCPY(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 3)
        {
            fprintf(stderr, "Keywords::BCDCPY() : '%s:%d' : syntax error, use 'BCDCPY <src bcd address>, <dst bcd address>, <length>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // BCD src address
        std::string srcToken = tokens[0];
        Expression::Numeric srcParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, srcToken, srcParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDCPY() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, srcToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdSrcAddr", false);

        // BCD dst address
        std::string dstToken = tokens[1];
        Expression::Numeric dstParam(true); // true = allow static init
        if(Compiler::parseExpression(codeLineIndex, dstToken, dstParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDCPY() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, dstToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "bcdDstAddr", false);

        // BCD length
        std::string lenToken = tokens[2];
        Expression::Numeric lenParam;
        if(Compiler::parseExpression(codeLineIndex, lenToken, lenParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::BCDCPY() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, lenToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("%BcdCpy", "", false);

        return true;
    }

    bool GPRINTF(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        // Produces vCPU code and allocates Gigatron memory only when compiling on the emulator
#ifdef STAND_ALONE
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(foundPos);
        UNREFERENCED_PARAM(tokenIndex);
        UNREFERENCED_PARAM(codeLineIndex);
        UNREFERENCED_PARAM(codeLineStart);
        UNREFERENCED_PARAM(codeLine);
#else
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        // Allocate memory for gprintf variable's addresses if it hasn't been allocated already
        uint16_t varsAddr = 0x0000;
        if(Compiler::getGprintfVarsAddr() == 0x0000)
        {
            if(!Memory::getFreeRAM(Memory::FitDescending, GPRINT_VAR_ADDRS*2, USER_CODE_START, Compiler::getRuntimeStart(), varsAddr))
            {
                fprintf(stderr, "Keywords::GPRINTF() : '%s:%d' : not enough RAM for variables LUT of size %d : %s\n", codeLine._moduleName.c_str(), codeLineStart, GPRINT_VAR_ADDRS*2, codeLine._text.c_str());
                return false;
            }
            Compiler::setGprintfVarsAddr(varsAddr);
        }

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',');
        if(tokens.size() < 2)
        {
            fprintf(stderr, "Keywords::GPRINTF() : '%s:%d' : syntax error, use 'GPRINTF \"<format string>\", <var1>, ... <varN>' : %s\n", codeLine._text.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        if(tokens.size() > GPRINT_VAR_ADDRS)
        {
            fprintf(stderr, "Keywords::GPRINTF() : '%s:%d' : maximum number of vars is '%d', found '%d' vars : %s\n", codeLine._moduleName.c_str(), codeLineStart, GPRINT_VAR_ADDRS, int(tokens.size()), codeLine._text.c_str());
            return false;
        }

        // Format string
        std::string formatStr = tokens[0];
        Expression::stripNonStringWhitespace(formatStr);
        if(formatStr[0] != '\"'  ||  formatStr.back() != '\"')
        {
            fprintf(stderr, "Keywords::GPRINTF() : '%s:%d' : syntax error in string format, use 'GPRINTF \"<format string>\", <var1>, ... <varN>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Vars
        Expression::Numeric numeric;
        std::vector<std::string> variables;
        for(int i=1; i<int(tokens.size()); i++)
        {
            uint16_t address = varsAddr + uint16_t((i-1)*2);
            if(i == 1)
            {
                Compiler::emitVcpuAsm("LDWI", Expression::wordToHexString(address), false);
                Compiler::emitVcpuAsm("STW", "memDstAddr", false);
            }
            else
            {
                Compiler::emitVcpuAsm("INC", "memDstAddr", false);
                Compiler::emitVcpuAsm("INC", "memDstAddr", false);
            }

            // Convert GBAS format to ASM format
            variables.push_back("*" + Expression::wordToHexString(address));

            if(!Expression::parse(tokens[i], codeLineIndex, numeric))
            {
                fprintf(stderr, "Keywords::GPRINTF() : '%s:%d' : syntax error in '%s' : %s\n", codeLine._moduleName.c_str(), codeLineStart, Expression::getExpression(), codeLine._text.c_str());
                return false;
            }
            if(numeric._varType == Expression::Number  ||  numeric._varType == Expression::IntVar16  ||  numeric._varType == Expression::StrVar)
            {
                Compiler::handleExpression(codeLineIndex, tokens[i], numeric);
            }

            // An extra doke as a dummy instruction so that gprintf is called at the correct address
            Compiler::emitVcpuAsm("DOKE", "memDstAddr", false);
            Compiler::emitVcpuAsm("DOKE", "memDstAddr", false);
        }

        return addGprintf(codeLine._code.substr(foundPos), formatStr, variables, Compiler::getGprintfVarsAddr(), codeLineIndex);
#endif

#ifdef STAND_ALONE
        return true;
#endif
    }

    bool EXEC(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() < 1  ||  tokens.size() > 2)
        {
            fprintf(stderr, "Keywords::EXEC() : '%s:%d' : syntax error, expected 'EXEC <rom address>, <optional ram address>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // ROM address to load from
        std::string romToken = tokens[0];
        Expression::Numeric romParam;
        if(Compiler::parseExpression(codeLineIndex, romToken, romParam) == Expression::IsInvalid)
        {
            fprintf(stderr, "Keywords::EXEC() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, romToken.c_str(), codeLine._text.c_str());
            return false;
        }
        Compiler::emitVcpuAsm("STW", "giga_sysArg0", false);

        // RAM address to execute at
        if(tokens.size() == 2)
        {
            std::string ramToken = tokens[1];
            Expression::Numeric ramParam;
            if(Compiler::parseExpression(codeLineIndex, ramToken, ramParam) == Expression::IsInvalid)
            {
                fprintf(stderr, "Keywords::EXEC() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, ramToken.c_str(), codeLine._text.c_str());
                return false;
            }
        }
        else
        {
            // Default execute address
            Compiler::emitVcpuAsm("LDWI", Expression::wordToHexString(DEFAULT_EXEC_ADDRESS), false);
        }

        // SYS_Exec_88
        Compiler::emitVcpuAsm("%RomExec", "",  false);

        return true;
    }

    void openUsage(Compiler::CodeLine& codeLine, int codeLineStart)
    {
        fprintf(stderr, "Keywords::OPEN() : '%s:%d' : usage, 'OPEN #<id>, <path>, <file>, <mode>', where mode is one of 'r', 'w', 'a', 'r+', 'w+', 'a+' : %s\n", codeLine._moduleName.c_str(),
                                                                                codeLineStart, codeLine._text.c_str());
    }
    bool OPEN(Compiler::CodeLine& codeLine, int codeLineIndex, int codeLineStart, int tokenIndex, size_t foundPos, KeywordFuncResult& result)
    {
        UNREFERENCED_PARAM(result);
        UNREFERENCED_PARAM(tokenIndex);

        std::vector<std::string> tokens = Expression::tokenise(codeLine._code.substr(foundPos), ',', false);
        if(tokens.size() != 4)
        {
            openUsage(codeLine, codeLineStart);
            fprintf(stderr, "Keywords::OPEN() : '%s:%d' : syntax error, wrong number of parameters : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // file id
        std::string hashToken = tokens[0];
        Expression::stripWhitespace(hashToken);
        if(hashToken[0] != '#')
        {
            openUsage(codeLine, codeLineStart);
            fprintf(stderr, "Keywords::OPEN() : '%s:%d' : syntax error, missing '#' in '#<id>' : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }
        Expression::Numeric idNumeric;
        std::string idOperand;
        std::string idToken = hashToken.substr(1);
        if(Compiler::parseStaticExpression(codeLineIndex, idToken, idOperand, idNumeric) == Compiler::OperandInvalid)
        {
            openUsage(codeLine, codeLineStart);
            fprintf(stderr, "Keywords::OPEN() : '%s:%d' : syntax error in %s : %s\n", codeLine._moduleName.c_str(), codeLineStart, idToken.c_str(), codeLine._text.c_str());
            return false;
        }
        int openId = int(std::lround(idNumeric._value));
        if(Compiler::getDefDataOpens().find(openId) != Compiler::getDefDataOpens().end())
        {
            openUsage(codeLine, codeLineStart);
            fprintf(stderr, "Keywords::OPEN() : '%s:%d' : #%d is not unique : %s\n", codeLine._moduleName.c_str(), codeLineStart, openId, codeLine._text.c_str());
            return false;
        }

        // File path
        std::string pathToken = tokens[1];
        Expression::stripWhitespace(pathToken);
        if(pathToken == "") pathToken = "/";

        // File name
        std::string fileToken = tokens[2];
        Expression::stripWhitespace(fileToken);
        if(fileToken == "")
        {
            openUsage(codeLine, codeLineStart);
            fprintf(stderr, "Keywords::OPEN() : '%s:%d' : syntax error, <file> is empty : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        // Open mode
        Compiler::DefDataOpen::OpenMode openMode;
        std::string modeToken = tokens[3];
        Expression::stripWhitespace(modeToken);
        Expression::strToUpper(modeToken);
        if(modeToken == "R")       openMode = Compiler::DefDataOpen::OpenRead;
        else if(modeToken == "W")  openMode = Compiler::DefDataOpen::OpenWrite;
        else if(modeToken == "A")  openMode = Compiler::DefDataOpen::OpenAppend;
        else if(modeToken == "R+") openMode = Compiler::DefDataOpen::OpenUpdateRW;
        else if(modeToken == "W+") openMode = Compiler::DefDataOpen::OpenCreateRW;
        else if(modeToken == "A+") openMode = Compiler::DefDataOpen::OpenAppendR;
        else
        {
            openUsage(codeLine, codeLineStart);
            fprintf(stderr, "Keywords::OPEN() : '%s:%d' : syntax error, <mode> is expecting one of 'r', 'w', 'a', 'r+', 'w+', 'a+', : %s\n", codeLine._moduleName.c_str(), codeLineStart, codeLine._text.c_str());
            return false;
        }

        Compiler::getDefDataOpens()[openId] = {openId, pathToken, fileToken, openMode};

        return true;
    }
}