--[[============================================================
--=
--=  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.

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

