--[[============================================================
--=
--=  LuaPreprocess v1.21-dev - preprocessing library
--=  by Marcus 'ReFreezed' Thunström
--=
--=  License: MIT (see the bottom of this file)
--=  Website: http://refreezed.com/luapreprocess/
--=  Documentation: http://refreezed.com/luapreprocess/docs/
--=
--=  Tested with Lua 5.1, 5.2, 5.3, 5.4 and LuaJIT.
--=
--==============================================================

	API:

	Global functions in metaprograms:
	- copyTable
	- escapePattern
	- getIndentation
	- isProcessing
	- pack
	- pairsSorted
	- printf
	- readFile, writeFile, fileExists
	- run
	- sortNatural, compareNatural
	- tokenize, newToken, concatTokens, removeUselessTokens, eachToken, isToken, getNextUsefulToken
	- toLua, serialize, evaluate
	Only during processing:
	- getCurrentPathIn, getCurrentPathOut
	- getOutputSoFar, getOutputSoFarOnLine, getOutputSizeSoFar, getCurrentLineNumberInOutput, getCurrentIndentationInOutput
	- loadResource, callMacro
	- outputValue, outputLua, outputLuaTemplate
	- startInterceptingOutput, stopInterceptingOutput
	Macros:
	- ASSERT
	- LOG
	Search this file for 'EnvironmentTable' and 'PredefinedMacros' for more info.

	Exported stuff from the library:
	- (all the functions above)
	- VERSION
	- metaEnvironment
	- processFile, processString
	Search this file for 'ExportTable' for more info.

----------------------------------------------------------------

	How to metaprogram:

	The exclamation mark (!) is used to indicate what code is part of
	the metaprogram. There are 4 main ways to write metaprogram code:

	!...     The line will simply run during preprocessing. The line can span multiple actual lines if it contains brackets.
	!!...    The line will appear in both the metaprogram and the final program. The line must be an assignment.
	!(...)   The result of the parenthesis will be outputted as a literal if it's an expression, otherwise it'll just run.
	!!(...)  The result of the expression in the parenthesis will be outputted as Lua code. The result must be a string.

	Short examples:

	!if not isDeveloper then
		sendTelemetry()
	!end

	!!local tau = 2*math.pi -- The expression will be evaluated in the metaprogram and the result will appear in the final program as a literal.

	local bigNumber = !(5^10)

	local font = !!(isDeveloper and "loadDevFont()" or "loadUserFont()")

	-- See the full documentation for additional features (like macros):
	-- http://refreezed.com/luapreprocess/docs/extra-functionality/

----------------------------------------------------------------

	-- Example program:

	-- Normal Lua.
	local n = 0
	doTheThing()

	-- Preprocessor lines.
	local n = 0
	!if math.random() < 0.5 then
		n = n+10 -- Normal Lua.
		-- Note: In the final program, this will be in the
		-- same scope as 'local n = 0' here above.
	!end

	!for i = 1, 3 do
		print("3 lines with print().")
	!end

	-- Extended preprocessor line. (Lines are consumed until brackets
	-- are balanced when the end of the line has been reached.)
	!newClass{ -- Starts here.
		name  = "Entity",
		props = {x=0, y=0},
	} -- Ends here.

	-- Preprocessor block.
	!(
	local dogWord = "Woof "
	function getDogText()
		return dogWord:rep(3)
	end
	)

	-- Preprocessor inline block. (Expression that returns a value.)
	local text = !("The dog said: "..getDogText())

	-- Preprocessor inline block variant. (Expression that returns a Lua code string.)
	_G.!!("myRandomGlobal"..math.random(5)) = 99

	-- Dual code (both preprocessor line and final output).
	!!local partial = "Hello"
	local   whole   = partial .. !(partial..", world!")
	print(whole) -- HelloHello, world!

	-- Beware in preprocessor blocks that only call a single function!
	!( func()  ) -- This will bee seen as an inline block and output whatever value func() returns as a literal.
	!( func(); ) -- If that's not wanted then a trailing `;` will prevent that. This line won't output anything by itself.
	-- When the full metaprogram is generated, `!(func())` translates into `outputValue(func())`
	-- while `!(func();)` simply translates into `func();` (because `outputValue(func();)` would be invalid Lua code).
	-- Though in this specific case a preprocessor line (without the parenthesis) would be nicer:
	!func()

	-- For the full documentation, see:
	-- http://refreezed.com/luapreprocess/docs/

--============================================================]]

local PP_VERSION = "1.21.0-dev"

local MAX_DUPLICATE_FILE_INSERTS = 1000 -- @Incomplete: Make this a parameter for processFile()/processString().
local MAX_CODE_LENGTH_IN_MESSAGES = 60

local KEYWORDS = {
    "and",
    "break",
    "do",
    "else",
    "elseif",
    "end",
    "false",
    "for",
    "function",
    "if",
    "in",
    "local",
    "nil",
    "not",
    "or",
    "repeat",
    "return",
    "then",
    "true",
    "until",
    "while",
    -- Lua 5.2
    "goto", -- @Incomplete: A parameter to disable this for Lua 5.1?
}
for i, v in ipairs(KEYWORDS) do
    KEYWORDS[v], KEYWORDS[i] = true, nil
end

local PREPROCESSOR_KEYWORDS = {
    "file",
    "insert",
    "line",
}
for i, v in ipairs(PREPROCESSOR_KEYWORDS) do
    PREPROCESSOR_KEYWORDS[v], PREPROCESSOR_KEYWORDS[i] = true, nil
end

local PUNCTUATION = {
    "+",
    "-",
    "*",
    "/",
    "%",
    "^",
    "#",
    "==",
    "~=",
    "<=",
    ">=",
    "<",
    ">",
    "=",
    "(",
    ")",
    "{",
    "}",
    "[",
    "]",
    ";",
    ":",
    ",",
    ".",
    "..",
    "...",
    -- Lua 5.2
    "::",
    -- Lua 5.3
    "//",
    "&",
    "|",
    "~",
    ">>",
    "<<",
}
for i, v in ipairs(PUNCTUATION) do
    PUNCTUATION[v], PUNCTUATION[i] = true, nil
end

local ESCAPE_SEQUENCES_EXCEPT_QUOTES = {
    ["\a"] = [[\a]],
    ["\b"] = [[\b]],
    ["\f"] = [[\f]],
    ["\n"] = [[\n]],
    ["\r"] = [[\r]],
    ["\t"] = [[\t]],
    ["\v"] = [[\v]],
    ["\\"] = [[\\]],
}
local ESCAPE_SEQUENCES = {
    ['"'] = [[\"]],
    ["'"] = [[\']],
}
for k, v in pairs(ESCAPE_SEQUENCES_EXCEPT_QUOTES) do
    ESCAPE_SEQUENCES[k] = v
end

local USELESS_TOKENS = { whitespace = true, comment = true }

local LOG_LEVELS = {
    ["off"] = 0,
    ["error"] = 1,
    ["warning"] = 2,
    ["info"] = 3,
    ["debug"] = 4,
    ["trace"] = 5,
}

local metaEnv = nil
local dummyEnv = {}

-- Controlled by processFileOrString():
local current_parsingAndMeta_isProcessing = false
local current_parsingAndMeta_isDebug = false

-- Controlled by _processFileOrString():
local current_anytime_isRunningMeta = false
local current_anytime_pathIn = ""
local current_anytime_pathOut = ""
local current_anytime_fastStrings = false
local current_parsing_insertCount = 0
local current_parsingAndMeta_onInsert = nil
local current_parsingAndMeta_resourceCache = nil
local current_parsingAndMeta_addLineNumbers = false
local current_parsingAndMeta_macroPrefix = ""
local current_parsingAndMeta_macroSuffix = ""
local current_parsingAndMeta_strictMacroArguments = true
local current_meta_pathForErrorMessages = ""
local current_meta_output = nil -- Top item in current_meta_outputStack.
local current_meta_outputStack = nil
local current_meta_canOutputNil = true
local current_meta_releaseMode = false
local current_meta_maxLogLevel = "trace"
local current_meta_locationTokens = nil

--==============================================================
--= Local Functions ============================================
--==============================================================

local assertarg
local countString, countSubString
local getLineNumber
local loadLuaString
local maybeOutputLineNumber
local sortNatural
local tableInsert, tableRemove, tableInsertFormat
local utf8GetCodepointAndLength

local F = string.format

local function tryToFormatError(err0)
    local err, path, ln = nil

    if type(err0) == "string" then
        do
            path, ln, err = err0:match("^(%a:[%w_/\\.]+):(%d+): (.*)")
            if not err then
                path, ln, err = err0:match("^([%w_/\\.]+):(%d+): (.*)")
                if not err then
                    path, ln, err = err0:match("^(%S-):(%d+): (.*)")
                end
            end
        end
    end

    if err then
        return F("Error @ %s:%s: %s", path, ln, err)
    else
        return "Error: " .. tostring(err0)
    end
end

local function printf(s, ...)
    print(F(s, ...))
end

-- printTokens( tokens [, filterUselessTokens ] )
local function printTokens(tokens, filter)
    for i, tok in ipairs(tokens) do
        if not (filter and USELESS_TOKENS[tok.type]) then
            printf("%d  %-12s '%s'", i, tok.type, (F("%q", tostring(tok.value)):sub(2, -2):gsub("\\\n", "\\n")))
        end
    end
end

local function printError(s)
    io.stderr:write(s, "\n")
end
local function printfError(s, ...)
    printError(F(s, ...))
end

-- message = formatTraceback( [ level=1 ] )
local function formatTraceback(level)
    local buffer = {}
    tableInsert(buffer, "stack traceback:\n")

    level = 1 + (level or 1)
    local stack = {}

    while level < 1 / 0 do
        local info = debug.getinfo(level, "nSl")
        if not info then
            break
        end

        local isFile = info.source:find("^@") ~= nil
        local sourceName = (isFile and info.source:sub(2) or info.short_src)

        local subBuffer = { "\t" }
        tableInsertFormat(subBuffer, "%s:", sourceName)

        if info.currentline > 0 then
            tableInsertFormat(subBuffer, "%d:", info.currentline)
        end

        if (info.name or "") ~= "" then
            tableInsertFormat(subBuffer, " in '%s'", info.name)
        elseif info.what == "main" then
            tableInsert(subBuffer, " in main chunk")
        elseif info.what == "C" or info.what == "tail" then
            tableInsert(subBuffer, " ?")
        else
            tableInsertFormat(subBuffer, " in <%s:%d>", sourceName:gsub("^.*[/\\]", ""), info.linedefined)
        end

        tableInsert(stack, table.concat(subBuffer))
        level = level + 1
    end

    while stack[#stack] == "\t[C]: ?" do
        stack[#stack] = nil
    end

    for _, s in ipairs(stack) do
        tableInsert(buffer, s)
        tableInsert(buffer, "\n")
    end

    return table.concat(buffer)
end

-- printErrorTraceback( message [, level=1 ] )
local function printErrorTraceback(message, level)
    printError(tryToFormatError(message))
    printError(formatTraceback(1 + (level or 1)))
end

-- debugExit( )
-- debugExit( messageValue )
-- debugExit( messageFormat, ... )
local function debugExit(...)
    if select("#", ...) > 1 then
        printfError(...)
    elseif select("#", ...) == 1 then
        printError(...)
    end
    os.exit(2)
end

-- errorf( [ level=1, ] string, ... )
local function errorf(sOrLevel, ...)
    if type(sOrLevel) == "number" then
        error(F(...), (sOrLevel == 0 and 0 or 1 + sOrLevel))
    else
        error(F(sOrLevel, ...), 2)
    end
end

-- local function errorLine(err) -- Unused.
-- 	if type(err) ~= "string" then  error(err)  end
-- 	error("\0"..err, 0) -- The 0 tells our own error handler not to print the traceback.
-- end
local function errorfLine(s, ...)
    errorf(0, (current_parsingAndMeta_isProcessing and "\0" or "") .. s, ...) -- The \0 tells our own error handler not to print the traceback.
end

-- errorOnLine( path, lineNumber, agent=nil, s, ... )
local function errorOnLine(path, ln, agent, s, ...)
    s = F(s, ...)
    if agent then
        errorfLine("%s:%d: [%s] %s", path, ln, agent, s)
    else
        errorfLine("%s:%d: %s", path, ln, s)
    end
end

local errorInFile, runtimeErrorInFile
do
    local function findStartOfLine(s, pos, canBeEmpty)
        while pos > 1 do
            if
                s:byte(pos - 1) == 10--[[\n]]
                and (
                    canBeEmpty or s:byte(pos) ~= 10--[[\n]]
                )
            then
                break
            end
            pos = pos - 1
        end
        return math.max(pos, 1)
    end
    local function findEndOfLine(s, pos)
        while pos < #s do
            if
                s:byte(pos + 1) == 10--[[\n]]
            then
                break
            end
            pos = pos + 1
        end
        return math.min(pos, #s)
    end

    local function _errorInFile(level, contents, path, pos, agent, s, ...)
        s = F(s, ...)

        pos = math.min(math.max(pos, 1), #contents + 1)
        local ln = getLineNumber(contents, pos)

        local lineStart = findStartOfLine(contents, pos, true)
        local lineEnd = findEndOfLine(contents, pos - 1)
        local linePre1Start = findStartOfLine(contents, lineStart - 1, false)
        local linePre1End = findEndOfLine(contents, linePre1Start - 1)
        local linePre2Start = findStartOfLine(contents, linePre1Start - 1, false)
        local linePre2End = findEndOfLine(contents, linePre2Start - 1)
        -- printfError("pos %d | lines %d..%d, %d..%d, %d..%d", pos, linePre2Start,linePre2End+1, linePre1Start,linePre1End+1, lineStart,lineEnd+1) -- DEBUG

        errorOnLine(
            path,
            ln,
            agent,
            "%s\n>\n%s%s%s>-%s^%s",
            s,
            (linePre2Start < linePre1Start and linePre2Start <= linePre2End)
                    and F("> %s\n", (contents:sub(linePre2Start, linePre2End):gsub("\t", "    ")))
                or "",
            (linePre1Start < lineStart and linePre1Start <= linePre1End)
                    and F("> %s\n", (contents:sub(linePre1Start, linePre1End):gsub("\t", "    ")))
                or "",
            (lineStart <= lineEnd) and F("> %s\n", (contents:sub(lineStart, lineEnd):gsub("\t", "    "))) or ">\n",
            ("-"):rep(pos - lineStart + 3 * countSubString(contents, lineStart, lineEnd, "\t", true)),
            (level and "\n" .. formatTraceback(1 + level) or "")
        )
    end

    -- errorInFile( contents, path, pos, agent, s, ... )
    --[[local]]
    function errorInFile(...)
        _errorInFile(nil, ...)
    end

    -- runtimeErrorInFile( level, contents, path, pos, agent, s, ... )
    --[[local]]
    function runtimeErrorInFile(level, ...)
        _errorInFile(1 + level, ...)
    end
end

-- errorAtToken( token, position=token.position, agent, s, ... )
local function errorAtToken(tok, pos, agent, s, ...)
    -- printErrorTraceback("errorAtToken", 2) -- DEBUG
    errorInFile(current_parsingAndMeta_resourceCache[tok.file], tok.file, (pos or tok.position), agent, s, ...)
end

-- errorAfterToken( token, agent, s, ... )
local function errorAfterToken(tok, agent, s, ...)
    -- printErrorTraceback("errorAfterToken", 2) -- DEBUG
    errorInFile(
        current_parsingAndMeta_resourceCache[tok.file],
        tok.file,
        tok.position + #tok.representation,
        agent,
        s,
        ...
    )
end

-- runtimeErrorAtToken( level, token, position=token.position, agent, s, ... )
local function runtimeErrorAtToken(level, tok, pos, agent, s, ...)
    -- printErrorTraceback("runtimeErrorAtToken", 2) -- DEBUG
    runtimeErrorInFile(
        1 + level,
        current_parsingAndMeta_resourceCache[tok.file],
        tok.file,
        (pos or tok.position),
        agent,
        s,
        ...
    )
end

-- internalError( [ message|value ] )
local function internalError(message)
    message = message and " (" .. tostring(message) .. ")" or ""
    error("Internal error." .. message, 2)
end

local function cleanError(err)
    if type(err) == "string" then
        err = err:gsub("%z", "")
    end
    return err
end

local function formatCodeForShortMessage(lua)
    lua = lua:gsub("^%s+", ""):gsub("%s+$", ""):gsub("%s+", " ")

    if #lua > MAX_CODE_LENGTH_IN_MESSAGES then
        lua = lua:sub(1, MAX_CODE_LENGTH_IN_MESSAGES / 2) .. "..." .. lua:sub(-MAX_CODE_LENGTH_IN_MESSAGES / 2)
    end

    return lua
end

local ERROR_UNFINISHED_STRINGLIKE = 1

local function parseStringlikeToken(s, ptr)
    local reprStart = ptr
    local reprEnd

    local valueStart
    local valueEnd

    local longEqualSigns = s:match("^%[(=*)%[", ptr)
    local isLong = longEqualSigns ~= nil

    -- Single line.
    if not isLong then
        valueStart = ptr

        local i = s:find("\n", ptr, true)
        if not i then
            reprEnd = #s
            valueEnd = #s
            ptr = reprEnd + 1
        else
            reprEnd = i
            valueEnd = i - 1
            ptr = reprEnd + 1
        end

    -- Multiline.
    else
        ptr = ptr + 1 + #longEqualSigns + 1
        valueStart = ptr

        local i1, i2 = s:find("]" .. longEqualSigns .. "]", ptr, true)
        if not i1 then
            return nil, ERROR_UNFINISHED_STRINGLIKE
        end

        reprEnd = i2
        valueEnd = i1 - 1
        ptr = reprEnd + 1
    end

    local repr = s:sub(reprStart, reprEnd)
    local v = s:sub(valueStart, valueEnd)
    local tok = { type = "stringlike", representation = repr, value = v, long = isLong }

    return tok, ptr
end

local NUM_HEX_FRAC_EXP = ("^( 0[Xx] (%x*) %.(%x+) [Pp]([-+]?%x+) )"):gsub(" +", "")
local NUM_HEX_FRAC = ("^( 0[Xx] (%x*) %.(%x+)                )"):gsub(" +", "")
local NUM_HEX_EXP = ("^( 0[Xx] (%x+) %.?     [Pp]([-+]?%x+) )"):gsub(" +", "")
local NUM_HEX = ("^( 0[Xx]  %x+  %.?                    )"):gsub(" +", "")
local NUM_DEC_FRAC_EXP = ("^(        %d*  %.%d+   [Ee][-+]?%d+   )"):gsub(" +", "")
local NUM_DEC_FRAC = ("^(        %d*  %.%d+                  )"):gsub(" +", "")
local NUM_DEC_EXP = ("^(        %d+  %.?     [Ee][-+]?%d+   )"):gsub(" +", "")
local NUM_DEC = ("^(        %d+  %.?                    )"):gsub(" +", "")

-- tokens = _tokenize( luaString, path, allowPreprocessorTokens, allowBacktickStrings, allowJitSyntax )
local function _tokenize(s, path, allowPpTokens, allowBacktickStrings, allowJitSyntax)
    s = s:gsub("\r", "") -- Normalize line breaks. (Assume the input is either "\n" or "\r\n".)

    local tokens = {}
    local ptr = 1
    local ln = 1

    while ptr <= #s do
        local tok
        local tokenPos = ptr

        -- Whitespace.
        if s:find("^%s", ptr) then
            local i1, i2, whitespace = s:find("^(%s+)", ptr)

            ptr = i2 + 1
            tok = { type = "whitespace", representation = whitespace, value = whitespace }

        -- Identifier/keyword.
        elseif s:find("^[%a_]", ptr) then
            local i1, i2, word = s:find("^([%a_][%w_]*)", ptr)
            ptr = i2 + 1

            if KEYWORDS[word] then
                tok = { type = "keyword", representation = word, value = word }
            else
                tok = { type = "identifier", representation = word, value = word }
            end

        -- Number (binary).
        elseif s:find("^0b", ptr) then
            if not allowJitSyntax then
                errorInFile(s, path, ptr, "Tokenizer", "Encountered binary numeral. (Feature not enabled.)")
            end

            local i1, i2, numStr = s:find("^(..[01]+)", ptr)

            -- @Copypaste from below.
            if not numStr then
                errorInFile(s, path, ptr, "Tokenizer", "Malformed number.")
            end

            local numStrFallback = numStr

            do
                if s:find("^[Ii]", i2 + 1) then -- Imaginary part of complex number.
                    numStr = s:sub(i1, i2 + 1)
                    i2 = i2 + 1
                elseif s:find("^[Uu][Ll][Ll]", i2 + 1) then -- Unsigned 64-bit integer.
                    numStr = s:sub(i1, i2 + 3)
                    i2 = i2 + 3
                elseif s:find("^[Ll][Ll]", i2 + 1) then -- Signed 64-bit integer.
                    numStr = s:sub(i1, i2 + 2)
                    i2 = i2 + 2
                end
            end

            local n = tonumber(numStr) or tonumber(numStrFallback) or tonumber(numStrFallback:sub(3), 2)

            if not n then
                errorInFile(s, path, ptr, "Tokenizer", "Invalid number.")
            end

            if s:find("^[%w_]", i2 + 1) then
                -- This is actually not an error in Lua 5.2 and 5.3. Maybe we should issue a warning instead of an error here?
                errorInFile(s, path, i2 + 1, "Tokenizer", "Malformed number.")
            end

            ptr = i2 + 1
            tok = { type = "number", representation = numStrFallback, value = n }

        -- Number.
        elseif s:find("^%.?%d", ptr) then
            local pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX_FRAC_EXP, false, true, s:find(NUM_HEX_FRAC_EXP, ptr)
            if not i1 then
                pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX_FRAC, false, true, s:find(NUM_HEX_FRAC, ptr)
                if not i1 then
                    pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX_EXP, false, true, s:find(NUM_HEX_EXP, ptr)
                    if not i1 then
                        pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX, true, false, s:find(NUM_HEX, ptr)
                        if not i1 then
                            pat, maybeInt, lua52Hex, i1, i2, numStr =
                                NUM_DEC_FRAC_EXP, false, false, s:find(NUM_DEC_FRAC_EXP, ptr)
                            if not i1 then
                                pat, maybeInt, lua52Hex, i1, i2, numStr =
                                    NUM_DEC_FRAC, false, false, s:find(NUM_DEC_FRAC, ptr)
                                if not i1 then
                                    pat, maybeInt, lua52Hex, i1, i2, numStr =
                                        NUM_DEC_EXP, false, false, s:find(NUM_DEC_EXP, ptr)
                                    if not i1 then
                                        pat, maybeInt, lua52Hex, i1, i2, numStr =
                                            NUM_DEC, true, false, s:find(NUM_DEC, ptr)
                                    end
                                end
                            end
                        end
                    end
                end
            end

            if not numStr then
                errorInFile(s, path, ptr, "Tokenizer", "Malformed number.")
            end

            local numStrFallback = numStr

            if allowJitSyntax then
                if s:find("^[Ii]", i2 + 1) then -- Imaginary part of complex number.
                    numStr = s:sub(i1, i2 + 1)
                    i2 = i2 + 1
                elseif not maybeInt or numStr:find(".", 1, true) then
                    -- void
                elseif s:find("^[Uu][Ll][Ll]", i2 + 1) then -- Unsigned 64-bit integer.
                    numStr = s:sub(i1, i2 + 3)
                    i2 = i2 + 3
                elseif s:find("^[Ll][Ll]", i2 + 1) then -- Signed 64-bit integer.
                    numStr = s:sub(i1, i2 + 2)
                    i2 = i2 + 2
                end
            end

            local n = tonumber(numStr) or tonumber(numStrFallback)

            -- Support hexadecimal floats in Lua 5.1.
            if not n and lua52Hex then
                -- Note: We know we're not running LuaJIT here as it supports hexadecimal floats, thus we use numStrFallback instead of numStr.
                local _, intStr, fracStr, expStr
                if pat == NUM_HEX_FRAC_EXP then
                    _, intStr, fracStr, expStr = numStrFallback:match(NUM_HEX_FRAC_EXP)
                elseif pat == NUM_HEX_FRAC then
                    _, intStr, fracStr = numStrFallback:match(NUM_HEX_FRAC)
                    expStr = "0"
                elseif pat == NUM_HEX_EXP then
                    _, intStr, expStr = numStrFallback:match(NUM_HEX_EXP)
                    fracStr = ""
                else
                    internalError()
                end

                n = tonumber(intStr, 16) or 0 -- intStr may be "".

                local fracValue = 1
                for i = 1, #fracStr do
                    fracValue = fracValue / 16
                    n = n + tonumber(fracStr:sub(i, i), 16) * fracValue
                end

                n = n * 2 ^ expStr:gsub("^+", "")
            end

            if not n then
                errorInFile(s, path, ptr, "Tokenizer", "Invalid number.")
            end

            if s:find("^[%w_]", i2 + 1) then
                -- This is actually not an error in Lua 5.2 and 5.3. Maybe we should issue a warning instead of an error here?
                errorInFile(s, path, i2 + 1, "Tokenizer", "Malformed number.")
            end

            ptr = i2 + 1
            tok = { type = "number", representation = numStrFallback, value = n }

        -- Comment.
        elseif s:find("^%-%-", ptr) then
            local reprStart = ptr
            ptr = ptr + 2

            tok, ptr = parseStringlikeToken(s, ptr)
            if not tok then
                local errCode = ptr
                if errCode == ERROR_UNFINISHED_STRINGLIKE then
                    errorInFile(s, path, reprStart, "Tokenizer", "Unfinished long comment.")
                else
                    errorInFile(s, path, reprStart, "Tokenizer", "Invalid comment.")
                end
            end

            if tok.long then
                -- Check for nesting of [[...]], which is deprecated in Lua.
                local chunk, err = loadLuaString("--" .. tok.representation, "@", nil)

                if not chunk then
                    local lnInString, luaErr = err:match("^:(%d+): (.*)")
                    if luaErr then
                        errorOnLine(
                            path,
                            getLineNumber(s, reprStart) + tonumber(lnInString) - 1,
                            "Tokenizer",
                            "Malformed long comment. (%s)",
                            luaErr
                        )
                    else
                        errorInFile(s, path, reprStart, "Tokenizer", "Malformed long comment.")
                    end
                end
            end

            tok.type = "comment"
            tok.representation = s:sub(reprStart, ptr - 1)

        -- String (short).
        elseif s:find([=[^["']]=], ptr) then
            local reprStart = ptr
            local reprEnd

            local quoteChar = s:sub(ptr, ptr)
            ptr = ptr + 1

            local valueStart = ptr
            local valueEnd

            while true do
                local c = s:sub(ptr, ptr)

                if c == "" then
                    errorInFile(s, path, reprStart, "Tokenizer", "Unfinished string.")
                elseif c == quoteChar then
                    reprEnd = ptr
                    valueEnd = ptr - 1
                    ptr = reprEnd + 1
                    break
                elseif c == "\\" then
                    -- Note: We don't have to look for multiple characters after
                    -- the escape, like \nnn - this algorithm works anyway.
                    if ptr + 1 > #s then
                        errorInFile(s, path, reprStart, "Tokenizer", "Unfinished string after escape.")
                    end
                    ptr = ptr + 2
                elseif c == "\n" then
                    -- Can't have unescaped newlines. Lua, this is a silly rule! @Ugh
                    errorInFile(s, path, ptr, "Tokenizer", "Newlines must be escaped in strings.")
                else
                    ptr = ptr + 1
                end
            end

            local repr = s:sub(reprStart, reprEnd)

            local valueChunk = loadLuaString("return" .. repr, nil, nil)
            if not valueChunk then
                errorInFile(s, path, reprStart, "Tokenizer", "Malformed string.")
            end

            local v = valueChunk()
            assert(type(v) == "string")

            tok = { type = "string", representation = repr, value = valueChunk(), long = false }

        -- Long string.
        elseif s:find("^%[=*%[", ptr) then
            local reprStart = ptr

            tok, ptr = parseStringlikeToken(s, ptr)
            if not tok then
                local errCode = ptr
                if errCode == ERROR_UNFINISHED_STRINGLIKE then
                    errorInFile(s, path, reprStart, "Tokenizer", "Unfinished long string.")
                else
                    errorInFile(s, path, reprStart, "Tokenizer", "Invalid long string.")
                end
            end

            -- Check for nesting of [[...]], which is deprecated in Lua.
            local valueChunk, err = loadLuaString("return" .. tok.representation, "@", nil)

            if not valueChunk then
                local lnInString, luaErr = err:match("^:(%d+): (.*)")
                if luaErr then
                    errorOnLine(
                        path,
                        getLineNumber(s, reprStart) + tonumber(lnInString) - 1,
                        "Tokenizer",
                        "Malformed long string. (%s)",
                        luaErr
                    )
                else
                    errorInFile(s, path, reprStart, "Tokenizer", "Malformed long string.")
                end
            end

            local v = valueChunk()
            assert(type(v) == "string")

            tok.type = "string"
            tok.value = v

        -- Backtick string.
        elseif s:find("^`", ptr) then
            if not allowBacktickStrings then
                errorInFile(s, path, ptr, "Tokenizer", "Encountered backtick string. (Feature not enabled.)")
            end

            local i1, i2, repr, v = s:find("^(`([^`]*)`)", ptr)
            if not i2 then
                errorInFile(s, path, ptr, "Tokenizer", "Unfinished backtick string.")
            end

            ptr = i2 + 1
            tok = { type = "string", representation = repr, value = v, long = false }

        -- Punctuation etc.
        elseif s:find("^%.%.%.", ptr) then -- 3
            local repr = s:sub(ptr, ptr + 2)
            tok = { type = "punctuation", representation = repr, value = repr }
            ptr = ptr + #repr
        elseif
            s:find("^%.%.", ptr)
            or s:find("^[=~<>]=", ptr)
            or s:find("^::", ptr)
            or s:find("^//", ptr)
            or s:find("^<<", ptr)
            or s:find("^>>", ptr)
        then -- 2
            local repr = s:sub(ptr, ptr + 1)
            tok = { type = "punctuation", representation = repr, value = repr }
            ptr = ptr + #repr
        elseif s:find("^[+%-*/%%^#<>=(){}[%];:,.&|~]", ptr) then -- 1
            local repr = s:sub(ptr, ptr)
            tok = { type = "punctuation", representation = repr, value = repr }
            ptr = ptr + #repr

        -- Preprocessor entry.
        elseif s:find("^!", ptr) then
            if not allowPpTokens then
                errorInFile(s, path, ptr, "Tokenizer", "Encountered preprocessor entry. (Feature not enabled.)")
            end

            local double = s:find("^!", ptr + 1) ~= nil
            local repr = s:sub(ptr, ptr + (double and 1 or 0))

            tok = { type = "pp_entry", representation = repr, value = repr, double = double }
            ptr = ptr + #repr

        -- Preprocessor keyword.
        elseif s:find("^@", ptr) then
            if not allowPpTokens then
                errorInFile(s, path, ptr, "Tokenizer", "Encountered preprocessor keyword. (Feature not enabled.)")
            end

            if s:find("^@@", ptr) then
                ptr = ptr + 2
                tok = { type = "pp_keyword", representation = "@@", value = "insert" }
            else
                local i1, i2, repr, word = s:find("^(@([%a_][%w_]*))", ptr)
                if not i1 then
                    errorInFile(s, path, ptr + 1, "Tokenizer", "Expected an identifier.")
                elseif not PREPROCESSOR_KEYWORDS[word] then
                    errorInFile(s, path, ptr + 1, "Tokenizer", "Invalid preprocessor keyword '%s'.", word)
                end
                ptr = i2 + 1
                tok = { type = "pp_keyword", representation = repr, value = word }
            end

        -- Preprocessor symbol.
        elseif s:find("^%$", ptr) then
            if not allowPpTokens then
                errorInFile(s, path, ptr, "Tokenizer", "Encountered preprocessor symbol. (Feature not enabled.)")
            end

            local i1, i2, repr, word = s:find("^(%$([%a_][%w_]*))", ptr)
            if not i1 then
                errorInFile(s, path, ptr + 1, "Tokenizer", "Expected an identifier.")
            elseif KEYWORDS[word] then
                errorInFile(
                    s,
                    path,
                    ptr + 1,
                    "Tokenizer",
                    "Invalid preprocessor symbol '%s'. (Must not be a Lua keyword.)",
                    word
                )
            end
            ptr = i2 + 1
            tok = { type = "pp_symbol", representation = repr, value = word }
        else
            errorInFile(s, path, ptr, "Tokenizer", "Unknown character.")
        end

        tok.line = ln
        tok.position = tokenPos
        tok.file = path

        ln = ln + countString(tok.representation, "\n", true)
        tok.lineEnd = ln

        tableInsert(tokens, tok)
        -- print(#tokens, tok.type, tok.representation) -- DEBUG
    end

    return tokens
end

-- luaString = _concatTokens( tokens, lastLn=nil, addLineNumbers, fromIndex=1, toIndex=#tokens )
local function _concatTokens(tokens, lastLn, addLineNumbers, i1, i2)
    local parts = {}

    if addLineNumbers then
        for i = (i1 or 1), (i2 or #tokens) do
            local tok = tokens[i]
            lastLn = maybeOutputLineNumber(parts, tok, lastLn)
            tableInsert(parts, tok.representation)
        end
    else
        for i = (i1 or 1), (i2 or #tokens) do
            tableInsert(parts, tokens[i].representation)
        end
    end

    return table.concat(parts)
end

local function insertTokenRepresentations(parts, tokens, i1, i2)
    for i = i1, i2 do
        tableInsert(parts, tokens[i].representation)
    end
end

local function readFile(path, isTextFile)
    assertarg(1, path, "string")
    assertarg(2, isTextFile, "boolean", "nil")

    local file, err = io.open(path, "r" .. (isTextFile and "" or "b"))
    if not file then
        return nil, err
    end

    local contents = file:read("*a")
    file:close()
    return contents
end

-- success, error = writeFile( path, [ isTextFile=false, ] contents )
local function writeFile(path, isTextFile, contents)
    assertarg(1, path, "string")

    if type(isTextFile) == "boolean" then
        assertarg(3, contents, "string")
    else
        isTextFile, contents = false, isTextFile
        assertarg(2, contents, "string")
    end

    local file, err = io.open(path, "w" .. (isTextFile and "" or "b"))
    if not file then
        return false, err
    end

    file:write(contents)
    file:close()
    return true
end

local function fileExists(path)
    assertarg(1, path, "string")

    local file = io.open(path, "r")
    if not file then
        return false
    end

    file:close()
    return true
end

-- assertarg( argumentNumber, value, expectedValueType1, ... )
--[[local]]
function assertarg(n, v, ...)
    local vType = type(v)

    for i = 1, select("#", ...) do
        if vType == select(i, ...) then
            return
        end
    end

    local fName = debug.getinfo(2, "n").name
    local expects = table.concat({ ... }, " or ")

    if fName == "" then
        fName = "?"
    end

    errorf(3, "bad argument #%d to '%s' (%s expected, got %s)", n, fName, expects, vType)
end

-- count = countString( haystack, needle [, plain=false ] )
--[[local]]
function countString(s, needle, plain)
    local count = 0
    local i = 0
    local _

    while true do
        _, i = s:find(needle, i + 1, plain)
        if not i then
            return count
        end

        count = count + 1
    end
end

-- count = countSubString( string, startPosition, endPosition, needle [, plain=false ] )
--[[local]]
function countSubString(s, pos, posEnd, needle, plain)
    local count = 0

    while true do
        local _, i2 = s:find(needle, pos, plain)
        if not i2 or i2 > posEnd then
            return count
        end

        count = count + 1
        pos = i2 + 1
    end
end

local getfenv = getfenv
    or function(f) -- Assume Lua is version 5.2+ if getfenv() doesn't exist.
        f = f or 1

        if type(f) == "function" then
        -- void
        elseif type(f) == "number" then
            if f == 0 then
                return _ENV
            end
            if f < 0 then
                error("bad argument #1 to 'getfenv' (level must be non-negative)")
            end

            f = debug.getinfo(1 + f, "f") or error("bad argument #1 to 'getfenv' (invalid level)")
            f = f.func
        else
            error("bad argument #1 to 'getfenv' (number expected, got " .. type(f) .. ")")
        end

        for i = 1, 1 / 0 do
            local name, v = debug.getupvalue(f, i)
            if name == "_ENV" then
                return v
            end
            if not name then
                return _ENV
            end
        end
    end

-- (Table generated by misc/generateStringEscapeSequenceInfo.lua)
local UNICODE_RANGES_NOT_TO_ESCAPE = {
    { from = 32, to = 126 },
    { from = 161, to = 591 },
    { from = 880, to = 887 },
    { from = 890, to = 895 },
    { from = 900, to = 906 },
    { from = 908, to = 908 },
    { from = 910, to = 929 },
    { from = 931, to = 1154 },
    { from = 1162, to = 1279 },
    { from = 7682, to = 7683 },
    { from = 7690, to = 7691 },
    { from = 7710, to = 7711 },
    { from = 7744, to = 7745 },
    { from = 7766, to = 7767 },
    { from = 7776, to = 7777 },
    { from = 7786, to = 7787 },
    { from = 7808, to = 7813 },
    { from = 7835, to = 7835 },
    { from = 7922, to = 7923 },
    { from = 8208, to = 8208 },
    { from = 8210, to = 8231 },
    { from = 8240, to = 8286 },
    { from = 8304, to = 8305 },
    { from = 8308, to = 8334 },
    { from = 8336, to = 8348 },
    { from = 8352, to = 8383 },
    { from = 8448, to = 8587 },
    { from = 8592, to = 9254 },
    { from = 9312, to = 10239 },
    { from = 10496, to = 11007 },
    { from = 64256, to = 64262 },
}

local function shouldCodepointBeEscaped(cp)
    for _, range in ipairs(UNICODE_RANGES_NOT_TO_ESCAPE) do -- @Speed: Don't use a loop?
        if cp >= range.from and cp <= range.to then
            return false
        end
    end
    return true
end

-- local cache = setmetatable({}, {__mode="kv"}) -- :SerializationCache (This doesn't seem to speed things up.)

-- success, error = serialize( buffer, value )
local function serialize(buffer, v)
    --[[ :SerializationCache
	if cache[v] then
		tableInsert(buffer, cache[v])
		return true
	end
	local bufferStart = #buffer + 1
	--]]

    local vType = type(v)

    if vType == "table" then
        local first = true
        tableInsert(buffer, "{")

        local indices = {}
        for i, item in ipairs(v) do
            if not first then
                tableInsert(buffer, ",")
            end
            first = false

            local ok, err = serialize(buffer, item)
            if not ok then
                return false, err
            end

            indices[i] = true
        end

        local keys = {}
        for k, item in pairs(v) do
            if indices[k] then
                -- void
            elseif type(k) == "table" then
                return false, "Table keys cannot be tables."
            else
                tableInsert(keys, k)
            end
        end

        table.sort(keys, function(a, b)
            return tostring(a) < tostring(b)
        end)

        for _, k in ipairs(keys) do
            local item = v[k]

            if not first then
                tableInsert(buffer, ",")
            end
            first = false

            if not KEYWORDS[k] and type(k) == "string" and k:find("^[%a_][%w_]*$") then
                tableInsert(buffer, k)
                tableInsert(buffer, "=")
            else
                tableInsert(buffer, "[")

                local ok, err = serialize(buffer, k)
                if not ok then
                    return false, err
                end

                tableInsert(buffer, "]=")
            end

            local ok, err = serialize(buffer, item)
            if not ok then
                return false, err
            end
        end

        tableInsert(buffer, "}")
    elseif vType == "string" then
        if v == "" then
            tableInsert(buffer, '""')
            return true
        end

        local useApostrophe = v:find('"', 1, true) and not v:find("'", 1, true)
        local quote = useApostrophe and "'" or '"'

        tableInsert(buffer, quote)

        if current_anytime_fastStrings or not v:find("[^\32-\126\t\n]") then
            -- print(">> FAST", #v) -- DEBUG

            local s = v:gsub((useApostrophe and "[\t\n\\']" or '[\t\n\\"]'), function(c)
                return ESCAPE_SEQUENCES[c] or internalError(c:byte())
            end)
            tableInsert(buffer, s)
        else
            -- print(">> SLOW", #v) -- DEBUG
            local pos = 1

            -- @Speed: There are optimizations to be made here!
            while pos <= #v do
                local c = v:sub(pos, pos)
                local cp, len = utf8GetCodepointAndLength(v, pos)

                -- Named escape sequences.
                if ESCAPE_SEQUENCES_EXCEPT_QUOTES[c] then
                    tableInsert(buffer, ESCAPE_SEQUENCES_EXCEPT_QUOTES[c])
                    pos = pos + 1
                elseif c == quote then
                    tableInsert(buffer, [[\]])
                    tableInsert(buffer, quote)
                    pos = pos + 1

                -- UTF-8 character.
                elseif len == 1 and not shouldCodepointBeEscaped(cp) then
                    tableInsert(buffer, v:sub(pos, pos))
                    pos = pos + 1 -- @Speed: We can insert multiple single-byte characters sometimes!
                elseif len and not shouldCodepointBeEscaped(cp) then
                    tableInsert(buffer, v:sub(pos, pos + len - 1))
                    pos = pos + len

                -- Anything else.
                else
                    tableInsert(buffer, F((v:find("^%d", pos + 1) and "\\%03d" or "\\%d"), v:byte(pos)))
                    pos = pos + 1
                end
            end
        end

        tableInsert(buffer, quote)
    elseif v == 1 / 0 then
        tableInsert(buffer, "(1/0)")
    elseif v == -1 / 0 then
        tableInsert(buffer, "(-1/0)")
    elseif v ~= v then
        tableInsert(buffer, "(0/0)") -- NaN.
    elseif v == 0 then
        tableInsert(buffer, "0") -- In case it's actually -0 for some reason, which would be silly to output.
    elseif vType == "number" then
        if v < 0 then
            tableInsert(buffer, " ") -- The space prevents an accidental comment if a "-" is right before.
        end
        tableInsert(buffer, tostring(v)) -- (I'm not sure what precision tostring() uses for numbers. Maybe we should use string.format() instead.)
    elseif vType == "boolean" or v == nil then
        tableInsert(buffer, tostring(v))
    else
        return false, F("Cannot serialize value of type '%s'. (%s)", vType, tostring(v))
    end

    --[[ :SerializationCache
	if v ~= nil then
		cache[v] = table.concat(buffer, "", bufferStart, #buffer)
	end
	--]]

    return true
end

-- luaString = toLua( value )
-- Returns nil and a message on error.
local function toLua(v)
    local buffer = {}

    local ok, err = serialize(buffer, v)
    if not ok then
        return nil, err
    end

    return table.concat(buffer)
end

-- value = evaluate( expression [, environment=getfenv() ] )
-- Returns nil and a message on error.
local function evaluate(expr, env)
    local chunk, err = loadLuaString("return(" .. expr .. "\n)", "@<evaluate>", (env or getfenv(2)))
    if not chunk then
        return nil, F("Invalid expression '%s'. (%s)", expr, (err:gsub("^:%d+: ", "")))
    end

    local ok, valueOrErr = pcall(chunk)
    if not ok then
        return nil, valueOrErr
    end

    return valueOrErr -- May be nil or false!
end

local function escapePattern(s)
    return (s:gsub("[-+*^?$.%%()[%]]", "%%%0"))
end

local function outputLineNumber(parts, ln)
    tableInsert(parts, "--[[@")
    tableInsert(parts, ln)
    tableInsert(parts, "]]")
end

--[[local]]
function maybeOutputLineNumber(parts, tok, lastLn)
    if tok.line == lastLn or USELESS_TOKENS[tok.type] then
        return lastLn
    end

    outputLineNumber(parts, tok.line)
    return tok.line
end
--[=[
--[[local]] function maybeOutputLineNumber(parts, tok, lastLn, fromMetaToOutput)
	if tok.line == lastLn or USELESS_TOKENS[tok.type] then  return lastLn  end

	if fromMetaToOutput then
		tableInsert(parts, '__LUA"--[[@'..tok.line..']]"\n')
	else
		tableInsert(parts, "--[[@"..tok.line.."]]")
	end
	return tok.line
end
]=]

local function isAny(v, ...)
    for i = 1, select("#", ...) do
        if v == select(i, ...) then
            return true
        end
    end
    return false
end

local function errorIfNotRunningMeta(level)
    if not current_anytime_isRunningMeta then
        error("No file is being processed.", 1 + level)
    end
end

local function copyArray(t)
    local copy = {}
    for i, v in ipairs(t) do
        copy[i] = v
    end
    return copy
end

local copyTable
do
    local function deepCopy(t, copy, tableCopies)
        for k, v in pairs(t) do
            if type(v) == "table" then
                local vCopy = tableCopies[v]

                if vCopy then
                    copy[k] = vCopy
                else
                    vCopy = {}
                    tableCopies[v] = vCopy
                    copy[k] = deepCopy(v, vCopy, tableCopies)
                end
            else
                copy[k] = v
            end
        end
        return copy
    end

    -- copy = copyTable( table [, deep=false ] )
    --[[local]]
    function copyTable(t, deep)
        local copy = {}

        if deep then
            return deepCopy(t, copy, { [t] = copy })
        end

        for k, v in pairs(t) do
            copy[k] = v
        end

        return copy
    end
end

-- values = pack( value1, ... )
-- values.n is the amount of values (which can be zero).
local pack = (
    (_VERSION >= "Lua 5.2" or jit) and table.pack or function(...)
        return { n = select("#", ...), ... }
    end
)

local unpack = (_VERSION >= "Lua 5.2") and table.unpack or _G.unpack

--[[local]]
loadLuaString = (
    (_VERSION >= "Lua 5.2" or jit)
        and function(lua, chunkName, env)
            return load(lua, chunkName, "bt", env)
        end
    or function(lua, chunkName, env)
        local chunk, err = loadstring(lua, chunkName)
        if not chunk then
            return nil, err
        end

        if env then
            setfenv(chunk, env)
        end

        return chunk
    end
)

local loadLuaFile = (
    (_VERSION >= "Lua 5.2" or jit) and function(path, env)
        return loadfile(path, "bt", env)
    end
    or function(path, env)
        local chunk, err = loadfile(path)
        if not chunk then
            return nil, err
        end

        if env then
            setfenv(chunk, env)
        end

        return chunk
    end
)

local function isLuaStringValidExpression(lua)
    return loadLuaString("return(" .. lua .. "\n)", "@", nil) ~= nil
end

-- token, index = getNextUsableToken( tokens, startIndex, indexLimit=autoDependingOnDirection, direction )
local function getNextUsableToken(tokens, iStart, iLimit, dir)
    iLimit = (dir < 0 and math.max((iLimit or 1), 1) or math.min((iLimit or 1 / 0), #tokens))

    for i = iStart, iLimit, dir do
        if not USELESS_TOKENS[tokens[i].type] then
            return tokens[i], i
        end
    end

    return nil
end

-- bool = isToken( token, tokenType [, tokenValue=any ] )
local function isToken(tok, tokType, v)
    return tok.type == tokType and (v == nil or tok.value == v)
end

-- bool = isTokenAndNotNil( token, tokenType [, tokenValue=any ] )
local function isTokenAndNotNil(tok, tokType, v)
    return tok ~= nil and tok.type == tokType and (v == nil or tok.value == v)
end

--[[local]]
function getLineNumber(s, pos)
    return 1 + countSubString(s, 1, pos - 1, "\n", true)
end

-- text = getRelativeLocationText( tokenOfInterest, otherToken )
-- text = getRelativeLocationText( tokenOfInterest, otherFilename, otherLineNumber )
local function getRelativeLocationText(tokOfInterest, otherFilename, otherLn)
    if type(otherFilename) == "table" then
        return getRelativeLocationText(tokOfInterest, otherFilename.file, otherFilename.line)
    end

    if not (tokOfInterest.file and tokOfInterest.line) then
        return "at <UnknownLocation>"
    end

    if tokOfInterest.file ~= otherFilename then
        return F("at %s:%d", tokOfInterest.file, tokOfInterest.line)
    end
    if tokOfInterest.line + 1 == otherLn then
        return F("on the previous line")
    end
    if tokOfInterest.line - 1 == otherLn then
        return F("on the next line")
    end
    if tokOfInterest.line ~= otherLn then
        return F("on line %d", tokOfInterest.line)
    end
    return "on the same line"
end

--[[local]]
tableInsert = table.insert
--[[local]]
tableRemove = table.remove

--[[local]]
function tableInsertFormat(t, s, ...)
    tableInsert(t, F(s, ...))
end

-- length|nil = utf8GetCharLength( string [, position=1 ] )
local function utf8GetCharLength(s, pos)
    pos = pos or 1
    local b1, b2, b3, b4 = s:byte(pos, pos + 3)

    if b1 > 0 and b1 <= 127 then
        return 1
    elseif b1 >= 194 and b1 <= 223 then
        if not b2 then
            return nil
        end -- UTF-8 string terminated early.
        if b2 < 128 or b2 > 191 then
            return nil
        end -- Invalid UTF-8 character.
        return 2
    elseif b1 >= 224 and b1 <= 239 then
        if not b3 then
            return nil
        end -- UTF-8 string terminated early.
        if b1 == 224 and (b2 < 160 or b2 > 191) then
            return nil
        end -- Invalid UTF-8 character.
        if b1 == 237 and (b2 < 128 or b2 > 159) then
            return nil
        end -- Invalid UTF-8 character.
        if b2 < 128 or b2 > 191 then
            return nil
        end -- Invalid UTF-8 character.
        if b3 < 128 or b3 > 191 then
            return nil
        end -- Invalid UTF-8 character.
        return 3
    elseif b1 >= 240 and b1 <= 244 then
        if not b4 then
            return nil
        end -- UTF-8 string terminated early.
        if b1 == 240 and (b2 < 144 or b2 > 191) then
            return nil
        end -- Invalid UTF-8 character.
        if b1 == 244 and (b2 < 128 or b2 > 143) then
            return nil
        end -- Invalid UTF-8 character.
        if b2 < 128 or b2 > 191 then
            return nil
        end -- Invalid UTF-8 character.
        if b3 < 128 or b3 > 191 then
            return nil
        end -- Invalid UTF-8 character.
        if b4 < 128 or b4 > 191 then
            return nil
        end -- Invalid UTF-8 character.
        return 4
    end

    return nil -- Invalid UTF-8 character.
end

-- codepoint, length = utf8GetCodepointAndLength( string [, position=1 ] )
-- Returns nil if the text is invalid at the position.
--[[local]]
function utf8GetCodepointAndLength(s, pos)
    pos = pos or 1
    local len = utf8GetCharLength(s, pos)
    if not len then
        return nil
    end

    -- 2^6=64, 2^12=4096, 2^18=262144
    if len == 1 then
        return s:byte(pos), len
    end
    if len == 2 then
        local b1, b2 = s:byte(pos, pos + 1)
        return (b1 - 192) * 64 + (b2 - 128), len
    end
    if len == 3 then
        local b1, b2, b3 = s:byte(pos, pos + 2)
        return (b1 - 224) * 4096 + (b2 - 128) * 64 + (b3 - 128), len
    end
    do
        local b1, b2, b3, b4 = s:byte(pos, pos + 3)
        return (b1 - 240) * 262144 + (b2 - 128) * 4096 + (b3 - 128) * 64 + (b4 - 128), len
    end
end

-- for k, v in pairsSorted( table ) do
local function pairsSorted(t)
    local keys = {}
    for k in pairs(t) do
        tableInsert(keys, k)
    end
    sortNatural(keys)

    local i = 0

    return function()
        i = i + 1
        local k = keys[i]
        if k ~= nil then
            return k, t[k]
        end
    end
end

-- sortNatural( array )
-- aIsLessThanB = compareNatural( a, b )
local compareNatural
do
    local function pad(numStr)
        return F("%03d%s", #numStr, numStr)
    end
    --[[local]]
    function compareNatural(a, b)
        if type(a) == "number" and type(b) == "number" then
            return a < b
        else
            return (tostring(a):gsub("%d+", pad) < tostring(b):gsub("%d+", pad))
        end
    end

    --[[local]]
    function sortNatural(t, k)
        table.sort(t, compareNatural)
    end
end

-- lua = _loadResource( resourceName, isParsing==true , nameToken, stats ) -- At parse time.
-- lua = _loadResource( resourceName, isParsing==false, errorLevel       ) -- At metaprogram runtime.
local function _loadResource(resourceName, isParsing, nameTokOrErrLevel, stats)
    local lua = current_parsingAndMeta_resourceCache[resourceName]

    if not lua then
        if current_parsingAndMeta_onInsert then
            lua = current_parsingAndMeta_onInsert(resourceName)

            if type(lua) == "string" then
                -- void
            elseif isParsing then
                errorAtToken(
                    nameTokOrErrLevel,
                    nameTokOrErrLevel.position + 1,
                    "Parser/MetaProgram",
                    "Expected a string from params.onInsert(). (Got %s)",
                    type(lua)
                )
            else
                errorf(1 + nameTokOrErrLevel, "Expected a string from params.onInsert(). (Got %s)", type(lua))
            end
        else
            local err
            lua, err = readFile(resourceName, true)

            if lua then
                -- void
            elseif isParsing then
                errorAtToken(
                    nameTokOrErrLevel,
                    nameTokOrErrLevel.position + 1,
                    "Parser",
                    "Could not read file '%s'. (%s)",
                    resourceName,
                    tostring(err)
                )
            else
                errorf(1 + nameTokOrErrLevel, "Could not read file '%s'. (%s)", resourceName, tostring(err))
            end
        end

        current_parsingAndMeta_resourceCache[resourceName] = lua

        if isParsing then
            tableInsert(stats.insertedNames, resourceName)
        end
    elseif isParsing then
        current_parsing_insertCount = current_parsing_insertCount + 1 -- Note: We don't count insertions of newly encountered files.

        if current_parsing_insertCount > MAX_DUPLICATE_FILE_INSERTS then
            errorAtToken(
                nameTokOrErrLevel,
                nameTokOrErrLevel.position + 1,
                "Parser",
                "Too many duplicate inserts. We may be stuck in a recursive loop. (Unique files inserted so far: %s)",
                stats.insertedNames[1] and table.concat(stats.insertedNames, ", ") or "none"
            )
        end
    end

    return lua
end

--==============================================================
--= Preprocessor Functions =====================================
--==============================================================

-- :EnvironmentTable
----------------------------------------------------------------

metaEnv = copyTable(_G) -- Include all standard Lua stuff.
metaEnv._G = metaEnv

local metaFuncs = {}

-- printf()
--   printf( format, value1, ... )
--   Print a formatted string to stdout.
metaFuncs.printf = printf

-- readFile()
--   contents = readFile( path [, isTextFile=false ] )
--   Get the entire contents of a binary file or text file. Returns nil and a message on error.
metaFuncs.readFile = readFile
metaFuncs.getFileContents = readFile -- @Deprecated

-- writeFile()
--   success, error = writeFile( path, contents ) -- Writes a binary file.
--   success, error = writeFile( path, isTextFile, contents )
--   Write an entire binary file or text file.
metaFuncs.writeFile = writeFile

-- fileExists()
--   bool = fileExists( path )
--   Check if a file exists.
metaFuncs.fileExists = fileExists

-- toLua()
--   luaString = toLua( value )
--   Convert a value to a Lua literal. Does not work with certain types, like functions or userdata.
--   Returns nil and a message on error.
metaFuncs.toLua = toLua

-- serialize()
--   success, error = serialize( buffer, value )
--   Same as toLua() except adds the result to an array instead of returning the Lua code as a string.
--   This could avoid allocating unnecessary strings.
metaFuncs.serialize = serialize

-- evaluate()
--   value = evaluate( expression [, environment=getfenv() ] )
--   Evaluate a Lua expression. The function is kind of the opposite of toLua(). Returns nil and a message on error.
--   Note that nil or false can also be returned as the first value if that's the value the expression results in!
metaFuncs.evaluate = evaluate

-- escapePattern()
--   escapedString = escapePattern( string )
--   Escape a string so it can be used in a pattern as plain text.
metaFuncs.escapePattern = escapePattern

-- isToken()
--   bool = isToken( token, tokenType [, tokenValue=any ] )
--   Check if a token is of a specific type, optionally also check it's value.
metaFuncs.isToken = isToken

-- copyTable()
--   copy = copyTable( table [, deep=false ] )
--   Copy a table, optionally recursively (deep copy).
--   Multiple references to the same table and self-references are preserved during deep copying.
metaFuncs.copyTable = copyTable

-- unpack()
--   value1, ... = unpack( array [, fromIndex=1, toIndex=#array ] )
--   Is _G.unpack() in Lua 5.1 and alias for table.unpack() in Lua 5.2+.
metaFuncs.unpack = unpack

-- pack()
--   values = pack( value1, ... )
--   Put values in a new array. values.n is the amount of values (which can be zero)
--   including nil values. Alias for table.pack() in Lua 5.2+.
metaFuncs.pack = pack

-- pairsSorted()
--   for key, value in pairsSorted( table ) do
--   Same as pairs() but the keys are sorted (ascending).
metaFuncs.pairsSorted = pairsSorted

-- sortNatural()
--   sortNatural( array )
--   Sort an array using compareNatural().
metaFuncs.sortNatural = sortNatural

-- compareNatural()
--   aIsLessThanB = compareNatural( a, b )
--   Compare two strings. Numbers in the strings are compared as numbers (as opposed to as strings).
--   Examples:
--     print(               "foo9" < "foo10" ) -- false
--     print(compareNatural("foo9",  "foo10")) -- true
metaFuncs.compareNatural = compareNatural

-- run()
--   returnValue1, ... = run( path [, arg1, ... ] )
--   Execute a Lua file. Similar to dofile().
function metaFuncs.run(path, ...)
    assertarg(1, path, "string")

    local main_chunk, err = loadLuaFile(path, metaEnv)
    if not main_chunk then
        error(err, 0)
    end

    -- We want multiple return values while avoiding a tail call to preserve stack info.
    local returnValues = pack(main_chunk(...))
    return unpack(returnValues, 1, returnValues.n)
end

-- outputValue()
--   outputValue( value )
--   outputValue( value1, value2, ... ) -- Outputted values will be separated by commas.
--   Output one or more values, like strings or tables, as literals.
--   Raises an error if no file or string is being processed.
function metaFuncs.outputValue(...)
    errorIfNotRunningMeta(2)

    local argCount = select("#", ...)
    if argCount == 0 then
        error("No values to output.", 2)
    end

    for i = 1, argCount do
        local v = select(i, ...)

        if v == nil and not current_meta_canOutputNil then
            local ln = debug.getinfo(2, "l").currentline
            errorOnLine(
                current_meta_pathForErrorMessages,
                ln,
                "MetaProgram",
                "Trying to output nil which is disallowed through params.canOutputNil ."
            )
        end

        if i > 1 then
            tableInsert(current_meta_output, (current_parsingAndMeta_isDebug and ", " or ","))
        end

        local ok, err = serialize(current_meta_output, v)

        if not ok then
            local ln = debug.getinfo(2, "l").currentline
            errorOnLine(current_meta_pathForErrorMessages, ln, "MetaProgram", "%s", err)
        end
    end
end

-- outputLua()
--   outputLua( luaString1, ... )
--   Output one or more strings as raw Lua code.
--   Raises an error if no file or string is being processed.
function metaFuncs.outputLua(...)
    errorIfNotRunningMeta(2)

    local argCount = select("#", ...)
    if argCount == 0 then
        error("No Lua code to output.", 2)
    end

    for i = 1, argCount do
        local lua = select(i, ...)
        assertarg(i, lua, "string")
        tableInsert(current_meta_output, lua)
    end
end

-- outputLuaTemplate()
--   outputLuaTemplate( luaStringTemplate, value1, ... )
--   Use a string as a template for outputting Lua code with values.
--   Question marks (?) are replaced with the values.
--   Raises an error if no file or string is being processed.
--   Examples:
--     outputLuaTemplate("local name, age = ?, ?", "Harry", 48)
--     outputLuaTemplate("dogs[?] = ?", "greyhound", {italian=false, count=5})
function metaFuncs.outputLuaTemplate(lua, ...)
    errorIfNotRunningMeta(2)
    assertarg(1, lua, "string")

    local args = { ... } -- @Memory
    local n = 0
    local v, err

    lua = lua:gsub("%?", function()
        n = n + 1
        v, err = toLua(args[n])

        if not v then
            errorf(3, "Bad argument %d: %s", 1 + n, err)
        end

        return v
    end)

    tableInsert(current_meta_output, lua)
end

-- getOutputSoFar()
--   luaString = getOutputSoFar( [ asTable=false ] )
--   getOutputSoFar( buffer )
--   Get Lua code that's been outputted so far.
--   If asTable is false then the full Lua code string is returned.
--   If asTable is true then an array of Lua code segments is returned. (This avoids allocating, possibly large, strings.)
--   If a buffer array is given then Lua code segments are added to it.
--   Raises an error if no file or string is being processed.
function metaFuncs.getOutputSoFar(bufferOrAsTable)
    errorIfNotRunningMeta(2)

    -- Should there be a way to get the contents of current_meta_output etc.? :GetMoreOutputFromStack

    if type(bufferOrAsTable) == "table" then
        for _, lua in ipairs(current_meta_outputStack[1]) do
            tableInsert(bufferOrAsTable, lua)
        end
        -- Return nothing!
    else
        return bufferOrAsTable and copyArray(current_meta_outputStack[1]) or table.concat(current_meta_outputStack[1])
    end
end

local lineFragments = {}

local function getOutputSoFarOnLine()
    errorIfNotRunningMeta(2)

    local len = 0

    -- Should there be a way to get the contents of current_meta_output etc.? :GetMoreOutputFromStack
    for i = #current_meta_outputStack[1], 1, -1 do
        local fragment = current_meta_outputStack[1][i]

        if fragment:find("\n", 1, true) then
            len = len + 1
            lineFragments[len] = fragment:gsub(".*\n", "")
            break
        end

        len = len + 1
        lineFragments[len] = fragment
    end

    return table.concat(lineFragments, 1, len)
end

-- getOutputSoFarOnLine()
--   luaString = getOutputSoFarOnLine( )
--   Get Lua code that's been outputted so far on the current line.
--   Raises an error if no file or string is being processed.
metaFuncs.getOutputSoFarOnLine = getOutputSoFarOnLine

-- getOutputSizeSoFar()
--   size = getOutputSizeSoFar( )
--   Get the amount of bytes outputted so far.
--   Raises an error if no file or string is being processed.
function metaFuncs.getOutputSizeSoFar()
    errorIfNotRunningMeta(2)

    local size = 0

    for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack
        size = size + #lua
    end

    return size
end

-- getCurrentLineNumberInOutput()
--   lineNumber = getCurrentLineNumberInOutput( )
--   Get the current line number in the output.
function metaFuncs.getCurrentLineNumberInOutput()
    errorIfNotRunningMeta(2)

    local ln = 1

    for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack
        ln = ln + countString(lua, "\n", true)
    end

    return ln
end

local function getIndentation(line, tabWidth)
    if not tabWidth then
        return line:match("^[ \t]*")
    end

    local indent = 0

    for i = 1, #line do
        if line:sub(i, i) == "\t" then
            indent = math.floor(indent / tabWidth) * tabWidth + tabWidth
        elseif line:sub(i, i) == " " then
            indent = indent + 1
        else
            break
        end
    end

    return indent
end

-- getIndentation()
--   string = getIndentation( line )
--   size   = getIndentation( line, tabWidth )
--   Get indentation of a line, either as a string or as a size in spaces.
metaFuncs.getIndentation = getIndentation

-- getCurrentIndentationInOutput()
--   string = getCurrentIndentationInOutput( )
--   size   = getCurrentIndentationInOutput( tabWidth )
--   Get the indentation of the current line, either as a string or as a size in spaces.
function metaFuncs.getCurrentIndentationInOutput(tabWidth)
    errorIfNotRunningMeta(2)
    return (getIndentation(getOutputSoFarOnLine(), tabWidth))
end

-- getCurrentPathIn()
--   path = getCurrentPathIn( )
--   Get what file is currently being processed, if any.
function metaFuncs.getCurrentPathIn()
    return current_anytime_pathIn
end

-- getCurrentPathOut()
--   path = getCurrentPathOut( )
--   Get what file the currently processed file will be written to, if any.
function metaFuncs.getCurrentPathOut()
    return current_anytime_pathOut
end

-- tokenize()
--   tokens = tokenize( luaString [, allowPreprocessorCode=false ] )
--   token = {
--     type=tokenType, representation=representation, value=value,
--     line=lineNumber, lineEnd=lineNumber, position=bytePosition, file=filePath,
--     ...
--   }
--   Convert Lua code to tokens. Returns nil and a message on error. (See newToken() for token types.)
function metaFuncs.tokenize(lua, allowPpCode)
    local ok, errOrTokens = pcall(_tokenize, lua, "<string>", allowPpCode, allowPpCode, true) -- @Incomplete: Make allowJitSyntax a parameter to tokenize()?
    if not ok then
        return nil, cleanError(errOrTokens)
    end
    return errOrTokens
end

-- removeUselessTokens()
--   removeUselessTokens( tokens )
--   Remove whitespace and comment tokens.
function metaFuncs.removeUselessTokens(tokens)
    local len = #tokens
    local offset = 0

    for i, tok in ipairs(tokens) do
        if USELESS_TOKENS[tok.type] then
            offset = offset - 1
        else
            tokens[i + offset] = tok
        end
    end

    for i = len, len + offset + 1, -1 do
        tokens[i] = nil
    end
end

local function nextUsefulToken(tokens, i)
    while true do
        i = i + 1
        local tok = tokens[i]
        if not tok then
            return
        end
        if not USELESS_TOKENS[tok.type] then
            return i, tok
        end
    end
end

-- eachToken()
--   for index, token in eachToken( tokens [, ignoreUselessTokens=false ] ) do
--   Loop through tokens.
function metaFuncs.eachToken(tokens, ignoreUselessTokens)
    if ignoreUselessTokens then
        return nextUsefulToken, tokens, 0
    else
        return ipairs(tokens)
    end
end

-- getNextUsefulToken()
--   token, index = getNextUsefulToken( tokens, startIndex [, steps=1 ] )
--   Get the next token that isn't a whitespace or comment. Returns nil if no more tokens are found.
--   Specify a negative steps value to get an earlier token.
function metaFuncs.getNextUsefulToken(tokens, i1, steps)
    steps = (steps or 1)

    local i2, dir
    if steps == 0 then
        return tokens[i1], i1
    elseif steps < 0 then
        i2, dir = 1, -1
    else
        i2, dir = #tokens, 1
    end

    for i = i1, i2, dir do
        local tok = tokens[i]
        if not USELESS_TOKENS[tok.type] then
            steps = steps - dir
            if steps == 0 then
                return tok, i
            end
        end
    end

    return nil
end

local numberFormatters = {
    auto = function(n)
        return tostring(n)
    end,
    integer = function(n)
        return F("%d", n)
    end,
    int = function(n)
        return F("%d", n)
    end,
    float = function(n)
        return F("%f", n):gsub("(%d)0+$", "%1")
    end,
    scientific = function(n)
        return F("%e", n):gsub("(%d)0+e", "%1e"):gsub("0+(%d+)$", "%1")
    end,
    SCIENTIFIC = function(n)
        return F("%E", n):gsub("(%d)0+E", "%1E"):gsub("0+(%d+)$", "%1")
    end,
    e = function(n)
        return F("%e", n):gsub("(%d)0+e", "%1e"):gsub("0+(%d+)$", "%1")
    end,
    E = function(n)
        return F("%E", n):gsub("(%d)0+E", "%1E"):gsub("0+(%d+)$", "%1")
    end,
    hexadecimal = function(n)
        return (n == math.floor(n) and F("0x%x", n) or error("Hexadecimal floats not supported yet.", 3))
    end, -- @Incomplete
    HEXADECIMAL = function(n)
        return (n == math.floor(n) and F("0x%X", n) or error("Hexadecimal floats not supported yet.", 3))
    end,
    hex = function(n)
        return (n == math.floor(n) and F("0x%x", n) or error("Hexadecimal floats not supported yet.", 3))
    end,
    HEX = function(n)
        return (n == math.floor(n) and F("0x%X", n) or error("Hexadecimal floats not supported yet.", 3))
    end,
}

-- newToken()
--   token = newToken( tokenType, ... )
--   Create a new token. Different token types take different arguments.
--
--   commentToken     = newToken( "comment",     contents [, forceLongForm=false ] )
--   identifierToken  = newToken( "identifier",  identifier )
--   keywordToken     = newToken( "keyword",     keyword )
--   numberToken      = newToken( "number",      number [, numberFormat="auto" ] )
--   punctuationToken = newToken( "punctuation", symbol )
--   stringToken      = newToken( "string",      contents [, longForm=false ] )
--   whitespaceToken  = newToken( "whitespace",  contents )
--   ppEntryToken     = newToken( "pp_entry",    isDouble )
--   ppKeywordToken   = newToken( "pp_keyword",  ppKeyword ) -- ppKeyword can be "file", "insert", "line" or "@".
--   ppSymbolToken    = newToken( "pp_symbol",   identifier )
--
--   commentToken     = { type="comment",     representation=string, value=string, long=isLongForm }
--   identifierToken  = { type="identifier",  representation=string, value=string }
--   keywordToken     = { type="keyword",     representation=string, value=string }
--   numberToken      = { type="number",      representation=string, value=number }
--   punctuationToken = { type="punctuation", representation=string, value=string }
--   stringToken      = { type="string",      representation=string, value=string, long=isLongForm }
--   whitespaceToken  = { type="whitespace",  representation=string, value=string }
--   ppEntryToken     = { type="pp_entry",    representation=string, value=string, double=isDouble }
--   ppKeywordToken   = { type="pp_keyword",  representation=string, value=string }
--   ppSymbolToken    = { type="pp_symbol",   representation=string, value=string }
--
-- Number formats:
--   "integer"      E.g. 42
--   "int"          Same as integer, e.g. 42
--   "float"        E.g. 3.14
--   "scientific"   E.g. 0.7e+12
--   "SCIENTIFIC"   E.g. 0.7E+12 (upper case)
--   "e"            Same as scientific, e.g. 0.7e+12
--   "E"            Same as SCIENTIFIC, e.g. 0.7E+12 (upper case)
--   "hexadecimal"  E.g. 0x19af
--   "HEXADECIMAL"  E.g. 0x19AF (upper case)
--   "hex"          Same as hexadecimal, e.g. 0x19af
--   "HEX"          Same as HEXADECIMAL, e.g. 0x19AF (upper case)
--   "auto"         Note: Infinite numbers and NaN always get automatic format.
--
function metaFuncs.newToken(tokType, ...)
    if tokType == "comment" then
        local comment, long = ...
        long = not not (long or comment:find("[\r\n]"))
        assertarg(2, comment, "string")

        local repr
        if long then
            local equalSigns = ""

            while comment:find(F("]%s]", equalSigns), 1, true) do
                equalSigns = equalSigns .. "="
            end

            repr = F("--[%s[%s]%s]", equalSigns, comment, equalSigns)
        else
            repr = F("--%s\n", comment)
        end

        return { type = "comment", representation = repr, value = comment, long = long }
    elseif tokType == "identifier" then
        local ident = ...
        assertarg(2, ident, "string")

        if ident == "" then
            error("Identifier length is 0.", 2)
        elseif not ident:find("^[%a_][%w_]*$") then
            errorf(2, "Bad identifier format: '%s'", ident)
        elseif KEYWORDS[ident] then
            errorf(2, "Identifier must not be a keyword: '%s'", ident)
        end

        return { type = "identifier", representation = ident, value = ident }
    elseif tokType == "keyword" then
        local keyword = ...
        assertarg(2, keyword, "string")

        if not KEYWORDS[keyword] then
            errorf(2, "Bad keyword '%s'.", keyword)
        end

        return { type = "keyword", representation = keyword, value = keyword }
    elseif tokType == "number" then
        local n, numberFormat = ...
        numberFormat = numberFormat or "auto"
        assertarg(2, n, "number")
        assertarg(3, numberFormat, "string")

        -- Some of these are technically multiple other tokens. We could raise an error but ehhh...
        local numStr = (
            n ~= n and "(0/0)"
            or n == 1 / 0 and "(1/0)"
            or n == -1 / 0 and "(-1/0)"
            or numberFormatters[numberFormat] and numberFormatters[numberFormat](n)
            or errorf(2, "Invalid number format '%s'.", numberFormat)
        )

        return { type = "number", representation = numStr, value = n }
    elseif tokType == "punctuation" then
        local symbol = ...
        assertarg(2, symbol, "string")

        -- Note: "!" and "!!" are of a different token type (pp_entry).
        if not PUNCTUATION[symbol] then
            errorf(2, "Bad symbol '%s'.", symbol)
        end

        return { type = "punctuation", representation = symbol, value = symbol }
    elseif tokType == "string" then
        local s, long = ...
        long = not not long
        assertarg(2, s, "string")

        local repr

        if long then
            local equalSigns = ""

            while s:find(F("]%s]", equalSigns), 1, true) do
                equalSigns = equalSigns .. "="
            end

            repr = F("[%s[%s]%s]", equalSigns, s, equalSigns)
        else
            repr = toLua(s)
        end

        return { type = "string", representation = repr, value = s, long = long }
    elseif tokType == "whitespace" then
        local whitespace = ...
        assertarg(2, whitespace, "string")

        if whitespace == "" then
            error("String is empty.", 2)
        elseif whitespace:find("%S") then
            error("String contains non-whitespace characters.", 2)
        end

        return { type = "whitespace", representation = whitespace, value = whitespace }
    elseif tokType == "pp_entry" then
        local double = ...
        assertarg(2, double, "boolean")

        local symbol = double and "!!" or "!"

        return { type = "pp_entry", representation = symbol, value = symbol, double = double }
    elseif tokType == "pp_keyword" then
        local keyword = ...
        assertarg(2, keyword, "string")

        if keyword == "@" then
            return { type = "pp_keyword", representation = "@@", value = "insert" }
        elseif not PREPROCESSOR_KEYWORDS[keyword] then
            errorf(2, "Bad preprocessor keyword '%s'.", keyword)
        else
            return { type = "pp_keyword", representation = "@" .. keyword, value = keyword }
        end
    elseif tokType == "pp_symbol" then
        local ident = ...
        assertarg(2, ident, "string")

        if ident == "" then
            error("Identifier length is 0.", 2)
        elseif not ident:find("^[%a_][%w_]*$") then
            errorf(2, "Bad identifier format: '%s'", ident)
        elseif KEYWORDS[ident] then
            errorf(2, "Identifier must not be a keyword: '%s'", ident)
        else
            return { type = "pp_symbol", representation = "$" .. ident, value = ident }
        end
    else
        errorf(2, "Invalid token type '%s'.", tostring(tokType))
    end
end

-- concatTokens()
--   luaString = concatTokens( tokens )
--   Concatenate tokens by their representations.
function metaFuncs.concatTokens(tokens)
    return (_concatTokens(tokens, nil, false, nil, nil))
end

local recycledArrays = {}

-- startInterceptingOutput()
--   startInterceptingOutput( )
--   Start intercepting output until stopInterceptingOutput() is called.
--   The function can be called multiple times to intercept interceptions.
function metaFuncs.startInterceptingOutput()
    errorIfNotRunningMeta(2)

    current_meta_output = tableRemove(recycledArrays) or {}
    for i = 1, #current_meta_output do
        current_meta_output[i] = nil
    end
    tableInsert(current_meta_outputStack, current_meta_output)
end

local function _stopInterceptingOutput(errLevel)
    errorIfNotRunningMeta(1 + errLevel)

    local interceptedLua = tableRemove(current_meta_outputStack)
    current_meta_output = current_meta_outputStack[#current_meta_outputStack]
        or error("Called stopInterceptingOutput() before calling startInterceptingOutput().", 1 + errLevel)
    tableInsert(recycledArrays, interceptedLua)

    return table.concat(interceptedLua)
end

-- stopInterceptingOutput()
--   luaString = stopInterceptingOutput( )
--   Stop intercepting output and retrieve collected code.
function metaFuncs.stopInterceptingOutput()
    return (_stopInterceptingOutput(2))
end

-- loadResource()
--   luaString = loadResource( name )
--   Load a Lua file/resource (using the same mechanism as @insert"name").
--   Note that resources are cached after loading once.
function metaFuncs.loadResource(resourceName)
    errorIfNotRunningMeta(2)

    return (_loadResource(resourceName, false, 2))
end

local function isCallable(v)
    return type(v) == "function"
        -- We use debug.getmetatable instead of _G.getmetatable because we don't want to
        -- potentially invoke user code - we just want to know if the value is callable.
        or (type(v) == "table" and debug.getmetatable(v) ~= nil and type(debug.getmetatable(v).__call) == "function")
end

-- callMacro()
--   luaString = callMacro( function|macroName, argument1, ... )
--   Call a macro function (which must be a global in metaEnvironment if macroName is given).
--   The arguments should be Lua code strings.
function metaFuncs.callMacro(nameOrFunc, ...)
    errorIfNotRunningMeta(2)

    assertarg(1, nameOrFunc, "string", "function")
    local f

    if type(nameOrFunc) == "string" then
        local nameResult = current_parsingAndMeta_macroPrefix .. nameOrFunc .. current_parsingAndMeta_macroSuffix
        f = metaEnv[nameResult]

        if not isCallable(f) then
            if nameOrFunc == nameResult then
                errorf(2, "'%s' is not a macro/global function. (Got %s)", nameOrFunc, type(f))
            else
                errorf(
                    2,
                    "'%s' (resolving to '%s') is not a macro/global function. (Got %s)",
                    nameOrFunc,
                    nameResult,
                    type(f)
                )
            end
        end
    else
        f = nameOrFunc
    end

    return (metaEnv.__M()(f(...)))
end

-- isProcessing()
--   bool = isProcessing( )
--   Returns true if a file or string is currently being processed.
function metaFuncs.isProcessing()
    return current_parsingAndMeta_isProcessing
end

-- :PredefinedMacros

-- ASSERT()
--   @@ASSERT( condition [, message=auto ] )
--   Macro. Does nothing if params.release is set, otherwise calls error() if the
--   condition fails. The message argument is only evaluated if the condition fails.
function metaFuncs.ASSERT(conditionCode, messageCode)
    errorIfNotRunningMeta(2)
    if not conditionCode then
        error("missing argument #1 to 'ASSERT'", 2)
    end

    -- if not isLuaStringValidExpression(conditionCode) then
    -- 	errorf(2, "Invalid condition expression: %s", formatCodeForShortMessage(conditionCode))
    -- end

    if current_meta_releaseMode then
        return
    end

    tableInsert(current_meta_output, "if not (")
    tableInsert(current_meta_output, conditionCode)
    tableInsert(current_meta_output, ") then  error(")

    if messageCode then
        tableInsert(current_meta_output, "(")
        tableInsert(current_meta_output, messageCode)
        tableInsert(current_meta_output, ")")
    else
        tableInsert(current_meta_output, F("%q", "Assertion failed: " .. conditionCode))
    end

    tableInsert(current_meta_output, ")  end")
end

-- LOG()
--   @@LOG( logLevel, value )               -- [1]
--   @@LOG( logLevel, format, value1, ... ) -- [2]
--
--   Macro. Does nothing if logLevel is lower than params.logLevel,
--   otherwise prints a value[1] or a formatted message[2].
--
--   logLevel can be "error", "warning", "info", "debug" or "trace"
--   (from highest to lowest priority).
--
function metaFuncs.LOG(logLevelCode, valueOrFormatCode, ...)
    errorIfNotRunningMeta(2)
    if not logLevelCode then
        error("missing argument #1 to 'LOG'", 2)
    end
    if not valueOrFormatCode then
        error("missing argument #2 to 'LOG'", 2)
    end

    local chunk = loadLuaString("return(" .. logLevelCode .. "\n)", "@", dummyEnv)
    if not chunk then
        errorf(2, "Invalid logLevel expression: %s", formatCodeForShortMessage(logLevelCode))
    end

    local ok, logLevel = pcall(chunk)
    if not ok then
        errorf(2, "logLevel must be a constant expression. Got: %s", formatCodeForShortMessage(logLevelCode))
    end
    if not LOG_LEVELS[logLevel] then
        errorf(2, "Invalid logLevel '%s'.", tostring(logLevel))
    end
    if logLevel == "off" then
        errorf(2, "Invalid logLevel '%s'.", tostring(logLevel))
    end

    if LOG_LEVELS[logLevel] > LOG_LEVELS[current_meta_maxLogLevel] then
        return
    end

    tableInsert(current_meta_output, "print(")

    if ... then
        tableInsert(current_meta_output, "string.format(")
        tableInsert(current_meta_output, valueOrFormatCode)
        for i = 1, select("#", ...) do
            tableInsert(current_meta_output, ", ")
            tableInsert(current_meta_output, (select(i, ...)))
        end
        tableInsert(current_meta_output, ")")
    else
        tableInsert(current_meta_output, valueOrFormatCode)
    end

    tableInsert(current_meta_output, ")")
end

-- Extra stuff used by the command line program:
metaFuncs.tryToFormatError = tryToFormatError

----------------------------------------------------------------

for k, v in pairs(metaFuncs) do
    metaEnv[k] = v
end

metaEnv.__LUA = metaEnv.outputLua
metaEnv.__VAL = metaEnv.outputValue

function metaEnv.__TOLUA(v)
    return (assert(toLua(v)))
end
function metaEnv.__ISLUA(lua)
    if type(lua) ~= "string" then
        error("Value is not Lua code.", 2)
    end
    return lua
end

local function finalizeMacro(lua)
    if lua == nil then
        return (_stopInterceptingOutput(2))
    elseif type(lua) ~= "string" then
        errorf(2, "[Macro] Value is not Lua code. (Got %s)", type(lua))
    elseif current_meta_output[1] then
        error("[Macro] Got Lua code from both value expression and outputLua(). Only one method may be used.", 2) -- It's also possible interception calls are unbalanced.
    else
        _stopInterceptingOutput(2) -- Returns "" because nothing was outputted.
        return lua
    end
end
function metaEnv.__M()
    metaFuncs.startInterceptingOutput()
    return finalizeMacro
end

-- luaString = __ARG( locationTokenNumber, luaString|callback )
-- callback  = function( )
function metaEnv.__ARG(locTokNum, v)
    local lua
    if type(v) == "string" then
        lua = v
    else
        metaFuncs.startInterceptingOutput()
        v()
        lua = _stopInterceptingOutput(2)
    end

    if current_parsingAndMeta_strictMacroArguments and not isLuaStringValidExpression(lua) then
        runtimeErrorAtToken(
            2,
            current_meta_locationTokens[locTokNum],
            nil,
            "MacroArgument",
            "Argument result is not a valid Lua expression: %s",
            formatCodeForShortMessage(lua)
        )
    end

    return lua
end

function metaEnv.__EVAL(v) -- For symbols.
    if isCallable(v) then
        v = v()
    end
    return v
end

local function getLineCountWithCode(tokens)
    local lineCount = 0
    local lastLine = 0

    for _, tok in ipairs(tokens) do
        if not USELESS_TOKENS[tok.type] and tok.lineEnd > lastLine then
            lineCount = lineCount + (tok.lineEnd - tok.line + 1)
            lastLine = tok.lineEnd
        end
    end

    return lineCount
end

--
-- Preprocessor expansions (symbols etc., not macros).
--

local function newTokenAt(tok, locTok)
    tok.line = tok.line or locTok and locTok.line
    tok.lineEnd = tok.lineEnd or locTok and locTok.lineEnd
    tok.position = tok.position or locTok and locTok.position
    tok.file = tok.file or locTok and locTok.file
    return tok
end

local function popTokens(tokenStack, lastIndexToPop)
    for i = #tokenStack, lastIndexToPop, -1 do
        tokenStack[i] = nil
    end
end
local function popUseless(tokenStack)
    for i = #tokenStack, 1, -1 do
        if not USELESS_TOKENS[tokenStack[i].type] then
            break
        end
        tokenStack[i] = nil
    end
end

local function advanceToken(tokens)
    local tok = tokens[tokens.nextI]
    tokens.nextI = tokens.nextI + 1
    return tok
end
local function advancePastUseless(tokens)
    for i = tokens.nextI, #tokens do
        if not USELESS_TOKENS[tokens[i].type] then
            break
        end
        tokens.nextI = i + 1
    end
end

-- outTokens = doEarlyExpansions( tokensToExpand, stats )
local function doEarlyExpansions(tokensToExpand, stats)
    --
    -- Here we expand simple things that makes it easier for
    -- doLateExpansions*() to do more elaborate expansions.
    --
    -- Expand expressions:
    --   @file
    --   @line
    --   ` ... `
    --   $symbol
    --
    local tokenStack = {} -- We process the last token first, and we may push new tokens onto the stack.
    local outTokens = {}

    for i = #tokensToExpand, 1, -1 do
        tableInsert(tokenStack, tokensToExpand[i])
    end

    while tokenStack[1] do
        local tok = tokenStack[#tokenStack]

        -- Keyword.
        if isToken(tok, "pp_keyword") then
            local ppKeywordTok = tok

            -- @file
            -- @line
            if ppKeywordTok.value == "file" then
                tableRemove(tokenStack) -- '@file'
                tableInsert(
                    outTokens,
                    newTokenAt(
                        { type = "string", value = ppKeywordTok.file, representation = F("%q", ppKeywordTok.file) },
                        ppKeywordTok
                    )
                )
            elseif ppKeywordTok.value == "line" then
                tableRemove(tokenStack) -- '@line'
                tableInsert(
                    outTokens,
                    newTokenAt(
                        { type = "number", value = ppKeywordTok.line, representation = F(" %d ", ppKeywordTok.line) },
                        ppKeywordTok
                    )
                ) -- Is it fine for the representation to have spaces? Probably.
            else
                -- Expand later.
                tableInsert(outTokens, ppKeywordTok)
                tableRemove(tokenStack) -- '@...'
            end

        -- Backtick string.
        elseif isToken(tok, "string") and tok.representation:find("^`") then
            local stringTok = tok
            stringTok.representation = toLua(stringTok.value) --F("%q", stringTok.value)

            tableInsert(outTokens, stringTok)
            tableRemove(tokenStack) -- the string

        -- Symbol. (Should this expand later? Does it matter? Yeah, do this in the AST code instead. @Cleanup)
        elseif isToken(tok, "pp_symbol") then
            local ppSymbolTok = tok

            -- $symbol
            tableRemove(tokenStack) -- '$symbol'
            tableInsert(
                outTokens,
                newTokenAt({ type = "pp_entry", value = "!!", representation = "!!", double = true }, ppSymbolTok)
            )
            tableInsert(
                outTokens,
                newTokenAt({
                    type = "punctuation",
                    value = "(",
                    representation = "(",
                }, ppSymbolTok)
            )
            tableInsert(
                outTokens,
                newTokenAt({ type = "identifier", value = "__EVAL", representation = "__EVAL" }, ppSymbolTok)
            )
            tableInsert(
                outTokens,
                newTokenAt({
                    type = "punctuation",
                    value = "(",
                    representation = "(",
                }, ppSymbolTok)
            )
            tableInsert(
                outTokens,
                newTokenAt(
                    { type = "identifier", value = ppSymbolTok.value, representation = ppSymbolTok.value },
                    ppSymbolTok
                )
            )
            tableInsert(
                outTokens,
                newTokenAt({
                    type = "punctuation",
                    value = ")",
                    representation = ")",
                }, ppSymbolTok)
            )
            tableInsert(
                outTokens,
                newTokenAt({
                    type = "punctuation",
                    value = ")",
                    representation = ")",
                }, ppSymbolTok)
            )

        -- Anything else.
        else
            tableInsert(outTokens, tok)
            tableRemove(tokenStack) -- anything
        end
    end --while tokenStack

    return outTokens
end

-- outTokens = doLateExpansions( tokensToExpand, stats, allowBacktickStrings, allowJitSyntax )
local function doLateExpansions(tokensToExpand, stats, allowBacktickStrings, allowJitSyntax)
    --
    -- Expand expressions:
    --   @insert "name"
    --
    local tokenStack = {} -- We process the last token first, and we may push new tokens onto the stack.
    local outTokens = {}

    for i = #tokensToExpand, 1, -1 do
        tableInsert(tokenStack, tokensToExpand[i])
    end

    while tokenStack[1] do
        local tok = tokenStack[#tokenStack]

        -- Keyword.
        if isToken(tok, "pp_keyword") then
            local ppKeywordTok = tok
            local tokNext, iNext = getNextUsableToken(tokenStack, #tokenStack - 1, nil, -1)

            -- @insert "name"
            if
                ppKeywordTok.value == "insert"
                and isTokenAndNotNil(tokNext, "string")
                and tokNext.file == ppKeywordTok.file
            then
                local nameTok = tokNext
                popTokens(tokenStack, iNext) -- the string

                local toInsertName = nameTok.value
                local toInsertLua = _loadResource(toInsertName, true, nameTok, stats)
                local toInsertTokens = _tokenize(toInsertLua, toInsertName, true, allowBacktickStrings, allowJitSyntax)
                toInsertTokens = doEarlyExpansions(toInsertTokens, stats)

                for i = #toInsertTokens, 1, -1 do
                    tableInsert(tokenStack, toInsertTokens[i])
                end

                local lastTok = toInsertTokens[#toInsertTokens]
                stats.processedByteCount = stats.processedByteCount + #toInsertLua
                stats.lineCount = stats.lineCount
                    + (lastTok and lastTok.line + countString(lastTok.representation, "\n", true) or 0)
                stats.lineCountCode = stats.lineCountCode + getLineCountWithCode(toInsertTokens)

            -- @insert identifier ( argument1, ... )
            -- @insert identifier " ... "
            -- @insert identifier { ... }
            -- @insert identifier !( ... )
            -- @insert identifier !!( ... )
            elseif
                ppKeywordTok.value == "insert"
                and isTokenAndNotNil(tokNext, "identifier")
                and tokNext.file == ppKeywordTok.file
            then
                local identTok = tokNext
                tokNext, iNext = getNextUsableToken(tokenStack, iNext - 1, nil, -1)

                if
                    not (
                        tokNext
                        and (
                            tokNext.type == "string"
                            or (tokNext.type == "punctuation" and isAny(tokNext.value, "(", "{", ".", ":", "["))
                            or tokNext.type == "pp_entry"
                        )
                    )
                then
                    errorAtToken(
                        identTok,
                        identTok.position + #identTok.representation,
                        "Parser/Macro",
                        "Expected '(' after macro name '%s'.",
                        identTok.value
                    )
                end

                -- Expand later.
                tableInsert(outTokens, tok)
                tableRemove(tokenStack) -- '@insert'
            elseif ppKeywordTok.value == "insert" then
                errorAtToken(
                    ppKeywordTok,
                    (tokNext and tokNext.position or ppKeywordTok.position + #ppKeywordTok.representation),
                    "Parser",
                    "Expected a string or identifier after %s.",
                    ppKeywordTok.representation
                )
            else
                errorAtToken(ppKeywordTok, nil, "Parser", "Internal error. (%s)", ppKeywordTok.value)
            end

        -- Anything else.
        else
            tableInsert(outTokens, tok)
            tableRemove(tokenStack) -- anything
        end
    end --while tokenStack

    return outTokens
end

-- outTokens = doExpansions( params, tokensToExpand, stats )
local function doExpansions(params, tokens, stats)
    tokens = doEarlyExpansions(tokens, stats)
    tokens = doLateExpansions(tokens, stats, params.backtickStrings, params.jitSyntax) -- Resources.
    return tokens
end

--
-- Metaprogram generation.
--

local function AstSequence(locTok, tokens)
    return {
        type = "sequence",
        locationToken = locTok,
        nodes = tokens or {},
    }
end
local function AstLua(locTok, tokens)
    return { -- plain Lua
        type = "lua",
        locationToken = locTok,
        tokens = tokens or {},
    }
end
local function AstMetaprogram(locTok, tokens)
    return { -- `!(statements)` or `!statements`
        type = "metaprogram",
        locationToken = locTok,
        originIsLine = false,
        tokens = tokens or {},
    }
end
local function AstExpressionCode(locTok, tokens)
    return { -- `!!(expression)`
        type = "expressionCode",
        locationToken = locTok,
        tokens = tokens or {},
    }
end
local function AstExpressionValue(locTok, tokens)
    return { -- `!(expression)`
        type = "expressionValue",
        locationToken = locTok,
        tokens = tokens or {},
    }
end
local function AstDualCode(locTok, valueTokens)
    return { -- `!!declaration` or `!!assignment`
        type = "dualCode",
        locationToken = locTok,
        isDeclaration = false,
        names = {},
        valueTokens = valueTokens or {},
    }
end
-- local function AstSymbol(locTok) return { -- `$name`
-- 	type          = "symbol",
-- 	locationToken = locTok,
-- 	name          = "",
-- } end
local function AstMacro(locTok, calleeTokens)
    return { -- `@@callee(arguments)` or `@@callee{}` or `@@callee""`
        type = "macro",
        locationToken = locTok,
        calleeTokens = calleeTokens or {},
        arguments = {}, -- []MacroArgument
    }
end
local function MacroArgument(locTok, nodes)
    return {
        locationToken = locTok,
        isComplex = false,
        nodes = nodes or {},
    }
end

local astParseMetaBlockOrLine

local function astParseMetaBlock(tokens)
    local ppEntryTokIndex = tokens.nextI
    local ppEntryTok = tokens[ppEntryTokIndex]
    tokens.nextI = tokens.nextI + 2 -- '!(' or '!!('

    local outTokens = {}
    local depthStack = {}

    while true do
        local tok = tokens[tokens.nextI]

        if not tok then
            if depthStack[1] then
                tok = depthStack[#depthStack].startToken
                errorAtToken(
                    tok,
                    nil,
                    "Parser/MetaBlock",
                    "Could not find matching bracket before EOF. (Preprocessor line starts %s)",
                    getRelativeLocationText(ppEntryTok, tok)
                )
            end
            break
        end

        -- End of meta block.
        if not depthStack[1] and isToken(tok, "punctuation", ")") then
            tokens.nextI = tokens.nextI + 1 -- after ')'
            break

        -- Nested metaprogram (not supported).
        elseif tok.type:find("^pp_") then
            errorAtToken(
                tok,
                nil,
                "Parser/MetaBlock",
                "Preprocessor token inside metaprogram (starting %s).",
                getRelativeLocationText(ppEntryTok, tok)
            )

        -- Continuation of meta block.
        else
            if isToken(tok, "punctuation", "(") then
                tableInsert(depthStack, {
                    startToken = tok, --[[1]]
                    "punctuation", --[[2]]
                    ")",
                })
            elseif isToken(tok, "punctuation", "[") then
                tableInsert(depthStack, {
                    startToken = tok, --[[1]]
                    "punctuation", --[[2]]
                    "]",
                })
            elseif isToken(tok, "punctuation", "{") then
                tableInsert(depthStack, {
                    startToken = tok, --[[1]]
                    "punctuation", --[[2]]
                    "}",
                })
            elseif
                isToken(tok, "punctuation", ")")
                or isToken(tok, "punctuation", "]")
                or isToken(tok, "punctuation", "}")
            then
                if not depthStack[1] then
                    errorAtToken(
                        tok,
                        nil,
                        "Parser/MetaBlock",
                        "Unexpected '%s'. (Preprocessor line starts %s)",
                        tok.value,
                        getRelativeLocationText(ppEntryTok, tok)
                    )
                elseif not isToken(tok, unpack(depthStack[#depthStack])) then
                    local startTok = depthStack[#depthStack].startToken
                    errorAtToken(
                        tok,
                        nil,
                        "Parser/MetaBlock",
                        "Expected '%s' (to close '%s' %s) but got '%s'. (Preprocessor line starts %s)",
                        depthStack[#depthStack][2],
                        startTok.value,
                        getRelativeLocationText(startTok, tok),
                        tok.value,
                        getRelativeLocationText(ppEntryTok, tok)
                    )
                end
                tableRemove(depthStack)
            end

            tableInsert(outTokens, tok)
            tokens.nextI = tokens.nextI + 1 -- after anything
        end
    end

    local lua = _concatTokens(outTokens, nil, false, nil, nil)
    local chunk, err = loadLuaString("return 0," .. lua .. "\n,0", "@", nil)
    local isExpression = (chunk ~= nil)

    if not isExpression and ppEntryTok.double then
        errorAtToken(tokens[ppEntryTokIndex + 1], nil, "Parser/MetaBlock", "Invalid expression in preprocessor block.")
        -- err = err:gsub("^:%d+: ", "")
        -- errorAtToken(tokens[ppEntryTokIndex+1], nil, "Parser/MetaBlock", "Invalid expression in preprocessor block. (%s)", err)
    elseif isExpression and not isLuaStringValidExpression(lua) then
        if #lua > 100 then
            lua = lua:sub(1, 50) .. "..." .. lua:sub(-50)
        end
        errorAtToken(
            tokens[ppEntryTokIndex + 1],
            nil,
            "Parser/MetaBlock",
            "Ambiguous expression '%s'. (Comma-separated list?)",
            formatCodeForShortMessage(lua)
        )
    end

    local astOutNode = (
        (ppEntryTok.double and AstExpressionCode) or (isExpression and AstExpressionValue or AstMetaprogram)
    )(ppEntryTok, outTokens)
    return astOutNode
end

local function astParseMetaLine(tokens)
    local ppEntryTok = tokens[tokens.nextI]
    tokens.nextI = tokens.nextI + 1 -- '!' or '!!'

    local isDual = ppEntryTok.double
    local astOutNode = (isDual and AstDualCode or AstMetaprogram)(ppEntryTok)

    if astOutNode.type == "metaprogram" then
        astOutNode.originIsLine = true
    end

    if isDual then
        -- We expect the statement to look like any of these:
        -- !!local x, y = ...
        -- !!x, y = ...
        local tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)

        if isTokenAndNotNil(tokNext, "keyword", "local") then
            astOutNode.isDeclaration = true

            tokens.nextI = iNext + 1 -- after 'local'
            tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)
        end

        local usedNames = {}

        while true do
            if not isTokenAndNotNil(tokNext, "identifier") then
                local tok = tokNext or tokens[#tokens]
                errorAtToken(
                    tok,
                    nil,
                    "Parser/DualCodeLine",
                    "Expected %sidentifier. (Preprocessor line starts %s)",
                    (astOutNode.names[1] and "" or "'local' or "),
                    getRelativeLocationText(ppEntryTok, tok)
                )
            elseif usedNames[tokNext.value] then
                errorAtToken(
                    tokNext,
                    nil,
                    "Parser/DualCodeLine",
                    "Duplicate name '%s' in %s. (Preprocessor line starts %s)",
                    tokNext.value,
                    (astOutNode.isDeclaration and "declaration" or "assignment"),
                    getRelativeLocationText(ppEntryTok, tokNext)
                )
            end
            tableInsert(astOutNode.names, tokNext.value)
            usedNames[tokNext.value] = tokNext
            tokens.nextI = iNext + 1 -- after the identifier
            tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)

            if not isTokenAndNotNil(tokNext, "punctuation", ",") then
                break
            end
            tokens.nextI = iNext + 1 -- after ','
            tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)
        end

        if not isTokenAndNotNil(tokNext, "punctuation", "=") then
            local tok = tokNext or tokens[#tokens]
            errorAtToken(
                tok,
                nil,
                "Parser/DualCodeLine",
                "Expected '=' in %s. (Preprocessor line starts %s)",
                (astOutNode.isDeclaration and "declaration" or "assignment"),
                getRelativeLocationText(ppEntryTok, tok)
            )
        end
        tokens.nextI = iNext + 1 -- after '='
    end

    -- Find end of metaprogram line.
    local outTokens = isDual and astOutNode.valueTokens or astOutNode.tokens
    local depthStack = {}

    while true do
        local tok = tokens[tokens.nextI]

        if not tok then
            if depthStack[1] then
                tok = depthStack[#depthStack].startToken
                errorAtToken(
                    tok,
                    nil,
                    "Parser/MetaLine",
                    "Could not find matching bracket before EOF. (Preprocessor line starts %s)",
                    getRelativeLocationText(ppEntryTok, tok)
                )
            end
            break
        end

        -- End of meta line.
        if
            not depthStack[1]
            and (
                (tok.type == "whitespace" and tok.value:find("\n", 1, true))
                or (tok.type == "comment" and not tok.long)
            )
        then
            tableInsert(outTokens, tok)
            tokens.nextI = tokens.nextI + 1 -- after the whitespace or comment
            break

        -- Nested metaprogram (not supported).
        elseif tok.type:find("^pp_") then
            errorAtToken(
                tok,
                nil,
                "Parser/MetaLine",
                "Preprocessor token inside metaprogram (starting %s).",
                getRelativeLocationText(ppEntryTok, tok)
            )

        -- Continuation of meta line.
        else
            if isToken(tok, "punctuation", "(") then
                tableInsert(depthStack, {
                    startToken = tok, --[[1]]
                    "punctuation", --[[2]]
                    ")",
                })
            elseif isToken(tok, "punctuation", "[") then
                tableInsert(depthStack, {
                    startToken = tok, --[[1]]
                    "punctuation", --[[2]]
                    "]",
                })
            elseif isToken(tok, "punctuation", "{") then
                tableInsert(depthStack, {
                    startToken = tok, --[[1]]
                    "punctuation", --[[2]]
                    "}",
                })
            elseif
                isToken(tok, "punctuation", ")")
                or isToken(tok, "punctuation", "]")
                or isToken(tok, "punctuation", "}")
            then
                if not depthStack[1] then
                    errorAtToken(
                        tok,
                        nil,
                        "Parser/MetaLine",
                        "Unexpected '%s'. (Preprocessor line starts %s)",
                        tok.value,
                        getRelativeLocationText(ppEntryTok, tok)
                    )
                elseif not isToken(tok, unpack(depthStack[#depthStack])) then
                    local startTok = depthStack[#depthStack].startToken
                    errorAtToken(
                        tok,
                        nil,
                        "Parser/MetaLine",
                        "Expected '%s' (to close '%s' %s) but got '%s'. (Preprocessor line starts %s)",
                        depthStack[#depthStack][2],
                        startTok.value,
                        getRelativeLocationText(startTok, tok),
                        tok.value,
                        getRelativeLocationText(ppEntryTok, tok)
                    )
                end
                tableRemove(depthStack)
            end

            tableInsert(outTokens, tok)
            tokens.nextI = tokens.nextI + 1 -- after anything
        end
    end

    return astOutNode
end

--[[local]]
function astParseMetaBlockOrLine(tokens)
    return isTokenAndNotNil(tokens[tokens.nextI + 1], "punctuation", "(") and astParseMetaBlock(tokens)
        or astParseMetaLine(tokens)
end

local function astParseMacro(params, tokens)
    local macroStartTok = tokens[tokens.nextI]
    tokens.nextI = tokens.nextI + 1 -- after '@insert'

    local astMacro = AstMacro(macroStartTok)

    --
    -- Callee.
    --

    -- Add 'ident' for start of (or whole) callee.
    local tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)
    if not isTokenAndNotNil(tokNext, "identifier") then
        printErrorTraceback("Internal error.")
        errorAtToken(tokNext, nil, "Parser/Macro", "Internal error. (%s)", (tokNext and tokNext.type or "?"))
    end
    tokens.nextI = iNext + 1 -- after the identifier
    tableInsert(astMacro.calleeTokens, tokNext)
    local initialCalleeIdentTok = tokNext

    -- Add macro prefix and suffix. (Note: We only edit the initial identifier in the callee if there are more.)
    initialCalleeIdentTok.value = current_parsingAndMeta_macroPrefix
        .. initialCalleeIdentTok.value
        .. current_parsingAndMeta_macroSuffix
    initialCalleeIdentTok.representation = initialCalleeIdentTok.value

    -- Maybe add '.field[expr]:method' for rest of callee.
    tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)

    while tokNext do
        if isToken(tokNext, "punctuation", ".") or isToken(tokNext, "punctuation", ":") then
            local punctTok = tokNext
            tokens.nextI = iNext + 1 -- after '.' or ':'
            tableInsert(astMacro.calleeTokens, tokNext)

            tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)
            if not tokNext then
                errorAfterToken(punctTok, "Parser/Macro", "Expected an identifier after '%s'.", punctTok.value)
            end
            tokens.nextI = iNext + 1 -- after the identifier
            tableInsert(astMacro.calleeTokens, tokNext)

            tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)

            if punctTok.value == ":" then
                break
            end
        elseif isToken(tokNext, "punctuation", "[") then
            local punctTok = tokNext
            tokens.nextI = iNext + 1 -- after '['
            tableInsert(astMacro.calleeTokens, tokNext)

            local bracketBalance = 1

            while true do
                tokNext = advanceToken(tokens) -- anything
                if not tokNext then
                    errorAtToken(
                        punctTok,
                        nil,
                        "Parser/Macro",
                        "Could not find matching bracket before EOF. (Macro starts %s)",
                        getRelativeLocationText(macroStartTok, punctTok)
                    )
                end
                tableInsert(astMacro.calleeTokens, tokNext)

                if isToken(tokNext, "punctuation", "[") then
                    bracketBalance = bracketBalance + 1
                elseif isToken(tokNext, "punctuation", "]") then
                    bracketBalance = bracketBalance - 1
                    if bracketBalance == 0 then
                        break
                    end
                elseif tokNext.type:find("^pp_") then
                    errorAtToken(
                        tokNext,
                        nil,
                        "Parser/Macro",
                        "Preprocessor token inside metaprogram/macro name expression (starting %s).",
                        getRelativeLocationText(macroStartTok, tokNext)
                    )
                end
            end

            tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)

            -- @UX: Validate that the contents form an expression.
        else
            break
        end
    end

    --
    -- Arguments.
    --

    -- @insert identifier " ... "
    if isTokenAndNotNil(tokNext, "string") then
        tableInsert(astMacro.arguments, MacroArgument(tokNext, { AstLua(tokNext, { tokNext }) })) -- The one and only argument for this macro variant.
        tokens.nextI = iNext + 1 -- after the string

    -- @insert identifier { ... } -- Same as: @insert identifier ( { ... } )
    elseif isTokenAndNotNil(tokNext, "punctuation", "{") then
        local macroArg = MacroArgument(tokNext) -- The one and only argument for this macro variant.
        astMacro.arguments[1] = macroArg

        local astLuaInCurrentArg = AstLua(tokNext, { tokNext })
        tableInsert(macroArg.nodes, astLuaInCurrentArg)

        tokens.nextI = iNext + 1 -- after '{'

        --
        -- (Similar code as `@insert identifier()` below.)
        --

        -- Collect tokens for the table arg.
        -- We're looking for the closing '}'.
        local bracketDepth = 1 -- @Incomplete: Track all brackets!

        while true do
            local tok = tokens[tokens.nextI]

            if not tok then
                errorAtToken(
                    macroArg.locationToken,
                    nil,
                    "Parser/MacroArgument",
                    "Could not find end of table constructor before EOF."
                )

            -- Preprocessor block in macro.
            elseif tok.type == "pp_entry" then
                tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens))
                astLuaInCurrentArg = nil

            -- Nested macro.
            elseif isToken(tok, "pp_keyword", "insert") then
                tableInsert(macroArg.nodes, astParseMacro(params, tokens))
                astLuaInCurrentArg = nil

            -- Other preprocessor code in macro. (Not sure we ever get here.)
            elseif tok.type:find("^pp_") then
                errorAtToken(
                    tok,
                    nil,
                    "Parser/MacroArgument",
                    "Unsupported preprocessor code. (Macro starts %s)",
                    getRelativeLocationText(macroStartTok, tok)
                )

            -- End of table and argument.
            elseif bracketDepth == 1 and isToken(tok, "punctuation", "}") then
                if not astLuaInCurrentArg then
                    astLuaInCurrentArg = AstLua(tok)
                    tableInsert(macroArg.nodes, astLuaInCurrentArg)
                end
                tableInsert(astLuaInCurrentArg.tokens, tok)
                advanceToken(tokens) -- '}'
                break

            -- Normal token.
            else
                if isToken(tok, "punctuation", "{") then
                    bracketDepth = bracketDepth + 1
                elseif isToken(tok, "punctuation", "}") then
                    bracketDepth = bracketDepth - 1
                end

                if not astLuaInCurrentArg then
                    astLuaInCurrentArg = AstLua(tok)
                    tableInsert(macroArg.nodes, astLuaInCurrentArg)
                end
                tableInsert(astLuaInCurrentArg.tokens, tok)
                advanceToken(tokens) -- anything
            end
        end

    -- @insert identifier ( argument1, ... )
    elseif isTokenAndNotNil(tokNext, "punctuation", "(") then
        -- Apply the same 'ambiguous syntax' rule as Lua. (Will comments mess this check up? @Check)
        if isTokenAndNotNil(tokens[iNext - 1], "whitespace") and tokens[iNext - 1].value:find("\n", 1, true) then
            errorAtToken(tokNext, nil, "Parser/Macro", "Ambiguous syntax near '(' - part of macro, or new statement?")
        end

        local parensStartTok = tokNext
        tokens.nextI = iNext + 1 -- after '('
        tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1)

        if isTokenAndNotNil(tokNext, "punctuation", ")") then
            tokens.nextI = iNext + 1 -- after ')'
        else
            for argNum = 1, 1 / 0 do
                -- Collect tokens for this arg.
                -- We're looking for the next comma at depth 0 or closing ')'.
                local macroArg = MacroArgument(tokens[tokens.nextI])
                astMacro.arguments[argNum] = macroArg

                advancePastUseless(tokens) -- Trim leading useless tokens.

                local astLuaInCurrentArg = nil
                local depthStack = {}

                while true do
                    local tok = tokens[tokens.nextI]

                    if not tok then
                        errorAtToken(
                            parensStartTok,
                            nil,
                            "Parser/Macro",
                            "Could not find end of argument list before EOF."
                        )

                    -- Preprocessor block in macro.
                    elseif tok.type == "pp_entry" then
                        tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens))
                        astLuaInCurrentArg = nil

                    -- Nested macro.
                    elseif isToken(tok, "pp_keyword", "insert") then
                        tableInsert(macroArg.nodes, astParseMacro(params, tokens))
                        astLuaInCurrentArg = nil

                    -- Other preprocessor code in macro. (Not sure we ever get here.)
                    elseif tok.type:find("^pp_") then
                        errorAtToken(
                            tok,
                            nil,
                            "Parser/MacroArgument",
                            "Unsupported preprocessor code. (Macro starts %s)",
                            getRelativeLocationText(macroStartTok, tok)
                        )

                    -- End of argument.
                    elseif
                        not depthStack[1] and (isToken(tok, "punctuation", ",") or isToken(tok, "punctuation", ")"))
                    then
                        break

                    -- Normal token.
                    else
                        if isToken(tok, "punctuation", "(") then
                            tableInsert(depthStack, {
                                startToken = tok, --[[1]]
                                "punctuation", --[[2]]
                                ")",
                            })
                        elseif isToken(tok, "punctuation", "[") then
                            tableInsert(depthStack, {
                                startToken = tok, --[[1]]
                                "punctuation", --[[2]]
                                "]",
                            })
                        elseif isToken(tok, "punctuation", "{") then
                            tableInsert(depthStack, {
                                startToken = tok, --[[1]]
                                "punctuation", --[[2]]
                                "}",
                            })
                        elseif
                            isToken(tok, "keyword", "function")
                            or isToken(tok, "keyword", "if")
                            or isToken(tok, "keyword", "do")
                        then
                            tableInsert(depthStack, {
                                startToken = tok, --[[1]]
                                "keyword", --[[2]]
                                "end",
                            })
                        elseif isToken(tok, "keyword", "repeat") then
                            tableInsert(depthStack, {
                                startToken = tok, --[[1]]
                                "keyword", --[[2]]
                                "until",
                            })
                        elseif
                            isToken(tok, "punctuation", ")")
                            or isToken(tok, "punctuation", "]")
                            or isToken(tok, "punctuation", "}")
                            or isToken(tok, "keyword", "end")
                            or isToken(tok, "keyword", "until")
                        then
                            if not depthStack[1] then
                                errorAtToken(tok, nil, "Parser/MacroArgument", "Unexpected '%s'.", tok.value)
                            elseif not isToken(tok, unpack(depthStack[#depthStack])) then
                                local startTok = depthStack[#depthStack].startToken
                                errorAtToken(
                                    tok,
                                    nil,
                                    "Parser/MacroArgument",
                                    "Expected '%s' (to close '%s' %s) but got '%s'.",
                                    depthStack[#depthStack][2],
                                    startTok.value,
                                    getRelativeLocationText(startTok, tok),
                                    tok.value
                                )
                            end
                            tableRemove(depthStack)
                        end

                        if not astLuaInCurrentArg then
                            astLuaInCurrentArg = AstLua(tok)
                            tableInsert(macroArg.nodes, astLuaInCurrentArg)
                        end
                        tableInsert(astLuaInCurrentArg.tokens, tok)
                        advanceToken(tokens) -- anything
                    end
                end

                if astLuaInCurrentArg then
                    -- Trim trailing useless tokens.
                    popUseless(astLuaInCurrentArg.tokens)
                    if not astLuaInCurrentArg.tokens[1] then
                        assert(tableRemove(macroArg.nodes) == astLuaInCurrentArg)
                    end
                end

                if not macroArg.nodes[1] and current_parsingAndMeta_strictMacroArguments then
                    -- There were no useful tokens for the argument!
                    errorAtToken(macroArg.locationToken, nil, "Parser/MacroArgument", "Expected argument #%d.", argNum)
                end

                -- Do next argument or finish arguments.
                if isTokenAndNotNil(tokens[tokens.nextI], "punctuation", ")") then
                    tokens.nextI = tokens.nextI + 1 -- after ')'
                    break
                end

                assert(isToken(advanceToken(tokens), "punctuation", ",")) -- The loop above should have continued otherwise!
            end --for argNum
        end

    -- @insert identifier !( ... )  -- Same as: @insert identifier ( !( ... ) )
    -- @insert identifier !!( ... ) -- Same as: @insert identifier ( !!( ... ) )
    elseif isTokenAndNotNil(tokNext, "pp_entry") then
        tokens.nextI = iNext -- until '!' or '!!'

        if not isTokenAndNotNil(tokens[tokens.nextI + 1], "punctuation", "(") then
            errorAfterToken(tokNext, "Parser/Macro", "Expected '(' after '%s'.", tokNext.value)
        end

        astMacro.arguments[1] = MacroArgument(tokNext, { astParseMetaBlock(tokens) }) -- The one and only argument for this macro variant.
    else
        errorAfterToken(astMacro.calleeTokens[#astMacro.calleeTokens], "Parser/Macro", "Expected '(' after macro name.")
    end

    return astMacro
end

local function astParse(params, tokens)
    -- @Robustness: Make sure everywhere that key tokens came from the same source file.
    local astSequence = AstSequence(tokens[1])
    tokens.nextI = 1

    while true do
        local tok = tokens[tokens.nextI]
        if not tok then
            break
        end

        if isToken(tok, "pp_entry") then
            tableInsert(astSequence.nodes, astParseMetaBlockOrLine(tokens))
        elseif isToken(tok, "pp_keyword", "insert") then
            local astMacro = astParseMacro(params, tokens)
            tableInsert(astSequence.nodes, astMacro)

        -- elseif isToken(tok, "pp_symbol") then -- We currently expand these in doEarlyExpansions().
        -- 	errorAtToken(tok, nil, "Parser", "Internal error: @Incomplete: Handle symbols.")
        else
            local astLua = AstLua(tok)
            tableInsert(astSequence.nodes, astLua)

            while true do
                tableInsert(astLua.tokens, tok)
                advanceToken(tokens)

                tok = tokens[tokens.nextI]
                if not tok then
                    break
                end
                if tok.type:find("^pp_") then
                    break
                end
            end
        end
    end

    return astSequence
end

-- lineNumber, lineNumberMeta = astNodeToMetaprogram( buffer, ast, lineNumber, lineNumberMeta, asMacroArgumentExpression )
local function astNodeToMetaprogram(buffer, ast, ln, lnMeta, asMacroArgExpr)
    if current_parsingAndMeta_addLineNumbers and not asMacroArgExpr then
        lnMeta = maybeOutputLineNumber(buffer, ast.locationToken, lnMeta)
    end

    --
    -- lua -> __LUA"lua"
    --
    if ast.type == "lua" then
        local lua = _concatTokens(ast.tokens, ln, current_parsingAndMeta_addLineNumbers, nil, nil)
        ln = ast.tokens[#ast.tokens].line

        if not asMacroArgExpr then
            tableInsert(buffer, "__LUA")
        end

        if current_parsingAndMeta_isDebug then
            if not asMacroArgExpr then
                tableInsert(buffer, "(")
            end
            tableInsert(buffer, (F("%q", lua):gsub("\n", "n")))
            if not asMacroArgExpr then
                tableInsert(buffer, ")\n")
            end
        else
            tableInsert(buffer, F("%q", lua))
            if not asMacroArgExpr then
                tableInsert(buffer, "\n")
            end
        end

    --
    -- !(expression) -> __VAL(expression)
    --
    elseif ast.type == "expressionValue" then
        if asMacroArgExpr then
            tableInsert(buffer, "__TOLUA(")
        else
            tableInsert(buffer, "__VAL((")
        end

        for _, tok in ipairs(ast.tokens) do
            tableInsert(buffer, tok.representation)
        end

        if asMacroArgExpr then
            tableInsert(buffer, ")")
        else
            tableInsert(buffer, "))\n")
        end

    --
    -- !!(expression) -> __LUA(expression)
    --
    elseif ast.type == "expressionCode" then
        if asMacroArgExpr then
            tableInsert(buffer, "__ISLUA(")
        else
            tableInsert(buffer, "__LUA((")
        end

        for _, tok in ipairs(ast.tokens) do
            tableInsert(buffer, tok.representation)
        end

        if asMacroArgExpr then
            tableInsert(buffer, ")")
        else
            tableInsert(buffer, "))\n")
        end

    --
    -- !(statements) -> statements
    -- !statements   -> statements
    --
    elseif ast.type == "metaprogram" then
        if asMacroArgExpr then
            internalError(ast.type)
        end

        if ast.originIsLine then
            for i = 1, #ast.tokens - 1 do
                tableInsert(buffer, ast.tokens[i].representation)
            end

            local lastTok = ast.tokens[#ast.tokens]
            if lastTok.type == "whitespace" then
                if current_parsingAndMeta_isDebug then
                    tableInsert(buffer, (F("\n__LUA(%q)\n", lastTok.value):gsub("\\\n", "\\n"))) -- Note: "\\\n" does not match "\n".
                else
                    tableInsert(buffer, (F("\n__LUA%q\n", lastTok.value):gsub("\\\n", "\\n")))
                end
            else --if type == comment
                tableInsert(buffer, lastTok.representation)
                if current_parsingAndMeta_isDebug then
                    tableInsert(buffer, F('__LUA("\\n")\n'))
                else
                    tableInsert(buffer, F("__LUA'\\n'\n"))
                end
            end
        else
            for _, tok in ipairs(ast.tokens) do
                tableInsert(buffer, tok.representation)
            end
            tableInsert(buffer, "\n")
        end

    --
    -- @@callee(argument1, ...) -> __LUA(__M(callee(__ARG(1,<argument1>), ...)))
    --                       OR -> __LUA(__M(callee(__ARG(1,function()<argument1>end), ...)))
    --
    -- The code handling each argument will be different depending on the complexity of the argument.
    --
    elseif ast.type == "macro" then
        if not asMacroArgExpr then
            tableInsert(buffer, "__LUA(")
        end

        tableInsert(buffer, "__M()(")
        for _, tok in ipairs(ast.calleeTokens) do
            tableInsert(buffer, tok.representation)
        end
        tableInsert(buffer, "(")

        for argNum, macroArg in ipairs(ast.arguments) do
            local argIsComplex = false -- If any part of the argument cannot be an expression then it's complex.

            for _, astInArg in ipairs(macroArg.nodes) do
                if astInArg.type == "metaprogram" or astInArg.type == "dualCode" then
                    argIsComplex = true
                    break
                end
            end

            if argNum > 1 then
                tableInsert(buffer, ",")
                if current_parsingAndMeta_isDebug then
                    tableInsert(buffer, " ")
                end
            end

            local locTokNum = #current_meta_locationTokens + 1
            current_meta_locationTokens[locTokNum] = macroArg.nodes[1] and macroArg.nodes[1].locationToken
                or macroArg.locationToken
                or internalError()

            tableInsert(buffer, "__ARG(")
            tableInsert(buffer, tostring(locTokNum))
            tableInsert(buffer, ",")

            if argIsComplex then
                tableInsert(buffer, "function()\n")
                for nodeNumInArg, astInArg in ipairs(macroArg.nodes) do
                    ln, lnMeta = astNodeToMetaprogram(buffer, astInArg, ln, lnMeta, false)
                end
                tableInsert(buffer, "end")
            elseif macroArg.nodes[1] then
                for nodeNumInArg, astInArg in ipairs(macroArg.nodes) do
                    if nodeNumInArg > 1 then
                        tableInsert(buffer, "..")
                    end
                    ln, lnMeta = astNodeToMetaprogram(buffer, astInArg, ln, lnMeta, true)
                end
            else
                tableInsert(buffer, '""')
            end

            tableInsert(buffer, ")")
        end

        tableInsert(buffer, "))")

        if not asMacroArgExpr then
            tableInsert(buffer, ")\n")
        end

    --
    -- !!local names = values -> local names = values ; __LUA"local names = "__VAL(name1)__LUA", "__VAL(name2)...
    -- !!      names = values ->       names = values ;       __LUA"names = "__VAL(name1)__LUA", "__VAL(name2)...
    --
    elseif ast.type == "dualCode" then
        if asMacroArgExpr then
            internalError(ast.type)
        end

        -- Metaprogram.
        if ast.isDeclaration then
            tableInsert(buffer, "local ")
        end
        tableInsert(buffer, table.concat(ast.names, ", "))
        tableInsert(buffer, " = ")
        for _, tok in ipairs(ast.valueTokens) do
            tableInsert(buffer, tok.representation)
        end

        -- Final program.
        tableInsert(buffer, "__LUA")
        if current_parsingAndMeta_isDebug then
            tableInsert(buffer, "(")
        end
        tableInsert(buffer, '"') -- string start
        if current_parsingAndMeta_addLineNumbers then
            ln = maybeOutputLineNumber(buffer, ast.locationToken, ln)
        end
        if ast.isDeclaration then
            tableInsert(buffer, "local ")
        end
        tableInsert(buffer, table.concat(ast.names, ", "))
        tableInsert(buffer, ' = "') -- string end
        if current_parsingAndMeta_isDebug then
            tableInsert(buffer, "); ")
        end

        for i, name in ipairs(ast.names) do
            if i == 1 then -- void
            elseif current_parsingAndMeta_isDebug then
                tableInsert(buffer, '; __LUA(", "); ')
            else
                tableInsert(buffer, '__LUA", "')
            end
            tableInsert(buffer, "__VAL(")
            tableInsert(buffer, name)
            tableInsert(buffer, ")")
        end

        -- Use trailing semicolon if the user does.
        for i = #ast.valueTokens, 1, -1 do
            if isToken(ast.valueTokens[i], "punctuation", ";") then
                if current_parsingAndMeta_isDebug then
                    tableInsert(buffer, '; __LUA(";")')
                else
                    tableInsert(buffer, '__LUA";"')
                end
                break
            elseif not isToken(ast.valueTokens[i], "whitespace") then
                break
            end
        end

        if current_parsingAndMeta_isDebug then
            tableInsert(buffer, '; __LUA("\\n")\n')
        else
            tableInsert(buffer, '__LUA"\\n"\n')
        end

    --
    -- ...
    --
    elseif ast.type == "sequence" then
        for _, astChild in ipairs(ast.nodes) do
            ln, lnMeta = astNodeToMetaprogram(buffer, astChild, ln, lnMeta, false)
        end

    -- elseif ast.type == "symbol" then
    -- 	errorAtToken(ast.locationToken, nil, nil, "AstSymbol")
    else
        printErrorTraceback("Internal error.")
        errorAtToken(ast.locationToken, nil, "Parsing", "Internal error. (%s, %s)", ast.type, tostring(asMacroArgExpr))
    end

    return ln, lnMeta
end

local function astToLua(ast)
    local buffer = {}
    astNodeToMetaprogram(buffer, ast, 0, 0, false)
    return table.concat(buffer)
end

local function _processFileOrString(params, isFile)
    if isFile then
        if not params.pathIn then
            error("Missing 'pathIn' in params.", 2)
        end
        if not params.pathOut then
            error("Missing 'pathOut' in params.", 2)
        end

        if params.pathOut == params.pathIn and params.pathOut ~= "-" then
            error("'pathIn' and 'pathOut' are the same in params.", 2)
        end

        if (params.pathMeta or "-") == "-" then -- Should it be possible to output the metaprogram to stdout?
            -- void
        elseif params.pathMeta == params.pathIn then
            error("'pathIn' and 'pathMeta' are the same in params.", 2)
        elseif params.pathMeta == params.pathOut then
            error("'pathOut' and 'pathMeta' are the same in params.", 2)
        end
    else
        if not params.code then
            error("Missing 'code' in params.", 2)
        end
    end

    -- Read input.
    local luaUnprocessed, virtualPathIn

    if isFile then
        virtualPathIn = params.pathIn
        local err

        if virtualPathIn == "-" then
            luaUnprocessed, err = io.stdin:read("*a")
        else
            luaUnprocessed, err = readFile(virtualPathIn, true)
        end

        if not luaUnprocessed then
            errorf("Could not read file '%s'. (%s)", virtualPathIn, err)
        end

        current_anytime_pathIn = params.pathIn
        current_anytime_pathOut = params.pathOut
    else
        virtualPathIn = "<code>"
        luaUnprocessed = params.code
    end

    current_anytime_fastStrings = params.fastStrings
    current_parsing_insertCount = 0
    current_parsingAndMeta_resourceCache = { [virtualPathIn] = luaUnprocessed } -- The contents of files, unless params.onInsert() is specified in which case it's user defined.
    current_parsingAndMeta_onInsert = params.onInsert
    current_parsingAndMeta_addLineNumbers = params.addLineNumbers
    current_parsingAndMeta_macroPrefix = params.macroPrefix or ""
    current_parsingAndMeta_macroSuffix = params.macroSuffix or ""
    current_parsingAndMeta_strictMacroArguments = params.strictMacroArguments ~= false
    current_meta_locationTokens = {}

    local specialFirstLine, rest = luaUnprocessed:match("^(#[^\r\n]*\r?\n?)(.*)$")
    if specialFirstLine then
        specialFirstLine = specialFirstLine:gsub("\r", "") -- Normalize line breaks. (Assume the input is either "\n" or "\r\n".)
        luaUnprocessed = rest
    end

    -- Ensure there's a newline at the end of the code, otherwise there will be problems down the line.
    if not (luaUnprocessed == "" or luaUnprocessed:find("\n%s*$")) then
        luaUnprocessed = luaUnprocessed .. "\n"
    end

    local tokens = _tokenize(luaUnprocessed, virtualPathIn, true, params.backtickStrings, params.jitSyntax)
    -- printTokens(tokens) -- DEBUG

    -- Gather info.
    local lastTok = tokens[#tokens]

    local stats = {
        processedByteCount = #luaUnprocessed,
        lineCount = (specialFirstLine and 1 or 0)
            + (lastTok and lastTok.line + countString(lastTok.representation, "\n", true) or 0),
        lineCountCode = getLineCountWithCode(tokens),
        tokenCount = 0, -- Set later.
        hasPreprocessorCode = false,
        hasMetaprogram = false,
        insertedNames = {},
    }

    for _, tok in ipairs(tokens) do
        -- @Volatile: Make sure to update this when syntax is changed!
        if isToken(tok, "pp_entry") or isToken(tok, "pp_keyword", "insert") or isToken(tok, "pp_symbol") then
            stats.hasPreprocessorCode = true
            stats.hasMetaprogram = true
            break
        elseif isToken(tok, "pp_keyword") or (isToken(tok, "string") and tok.representation:find("^`")) then
            stats.hasPreprocessorCode = true
            -- Keep going as there may be metaprogram.
        end
    end

    -- Generate and run metaprogram.
    ----------------------------------------------------------------

    local shouldProcess = stats.hasPreprocessorCode or params.addLineNumbers

    if shouldProcess then
        tokens = doExpansions(params, tokens, stats)
    end
    stats.tokenCount = #tokens

    current_meta_maxLogLevel = params.logLevel or "trace"
    if not LOG_LEVELS[current_meta_maxLogLevel] then
        errorf(2, "Invalid 'logLevel' value in params. (%s)", tostring(current_meta_maxLogLevel))
    end

    local lua

    if shouldProcess then
        local luaMeta = astToLua(astParse(params, tokens))
        --[[ DEBUG :PrintCode
		print("=META===============================")
		print(luaMeta)
		print("====================================")
		--]]

        -- Run metaprogram.
        current_meta_pathForErrorMessages = params.pathMeta or "<meta>"
        current_meta_output = {}
        current_meta_outputStack = { current_meta_output }
        current_meta_canOutputNil = params.canOutputNil ~= false
        current_meta_releaseMode = params.release

        if params.pathMeta then
            local file, err = io.open(params.pathMeta, "wb")
            if not file then
                errorf("Count not open '%s' for writing. (%s)", params.pathMeta, err)
            end

            file:write(luaMeta)
            file:close()
        end

        if params.onBeforeMeta then
            params.onBeforeMeta(luaMeta)
        end

        local main_chunk, err = loadLuaString(luaMeta, "@" .. current_meta_pathForErrorMessages, metaEnv)
        if not main_chunk then
            local ln, _err = err:match("^.-:(%d+): (.*)")
            errorOnLine(current_meta_pathForErrorMessages, (tonumber(ln) or 0), nil, "%s", (_err or err))
        end

        current_anytime_isRunningMeta = true
        main_chunk() -- Note: Our caller should clean up current_meta_pathForErrorMessages etc. on error.
        current_anytime_isRunningMeta = false

        if not current_parsingAndMeta_isDebug and params.pathMeta then
            os.remove(params.pathMeta)
        end

        if current_meta_outputStack[2] then
            error("Called startInterceptingOutput() more times than stopInterceptingOutput().")
        end

        lua = table.concat(current_meta_output)
        --[[ DEBUG :PrintCode
		print("=OUTPUT=============================")
		print(lua)
		print("====================================")
		--]]

        current_meta_pathForErrorMessages = ""
        current_meta_output = nil
        current_meta_outputStack = nil
        current_meta_canOutputNil = true
        current_meta_releaseMode = false
    else
        -- @Copypaste from above.
        if not current_parsingAndMeta_isDebug and params.pathMeta then
            os.remove(params.pathMeta)
        end

        lua = luaUnprocessed
    end

    current_meta_maxLogLevel = "trace"
    current_meta_locationTokens = nil

    if params.onAfterMeta then
        local luaModified = params.onAfterMeta(lua)

        if type(luaModified) == "string" then
            lua = luaModified
        elseif luaModified ~= nil then
            errorf("onAfterMeta() did not return a string. (Got %s)", type(luaModified))
        end
    end

    -- Write output file.
    ----------------------------------------------------------------

    local pathOut = isFile and params.pathOut or "<output>"

    if isFile then
        if pathOut == "-" then
            io.stdout:write(specialFirstLine or "")
            io.stdout:write(lua)
        else
            local file, err = io.open(pathOut, "wb")
            if not file then
                errorf("Count not open '%s' for writing. (%s)", pathOut, err)
            end

            file:write(specialFirstLine or "")
            file:write(lua)
            file:close()
        end
    end

    -- Check if the output is valid Lua.
    if params.validate ~= false then
        local luaToCheck = lua:gsub("^#![^\n]*", "")
        local chunk, err = loadLuaString(luaToCheck, "@" .. pathOut, nil)

        if not chunk then
            local ln, _err = err:match("^.-:(%d+): (.*)")
            errorOnLine(pathOut, (tonumber(ln) or 0), nil, "Output is invalid Lua. (%s)", (_err or err))
        end
    end

    -- :ProcessInfo
    local info = {
        path = isFile and params.pathIn or "",
        outputPath = isFile and params.pathOut or "",
        processedByteCount = stats.processedByteCount,
        lineCount = stats.lineCount,
        linesOfCode = stats.lineCountCode,
        tokenCount = stats.tokenCount,
        hasPreprocessorCode = stats.hasPreprocessorCode,
        hasMetaprogram = stats.hasMetaprogram,
        insertedFiles = stats.insertedNames,
    }

    if params.onDone then
        params.onDone(info)
    end

    current_anytime_pathIn = ""
    current_anytime_pathOut = ""
    current_anytime_fastStrings = false
    current_parsingAndMeta_resourceCache = nil
    current_parsingAndMeta_onInsert = nil
    current_parsingAndMeta_addLineNumbers = false
    current_parsingAndMeta_macroPrefix = ""
    current_parsingAndMeta_macroSuffix = ""
    current_parsingAndMeta_strictMacroArguments = true

    ----------------------------------------------------------------

    if isFile then
        return info
    else
        if specialFirstLine then
            lua = specialFirstLine .. lua
        end
        return lua, info
    end
end

local function processFileOrString(params, isFile)
    if current_parsingAndMeta_isProcessing then
        error("Cannot process recursively.", 3) -- Note: We don't return failure in this case - it's a critical error!
    end

    -- local startTime = os.clock() -- :DebugMeasureTime  @Incomplete: Add processing time to returned info.
    local returnValues = nil

    current_parsingAndMeta_isProcessing = true
    current_parsingAndMeta_isDebug = params.debug

    local xpcallOk, xpcallErr = xpcall(function()
        returnValues = pack(_processFileOrString(params, isFile))
    end, function(err)
        if type(err) == "string" and err:find("\0", 1, true) then
            printError(tryToFormatError(cleanError(err)))
        else
            printErrorTraceback(err, 2) -- The level should be at error().
        end

        if params.onError then
            local cbOk, cbErr = pcall(params.onError, err)
            if not cbOk then
                printfError("Additional error in params.onError()...\n%s", tryToFormatError(cbErr))
            end
        end

        return err
    end)

    current_parsingAndMeta_isProcessing = false
    current_parsingAndMeta_isDebug = false

    -- Cleanup in case an error happened.
    current_anytime_isRunningMeta = false
    current_anytime_pathIn = ""
    current_anytime_pathOut = ""
    current_anytime_fastStrings = false
    current_parsing_insertCount = 0
    current_parsingAndMeta_onInsert = nil
    current_parsingAndMeta_resourceCache = nil
    current_parsingAndMeta_addLineNumbers = false
    current_parsingAndMeta_macroPrefix = ""
    current_parsingAndMeta_macroSuffix = ""
    current_parsingAndMeta_strictMacroArguments = true
    current_meta_pathForErrorMessages = ""
    current_meta_output = nil
    current_meta_outputStack = nil
    current_meta_canOutputNil = true
    current_meta_releaseMode = false
    current_meta_maxLogLevel = "trace"
    current_meta_locationTokens = nil

    -- print("time", os.clock()-startTime) -- :DebugMeasureTime
    if xpcallOk then
        return unpack(returnValues, 1, returnValues.n)
    else
        return nil, cleanError(xpcallErr or "Unknown processing error.")
    end
end

local function processFile(params)
    local returnValues = pack(processFileOrString(params, true))
    return unpack(returnValues, 1, returnValues.n)
end

local function processString(params)
    local returnValues = pack(processFileOrString(params, false))
    return unpack(returnValues, 1, returnValues.n)
end

-- :ExportTable
local pp = {

    -- Processing functions.
    ----------------------------------------------------------------

    -- processFile()
    -- Process a Lua file. Returns nil and a message on error.
    --
    -- info = processFile( params )
    -- info: Table with various information. (See 'ProcessInfo' for more info.)
    --
    -- params: Table with these fields:
    --   pathIn               = pathToInputFile       -- [Required] Specify "-" to use stdin.
    --   pathOut              = pathToOutputFile      -- [Required] Specify "-" to use stdout. (Note that if stdout is used then anything you print() in the metaprogram will end up there.)
    --   pathMeta             = pathForMetaprogram    -- [Optional] You can inspect this temporary output file if an error occurs in the metaprogram.
    --
    --   debug                = boolean               -- [Optional] Debug mode. The metaprogram file is formatted more nicely and does not get deleted automatically.
    --   addLineNumbers       = boolean               -- [Optional] Add comments with line numbers to the output.
    --
    --   backtickStrings      = boolean               -- [Optional] Enable the backtick (`) to be used as string literal delimiters. Backtick strings don't interpret any escape sequences and can't contain other backticks. (Default: false)
    --   jitSyntax            = boolean               -- [Optional] Allow LuaJIT-specific syntax. (Default: false)
    --   canOutputNil         = boolean               -- [Optional] Allow !(expression) and outputValue() to output nil. (Default: true)
    --   fastStrings          = boolean               -- [Optional] Force fast serialization of string values. (Non-ASCII characters will look ugly.) (Default: false)
    --   validate             = boolean               -- [Optional] Validate output. (Default: true)
    --   strictMacroArguments = boolean               -- [Optional] Check that macro arguments are valid Lua expressions. (Default: true)
    --
    --   macroPrefix          = prefix                -- [Optional] String to prepend to macro names. (Default: "")
    --   macroSuffix          = suffix                -- [Optional] String to append  to macro names. (Default: "")
    --
    --   release              = boolean               -- [Optional] Enable release mode. Currently only disables the @@ASSERT() macro when true. (Default: false)
    --   logLevel             = levelName             -- [Optional] Maximum log level for the @@LOG() macro. Can be "off", "error", "warning", "info", "debug" or "trace". (Default: "trace", which enables all logging)
    --
    --   onInsert             = function( name )      -- [Optional] Called for each @insert"name" instruction. It's expected to return a Lua code string. By default 'name' is a path to a file to be inserted.
    --   onBeforeMeta         = function( luaString ) -- [Optional] Called before the metaprogram runs, if a metaprogram is generated. luaString contains the metaprogram.
    --   onAfterMeta          = function( luaString ) -- [Optional] Here you can modify and return the Lua code before it's written to 'pathOut'.
    --   onError              = function( error )     -- [Optional] You can use this to get traceback information. 'error' is the same value as what is returned from processFile().
    --
    processFile = processFile,

    -- processString()
    -- Process Lua code. Returns nil and a message on error.
    --
    -- luaString, info = processString( params )
    -- info: Table with various information. (See 'ProcessInfo' for more info.)
    --
    -- params: Table with these fields:
    --   code                 = luaString             -- [Required]
    --   pathMeta             = pathForMetaprogram    -- [Optional] You can inspect this temporary output file if an error occurs in the metaprogram.
    --
    --   debug                = boolean               -- [Optional] Debug mode. The metaprogram file is formatted more nicely and does not get deleted automatically.
    --   addLineNumbers       = boolean               -- [Optional] Add comments with line numbers to the output.
    --
    --   backtickStrings      = boolean               -- [Optional] Enable the backtick (`) to be used as string literal delimiters. Backtick strings don't interpret any escape sequences and can't contain other backticks. (Default: false)
    --   jitSyntax            = boolean               -- [Optional] Allow LuaJIT-specific syntax. (Default: false)
    --   canOutputNil         = boolean               -- [Optional] Allow !(expression) and outputValue() to output nil. (Default: true)
    --   fastStrings          = boolean               -- [Optional] Force fast serialization of string values. (Non-ASCII characters will look ugly.) (Default: false)
    --   validate             = boolean               -- [Optional] Validate output. (Default: true)
    --   strictMacroArguments = boolean               -- [Optional] Check that macro arguments are valid Lua expressions. (Default: true)
    --
    --   macroPrefix          = prefix                -- [Optional] String to prepend to macro names. (Default: "")
    --   macroSuffix          = suffix                -- [Optional] String to append  to macro names. (Default: "")
    --
    --   release              = boolean               -- [Optional] Enable release mode. Currently only disables the @@ASSERT() macro when true. (Default: false)
    --   logLevel             = levelName             -- [Optional] Maximum log level for the @@LOG() macro. Can be "off", "error", "warning", "info", "debug" or "trace". (Default: "trace", which enables all logging)
    --
    --   onInsert             = function( name )      -- [Optional] Called for each @insert"name" instruction. It's expected to return a Lua code string. By default 'name' is a path to a file to be inserted.
    --   onBeforeMeta         = function( luaString ) -- [Optional] Called before the metaprogram runs, if a metaprogram is generated. luaString contains the metaprogram.
    --   onError              = function( error )     -- [Optional] You can use this to get traceback information. 'error' is the same value as the second returned value from processString().
    --
    processString = processString,

    -- Values.
    ----------------------------------------------------------------

    VERSION = PP_VERSION, -- The version of LuaPreprocess.
    metaEnvironment = metaEnv, -- The environment used for metaprograms.
}

-- Include all functions from the metaprogram environment.
for k, v in pairs(metaFuncs) do
    pp[k] = v
end

return pp

--[[!===========================================================

Copyright © 2018-2022 Marcus 'ReFreezed' Thunström

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

==============================================================]]