diff --git a/.luacheckrc b/.luacheckrc index 305d3de..e553e7f 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,4 +1,4 @@ std = "lua54+love" stds.project = { globals = {"tiny"}, -} \ No newline at end of file +} diff --git a/assets/images/Ball.png b/assets/images/Ball.png new file mode 100644 index 0000000..41a4c1f Binary files /dev/null and b/assets/images/Ball.png differ diff --git a/assets/images/Flag.png b/assets/images/Flag.png new file mode 100644 index 0000000..ca8d6f4 Binary files /dev/null and b/assets/images/Flag.png differ diff --git a/assets/images/GolferRight.png b/assets/images/GolferRight.png new file mode 100644 index 0000000..3950f6f Binary files /dev/null and b/assets/images/GolferRight.png differ diff --git a/assets/images/GolferRightActivated.png b/assets/images/GolferRightActivated.png new file mode 100644 index 0000000..2bdcce7 Binary files /dev/null and b/assets/images/GolferRightActivated.png differ diff --git a/assets/images/Grass.png b/assets/images/Grass.png new file mode 100644 index 0000000..3c8ad59 Binary files /dev/null and b/assets/images/Grass.png differ diff --git a/assets/images/SandTrap.png b/assets/images/SandTrap.png new file mode 100644 index 0000000..7392398 Binary files /dev/null and b/assets/images/SandTrap.png differ diff --git a/assets/images/SmallSandTrap.png b/assets/images/SmallSandTrap.png new file mode 100644 index 0000000..4ea92bb Binary files /dev/null and b/assets/images/SmallSandTrap.png differ diff --git a/assets/images/SmallSandTrapActivated.png b/assets/images/SmallSandTrapActivated.png new file mode 100644 index 0000000..ac663b0 Binary files /dev/null and b/assets/images/SmallSandTrapActivated.png differ diff --git a/assets/images/StartButton.png b/assets/images/StartButton.png new file mode 100644 index 0000000..f6e12a1 Binary files /dev/null and b/assets/images/StartButton.png differ diff --git a/generated/all-systems.lua b/generated/all-systems.lua index 02b06cd..2e2451f 100644 --- a/generated/all-systems.lua +++ b/generated/all-systems.lua @@ -5,4 +5,5 @@ require("../systems/decay") require("../systems/draw") require("../systems/gravity") require("../systems/input") +require("../systems/rounds") require("../systems/velocity") diff --git a/generated/assets.lua b/generated/assets.lua index 7d629f6..deb0942 100644 --- a/generated/assets.lua +++ b/generated/assets.lua @@ -1,11 +1,47 @@ -- GENERATED FILE - DO NOT EDIT -- Instead, edit the source file directly: assets.lua2p. +-- luacheck: ignore +---@type love.Texture +Ball = love.graphics.newImage("assets/images/Ball.png") + +-- luacheck: ignore +---@type love.Texture +Flag = love.graphics.newImage("assets/images/Flag.png") + +-- luacheck: ignore +---@type love.Texture +GolferRightActivated = love.graphics.newImage("assets/images/GolferRightActivated.png") + +-- luacheck: ignore +---@type love.Texture +GolferRight = love.graphics.newImage("assets/images/GolferRight.png") + +-- luacheck: ignore +---@type love.Texture +Grass = love.graphics.newImage("assets/images/Grass.png") + +-- luacheck: ignore +---@type love.Texture +SandTrap = love.graphics.newImage("assets/images/SandTrap.png") + +-- luacheck: ignore +---@type love.Texture +SmallSandTrapActivated = love.graphics.newImage("assets/images/SmallSandTrapActivated.png") + +-- luacheck: ignore +---@type love.Texture +SmallSandTrap = love.graphics.newImage("assets/images/SmallSandTrap.png") + +-- luacheck: ignore +---@type love.Texture +StartButton = love.graphics.newImage("assets/images/StartButton.png") + -- luacheck: ignore ----@type FontData +---@type fun(fontSize: number | nil): love.Font EtBt7001Z0xa = function(fontSize) return love.graphics.newFont("assets/fonts/EtBt7001Z0xa.ttf", fontSize) end diff --git a/generated/assets.lua2p b/generated/assets.lua2p index 7b4672a..814adc1 100644 --- a/generated/assets.lua2p +++ b/generated/assets.lua2p @@ -25,9 +25,11 @@ function generatedFileWarning() return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p." end)!!(generatedFileWarning()) -!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'Image')) -!!(dirLookup('assets/sounds', 'wav', 'love.sound.newSoundData', 'SoundData')) -!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'SoundData')) -!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'FontData', function(varName, newFunc, file) +!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'love.Texture')) +!!(dirLookup('assets/sounds', 'ogg', 'love.audio.newSource', 'love.Source', function(varName, newFunc, file) + return varName .. ' = ' .. newFunc .. '("' .. file .. '", "static")' +end)) +!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'love.SoundData')) +!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'fun(fontSize: number | nil): love.Font', function(varName, newFunc, file) return varName .. ' = function(fontSize)\n return ' .. newFunc .. '("' .. file .. '", fontSize)\nend' end)) diff --git a/lib/luaunit.lua b/lib/luaunit.lua index e2c0bd7..fa8ed9b 100644 --- a/lib/luaunit.lua +++ b/lib/luaunit.lua @@ -6,28 +6,30 @@ Homepage: https://github.com/bluebird75/luaunit Development by Philippe Fremy Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) License: BSD License, see LICENSE.txt -]]-- +]] +-- require("math") -local M={} +local M = {} -- private exported functions (for testing) M.private = {} -M.VERSION='3.4' -M._VERSION=M.VERSION -- For LuaUnit v2 compatibility +M.VERSION = "3.4" +M._VERSION = M.VERSION -- For LuaUnit v2 compatibility -- a version which distinguish between regular Lua and LuaJit M._LUAVERSION = (jit and jit.version) or _VERSION --[[ Some people like assertEquals( actual, expected ) and some people prefer assertEquals( expected, actual ). -]]-- +]] +-- M.ORDER_ACTUAL_EXPECTED = true M.PRINT_TABLE_REF_IN_ERROR_MSG = false M.LINE_LENGTH = 80 -M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items -M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items -- this setting allow to remove entries from the stack-trace, for -- example to hide a call to a framework which would be calling luaunit @@ -45,21 +47,21 @@ is not acceptable, it can be changed by the user to better suit specific needs. See also: https://en.wikipedia.org/wiki/Machine_epsilon ]] -M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +M.EPS = 2 ^ -52 -- = machine epsilon for "double", ~2.22E-16 if math.abs(1.1 - 1 - 0.1) > M.EPS then -- rounding error is above EPS, assume single precision - M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 + M.EPS = 2 ^ -23 -- = machine epsilon for "float", ~1.19E-07 end -- set this to false to debug luaunit local STRIP_LUAUNIT_FROM_STACKTRACE = true M.VERBOSITY_DEFAULT = 10 -M.VERBOSITY_LOW = 1 -M.VERBOSITY_QUIET = 0 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 M.VERBOSITY_VERBOSE = 20 M.DEFAULT_DEEP_ANALYSIS = nil -M.FORCE_DEEP_ANALYSIS = true +M.FORCE_DEEP_ANALYSIS = true M.DISABLE_DEEP_ANALYSIS = false -- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values @@ -68,13 +70,11 @@ M.DISABLE_DEEP_ANALYSIS = false -- we need to keep a copy of the script args before it is overriden local cmdline_argv = rawget(_G, "arg") -M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests -M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early -M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests +M.FAILURE_PREFIX = "LuaUnit test FAILURE: " -- prefix string for failed tests +M.SUCCESS_PREFIX = "LuaUnit test SUCCESS: " -- prefix string for successful tests finished early +M.SKIP_PREFIX = "LuaUnit test SKIP: " -- prefix string for skipped tests - - -M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +M.USAGE = [[Usage: lua [options] [testname1 [testname2] ... ] Options: -h, --help: Print this help --version: Print version information @@ -135,7 +135,7 @@ end local function pcall_or_abort(func, ...) -- unpack is a global function for Lua 5.1, otherwise use table.unpack local unpack = rawget(_G, "unpack") or table.unpack - local result = {pcall(func, ...)} + local result = { pcall(func, ...) } if not result[1] then -- an error occurred print(result[2]) -- error message @@ -147,12 +147,22 @@ local function pcall_or_abort(func, ...) end local crossTypeOrdering = { - number = 1, boolean = 2, string = 3, table = 4, other = 5 + number = 1, + boolean = 2, + string = 3, + table = 4, + other = 5, } local crossTypeComparison = { - number = function(a, b) return a < b end, - string = function(a, b) return a < b end, - other = function(a, b) return tostring(a) < tostring(b) end, + number = function(a, b) + return a < b + end, + string = function(a, b) + return a < b + end, + other = function(a, b) + return tostring(a) < tostring(b) + end, } local function crossTypeSort(a, b) @@ -166,11 +176,11 @@ local function crossTypeSort(a, b) return type_a < type_b end -local function __genSortedIndex( t ) +local function __genSortedIndex(t) -- Returns a sequence consisting of t's keys, sorted. local sortedIndex = {} - for key,_ in pairs(t) do + for key, _ in pairs(t) do table.insert(sortedIndex, key) end @@ -235,14 +245,14 @@ local function sortedPairs(tbl) -- sorted order. As required by "generic for" loops, this will return the -- iterator (function), an "invariant state", and the initial control value. -- (see http://www.lua.org/pil/7.2.html) - return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil + return sortedNext, { t = tbl, sortedIdx = __genSortedIndex(tbl) }, nil end M.private.sortedPairs = sortedPairs -- seed the random with a strongly varying seed -math.randomseed(math.floor(os.clock()*1E11)) +math.randomseed(math.floor(os.clock() * 1E11)) -local function randomizeTable( t ) +local function randomizeTable(t) -- randomize the item orders of the table t for i = #t, 2, -1 do local j = math.random(i) @@ -254,9 +264,9 @@ end M.private.randomizeTable = randomizeTable local function strsplit(delimiter, text) --- Split text into a list consisting of the strings in text, separated --- by strings matching delimiter (which may _NOT_ be a pattern). --- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + -- Split text into a list consisting of the strings in text, separated + -- by strings matching delimiter (which may _NOT_ be a pattern). + -- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") if delimiter == "" or delimiter == nil then -- this would result in endless loops error("delimiter is nil or empty string!") end @@ -279,19 +289,19 @@ local function strsplit(delimiter, text) end M.private.strsplit = strsplit -local function hasNewLine( s ) +local function hasNewLine(s) -- return true if s has a newline - return (string.find(s, '\n', 1, true) ~= nil) + return (string.find(s, "\n", 1, true) ~= nil) end M.private.hasNewLine = hasNewLine -local function prefixString( prefix, s ) +local function prefixString(prefix, s) -- Prefix all the lines of s with prefix - return prefix .. string.gsub(s, '\n', '\n' .. prefix) + return prefix .. string.gsub(s, "\n", "\n" .. prefix) end M.private.prefixString = prefixString -local function strMatch(s, pattern, start, final ) +local function strMatch(s, pattern, start, final) -- return true if s matches completely the pattern from index start to index end -- return false in every other cases -- if start is nil, matches from the beginning of the string @@ -316,7 +326,7 @@ local function patternFilter(patterns, expr) if patterns ~= nil then for _, pattern in ipairs(patterns) do - local exclude = pattern:sub(1,1) == '!' + local exclude = pattern:sub(1, 1) == "!" if exclude then pattern = pattern:sub(2) else @@ -341,7 +351,7 @@ local function patternFilter(patterns, expr) end M.private.patternFilter = patternFilter -local function xmlEscape( s ) +local function xmlEscape(s) -- Return s escaped for XML attributes -- escapes table: -- " " @@ -350,30 +360,29 @@ local function xmlEscape( s ) -- > > -- & & - return string.gsub( s, '.', { - ['&'] = "&", + return string.gsub(s, ".", { + ["&"] = "&", ['"'] = """, ["'"] = "'", - ['<'] = "<", - ['>'] = ">", - } ) + ["<"] = "<", + [">"] = ">", + }) end M.private.xmlEscape = xmlEscape -local function xmlCDataEscape( s ) +local function xmlCDataEscape(s) -- Return s escaped for CData section, escapes: "]]>" - return string.gsub( s, ']]>', ']]>' ) + return string.gsub(s, "]]>", "]]>") end M.private.xmlCDataEscape = xmlCDataEscape - -local function lstrip( s ) +local function lstrip(s) --[[Return s with all leading white spaces and tabs removed]] local idx = 0 while idx < s:len() do idx = idx + 1 - local c = s:sub(idx,idx) - if c ~= ' ' and c ~= '\t' then + local c = s:sub(idx, idx) + if c ~= " " and c ~= "\t" then break end end @@ -381,29 +390,28 @@ local function lstrip( s ) end M.private.lstrip = lstrip -local function extractFileLineInfo( s ) +local function extractFileLineInfo(s) --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" Return the "file.lua:linenb" information ]] local s2 = lstrip(s) - local firstColon = s2:find(':', 1, true) + local firstColon = s2:find(":", 1, true) if firstColon == nil then -- string is not in the format file:line: return s end - local secondColon = s2:find(':', firstColon+1, true) + local secondColon = s2:find(":", firstColon + 1, true) if secondColon == nil then -- string is not in the format file:line: return s end - return s2:sub(1, secondColon-1) + return s2:sub(1, secondColon - 1) end M.private.extractFileLineInfo = extractFileLineInfo - -local function stripLuaunitTrace2( stackTrace, errMsg ) +local function stripLuaunitTrace2(stackTrace, errMsg) --[[ -- Example of a traceback: <>' ) - local t = strsplit( '\n', stackTrace ) + local t = strsplit("\n", stackTrace) -- print( prettystr(t) ) local idx = 2 @@ -505,15 +513,13 @@ local function stripLuaunitTrace2( stackTrace, errMsg ) end -- print( prettystr(t) ) - return table.concat( t, '\n') - + return table.concat(t, "\n") end M.private.stripLuaunitTrace2 = stripLuaunitTrace2 - -local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable) local type_v = type(v) - if "string" == type_v then + if "string" == type_v then -- use clever delimiters according to content: -- enclose with single quotes if string contains ", but no ' if v:find('"', 1, true) and not v:find("'", 1, true) then @@ -521,13 +527,11 @@ local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) end -- use double quotes otherwise, escape embedded " return '"' .. v:gsub('"', '\\"') .. '"' - elseif "table" == type_v then --if v.__class__ then -- return string.gsub( tostring(v), 'table', v.__class__ ) --end return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) - elseif "number" == type_v then -- eliminate differences in formatting between various Lua versions if v ~= v then @@ -550,12 +554,13 @@ local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) return tostring(v) end -local function prettystr( v ) +local function prettystr(v) --[[ Pretty string conversion, to display the full content of a variable of any type. * string are enclosed with " by default, or with ' if string contains a " * tables are expanded to show their full content, with indentation in case of nested tables - ]]-- + ]] + -- local cycleDetectTable = {} local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then @@ -569,7 +574,7 @@ local function prettystr( v ) end M.prettystr = prettystr -function M.adjust_err_msg_with_iter( err_msg, iter_msg ) +function M.adjust_err_msg_with_iter(err_msg, iter_msg) --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, add the iteration message if any and return the result. @@ -583,48 +588,55 @@ function M.adjust_err_msg_with_iter( err_msg, iter_msg ) contained in the error message. ]] if iter_msg then - iter_msg = iter_msg..', ' + iter_msg = iter_msg .. ", " else - iter_msg = '' + iter_msg = "" end - local RE_FILE_LINE = '.*:%d+: ' + local RE_FILE_LINE = ".*:%d+: " -- error message is not necessarily a string, -- so convert the value to string with prettystr() - if type( err_msg ) ~= 'string' then - err_msg = prettystr( err_msg ) + if type(err_msg) ~= "string" then + err_msg = prettystr(err_msg) end - if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + if + (err_msg:find(M.SUCCESS_PREFIX) == 1) or err_msg:match("(" .. RE_FILE_LINE .. ")" .. M.SUCCESS_PREFIX .. ".*") + then -- test finished early with success() return nil, M.NodeStatus.SUCCESS end - if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + if + (err_msg:find(M.SKIP_PREFIX) == 1) or ( + err_msg:match("(" .. RE_FILE_LINE .. ")" .. M.SKIP_PREFIX .. ".*") ~= nil + ) + then -- substitute prefix by iteration message - err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + err_msg = err_msg:gsub(".*" .. M.SKIP_PREFIX, iter_msg, 1) -- print("failure detected") return err_msg, M.NodeStatus.SKIP end - if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + if + (err_msg:find(M.FAILURE_PREFIX) == 1) + or (err_msg:match("(" .. RE_FILE_LINE .. ")" .. M.FAILURE_PREFIX .. ".*") ~= nil) + then -- substitute prefix by iteration message err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) -- print("failure detected") return err_msg, M.NodeStatus.FAIL end - - -- print("error detected") -- regular error, not a failure if iter_msg then local match -- "./test\\test_luaunit.lua:2241: some error msg - match = err_msg:match( '(.*:%d+: ).*' ) + match = err_msg:match("(.*:%d+: ).*") if match then - err_msg = err_msg:gsub( match, match .. iter_msg ) + err_msg = err_msg:gsub(match, match .. iter_msg) else -- no file:line: infromation, just add the iteration info at the beginning of the line err_msg = iter_msg .. err_msg @@ -633,7 +645,7 @@ function M.adjust_err_msg_with_iter( err_msg, iter_msg ) return err_msg, M.NodeStatus.ERROR end -local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) +local function tryMismatchFormatting(table_a, table_b, doDeepAnalysis, margin) --[[ Prepares a nice error message when comparing tables, performing a deeper analysis. @@ -653,7 +665,7 @@ local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) ]] -- check if table_a & table_b are suitable for deep analysis - if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + if type(table_a) ~= "table" or type(table_b) ~= "table" then return false end @@ -664,7 +676,7 @@ local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) local len_a, len_b, isPureList = #table_a, #table_b, true for k1, v1 in pairs(table_a) do - if type(k1) ~= 'number' or k1 > len_a then + if type(k1) ~= "number" or k1 > len_a then -- this table a mapping isPureList = false break @@ -673,7 +685,7 @@ local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) if isPureList then for k2, v2 in pairs(table_b) do - if type(k2) ~= 'number' or k2 > len_b then + if type(k2) ~= "number" or k2 > len_b then -- this table a mapping isPureList = false break @@ -688,7 +700,7 @@ local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) end if isPureList then - return M.private.mismatchFormattingPureList( table_a, table_b, margin ) + return M.private.mismatchFormattingPureList(table_a, table_b, margin) else -- only work on mapping for the moment -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) @@ -699,16 +711,16 @@ M.private.tryMismatchFormatting = tryMismatchFormatting local function getTaTbDescr() if not M.ORDER_ACTUAL_EXPECTED then - return 'expected', 'actual' + return "expected", "actual" end - return 'actual', 'expected' + return "actual", "expected" end -local function extendWithStrFmt( res, ... ) - table.insert( res, string.format( ... ) ) +local function extendWithStrFmt(res, ...) + table.insert(res, string.format(...)) end -local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) +local function mismatchFormattingMapping(table_a, table_b, doDeepAnalysis) --[[ Prepares a nice error message when comparing tables which are not pure lists, performing a deeper analysis. @@ -826,7 +838,7 @@ local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) end M.private.mismatchFormattingMapping = mismatchFormattingMapping -local function mismatchFormattingPureList( table_a, table_b, margin ) +local function mismatchFormattingPureList(table_a, table_b, margin) --[[ Prepares a nice error message when comparing tables which are lists, performing a deeper analysis. @@ -840,12 +852,13 @@ local function mismatchFormattingPureList( table_a, table_b, margin ) ]] local result, descrTa, descrTb = {}, getTaTbDescr() - local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + local len_a, len_b, refa, refb = #table_a, #table_b, "", "" if M.PRINT_TABLE_REF_IN_ERROR_MSG then - refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + refa, refb = + string.format("<%s> ", M.private.table_ref(table_a)), string.format("<%s> ", M.private.table_ref(table_b)) end local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) - local deltalv = longest - shortest + local deltalv = longest - shortest local commonUntil = shortest for i = 1, shortest do @@ -857,83 +870,96 @@ local function mismatchFormattingPureList( table_a, table_b, margin ) local commonBackTo = shortest - 1 for i = 0, shortest - 1 do - if not M.private.is_table_equals(table_a[len_a-i], table_b[len_b-i], margin) then + if not M.private.is_table_equals(table_a[len_a - i], table_b[len_b - i], margin) then commonBackTo = i - 1 break end end - - table.insert( result, 'List difference analysis:' ) + table.insert(result, "List difference analysis:") if len_a == len_b then -- TODO: handle expected/actual naming - extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + extendWithStrFmt(result, "* lists %sA (%s) and %sB (%s) have the same size", refa, descrTa, refb, descrTb) else - extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + extendWithStrFmt( + result, + "* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items", + refa, + descrTa, + len_a, + refb, + descrTb, + len_b + ) end - extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + extendWithStrFmt(result, "* lists A and B start differing at index %d", commonUntil + 1) if commonBackTo >= 0 then if deltalv > 0 then - extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + extendWithStrFmt( + result, + "* lists A and B are equal again from index %d for A, %d for B", + len_a - commonBackTo, + len_b - commonBackTo + ) else - extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + extendWithStrFmt(result, "* lists A and B are equal again from index %d", len_a - commonBackTo) end end local function insertABValue(ai, bi) bi = bi or ai - if M.private.is_table_equals( table_a[ai], table_b[bi], margin) then - return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + if M.private.is_table_equals(table_a[ai], table_b[bi], margin) then + return extendWithStrFmt(result, " = A[%d], B[%d]: %s", ai, bi, prettystr(table_a[ai])) else - extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) - extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + extendWithStrFmt(result, " - A[%d]: %s", ai, prettystr(table_a[ai])) + extendWithStrFmt(result, " + B[%d]: %s", bi, prettystr(table_b[bi])) end end -- common parts to list A & B, at the beginning if commonUntil > 0 then - table.insert( result, '* Common parts:' ) + table.insert(result, "* Common parts:") for i = 1, commonUntil do - insertABValue( i ) + insertABValue(i) end end -- diffing parts to list A & B if commonUntil < shortest - commonBackTo - 1 then - table.insert( result, '* Differing parts:' ) + table.insert(result, "* Differing parts:") for i = commonUntil + 1, shortest - commonBackTo - 1 do - insertABValue( i ) + insertABValue(i) end end -- display indexes of one list, with no match on other list if shortest - commonBackTo <= longest - commonBackTo - 1 then - table.insert( result, '* Present only in one list:' ) + table.insert(result, "* Present only in one list:") for i = shortest - commonBackTo, longest - commonBackTo - 1 do if len_a > len_b then - extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + extendWithStrFmt(result, " - A[%d]: %s", i, prettystr(table_a[i])) -- table.insert( result, '+ (no matching B index)') else -- table.insert( result, '- no matching A index') - extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + extendWithStrFmt(result, " + B[%d]: %s", i, prettystr(table_b[i])) end end end -- common parts to list A & B, at the end if commonBackTo >= 0 then - table.insert( result, '* Common parts at the end of the lists' ) + table.insert(result, "* Common parts at the end of the lists") for i = longest - commonBackTo, longest do if len_a > len_b then - insertABValue( i, i-deltalv ) + insertABValue(i, i - deltalv) else - insertABValue( i-deltalv, i ) + insertABValue(i - deltalv, i) end end end - return true, table.concat( result, '\n') + return true, table.concat(result, "\n") end M.private.mismatchFormattingPureList = mismatchFormattingPureList @@ -960,14 +986,14 @@ local function prettystrPairs(value1, value2, suffix_a, suffix_b) end M.private.prettystrPairs = prettystrPairs -local UNKNOWN_REF = 'table 00-unknown ref' -local ref_generator = { value=1, [UNKNOWN_REF]=0 } +local UNKNOWN_REF = "table 00-unknown ref" +local ref_generator = { value = 1, [UNKNOWN_REF] = 0 } -local function table_ref( t ) +local function table_ref(t) -- return the default tostring() for tables, with the table ID, even if the table has a metatable -- with the __tostring converter - local ref = '' - local mt = getmetatable( t ) + local ref = "" + local mt = getmetatable(t) if mt == nil then ref = tostring(t) else @@ -977,12 +1003,12 @@ local function table_ref( t ) -- protected table, if __tostring is defined, we can -- not get the reference. And we can not know in advance. ref = tostring(t) - if not ref:match( 'table: 0?x?[%x]+' ) then + if not ref:match("table: 0?x?[%x]+") then return UNKNOWN_REF end else ref = tostring(t) - setmetatable( t, mt ) + setmetatable(t, mt) end end -- strip the "table: " part @@ -990,12 +1016,12 @@ local function table_ref( t ) if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then -- Create a new reference number ref_generator[ref] = ref_generator.value - ref_generator.value = ref_generator.value+1 + ref_generator.value = ref_generator.value + 1 end if M.PRINT_TABLE_REF_IN_ERROR_MSG then - return string.format('table %02d-%s', ref_generator[ref], ref) + return string.format("table %02d-%s", ref_generator[ref], ref) else - return string.format('table %02d', ref_generator[ref]) + return string.format("table %02d", ref_generator[ref]) end end M.private.table_ref = table_ref @@ -1003,7 +1029,7 @@ M.private.table_ref = table_ref local TABLE_TOSTRING_SEP = ", " local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) -local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) +local function _table_tostring(tbl, indentLevel, printTableRefs, cycleDetectTable) printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG cycleDetectTable = cycleDetectTable or {} cycleDetectTable[tbl] = true @@ -1016,36 +1042,34 @@ local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTab if "string" == type(k) and k:match("^[_%a][_%w]*$") then return k end - return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + return prettystr_sub(k, indentLevel + 1, printTableRefs, cycleDetectTable) end - local mt = getmetatable( tbl ) + local mt = getmetatable(tbl) if mt and mt.__tostring then -- if table has a __tostring() function in its metatable, use it to display the table -- else, compute a regular table result = tostring(tbl) - if type(result) ~= 'string' then - return string.format( '', prettystr(result) ) + if type(result) ~= "string" then + return string.format('', prettystr(result)) end - result = strsplit( '\n', result ) - return M.private._table_tostring_format_multiline_string( result, indentLevel ) - + result = strsplit("\n", result) + return M.private._table_tostring_format_multiline_string(result, indentLevel) else -- no metatable, compute the table representation local entry, count, seq_index = nil, 0, 1 - for k, v in sortedPairs( tbl ) do - + for k, v in sortedPairs(tbl) do -- key part if k == seq_index then -- for the sequential part of tables, we'll skip the "=" output - entry = '' + entry = "" seq_index = seq_index + 1 elseif cycleDetectTable[k] then -- recursion in the key detected cycleDetectTable.detected = true - entry = "<"..table_ref(k)..">=" + entry = "<" .. table_ref(k) .. ">=" else entry = keytostring(k) .. "=" end @@ -1054,29 +1078,25 @@ local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTab if cycleDetectTable[v] then -- recursion in the value detected! cycleDetectTable.detected = true - entry = entry .. "<"..table_ref(v)..">" + entry = entry .. "<" .. table_ref(v) .. ">" else - entry = entry .. - prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + entry = entry .. prettystr_sub(v, indentLevel + 1, printTableRefs, cycleDetectTable) end count = count + 1 result[count] = entry end - return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + return M.private._table_tostring_format_result(tbl, result, indentLevel, printTableRefs) end - end M.private._table_tostring = _table_tostring -- prettystr_sub() needs it -local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) - local indentString = '\n'..string.rep(" ", indentLevel - 1) - return table.concat( tbl_str, indentString ) - +local function _table_tostring_format_multiline_string(tbl_str, indentLevel) + local indentString = "\n" .. string.rep(" ", indentLevel - 1) + return table.concat(tbl_str, indentString) end M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string - -local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) +local function _table_tostring_format_result(tbl, result, indentLevel, printTableRefs) -- final function called in _table_to_string() to format the resulting list of -- string describing the table. @@ -1084,8 +1104,8 @@ local function _table_tostring_format_result( tbl, result, indentLevel, printTab -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values local totalLength = 0 - for k, v in ipairs( result ) do - totalLength = totalLength + string.len( v ) + for k, v in ipairs(result) do + totalLength = totalLength + string.len(v) if totalLength >= M.LINE_LENGTH then dispOnMultLines = true break @@ -1107,18 +1127,18 @@ local function _table_tostring_format_result( tbl, result, indentLevel, printTab if dispOnMultLines then local indentString = string.rep(" ", indentLevel - 1) result = { - "{\n ", - indentString, - table.concat(result, ",\n " .. indentString), - "\n", - indentString, - "}" - } + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}", + } else - result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + result = { "{", table.concat(result, TABLE_TOSTRING_SEP), "}" } end if printTableRefs then - table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + table.insert(result, 1, "<" .. table_ref(tbl) .. "> ") -- prepend table ref end return table.concat(result) end @@ -1139,13 +1159,14 @@ local function table_findkeyof(t, element) return nil end -local function _is_table_items_equals(actual, expected ) +local function _is_table_items_equals(actual, expected) local type_a, type_e = type(actual), type(expected) if type_a ~= type_e then return false - - elseif (type_a == 'table') --[[and (type_e == 'table')]] then + elseif + type_a == "table" --[[and (type_e == 'table')]] + then for k, v in pairs(actual) do if table_findkeyof(expected, v) == nil then return false -- v not contained in expected @@ -1157,7 +1178,6 @@ local function _is_table_items_equals(actual, expected ) end end return true - elseif actual ~= expected then return false end @@ -1199,8 +1219,8 @@ local _recursion_cache_MT = { end return value - end - } + end, + }, } local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual) @@ -1220,21 +1240,21 @@ local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlm return false -- different types won't match end - if type_a == 'number' then + if type_a == "number" then if marginForAlmostEqual ~= nil then return M.almostEquals(actual, expected, marginForAlmostEqual) else return actual == expected end - elseif type_a ~= 'table' then + elseif type_a ~= "table" then -- other types compare directly return actual == expected end - cycleDetectTable = cycleDetectTable or { actual={}, expected={} } - if cycleDetectTable.actual[ actual ] then + cycleDetectTable = cycleDetectTable or { actual = {}, expected = {} } + if cycleDetectTable.actual[actual] then -- oh, we hit a cycle in actual - if cycleDetectTable.expected[ expected ] then + if cycleDetectTable.expected[expected] then -- uh, we hit a cycle at the same time in expected -- so the two tables have similar structure return true @@ -1244,7 +1264,7 @@ local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlm return false end - if cycleDetectTable.expected[ expected ] then + if cycleDetectTable.expected[expected] then -- no cycle in actual, but cycle in expected -- the structure differ return false @@ -1254,9 +1274,8 @@ local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlm -- seeing this table for the first time -- mark the cycle detection - cycleDetectTable.actual[ actual ] = true - cycleDetectTable.expected[ expected ] = true - + cycleDetectTable.actual[actual] = true + cycleDetectTable.expected[expected] = true local actualKeysMatched = {} for k, v in pairs(actual) do @@ -1264,8 +1283,8 @@ local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlm if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then -- table differs on this key -- clear the cycle detection before returning - cycleDetectTable.actual[ actual ] = nil - cycleDetectTable.expected[ expected ] = nil + cycleDetectTable.actual[actual] = nil + cycleDetectTable.expected[expected] = nil return false end end @@ -1274,16 +1293,16 @@ local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlm if not actualKeysMatched[k] then -- Found a key that we did not see in "actual" -> mismatch -- clear the cycle detection before returning - cycleDetectTable.actual[ actual ] = nil - cycleDetectTable.expected[ expected ] = nil + cycleDetectTable.actual[actual] = nil + cycleDetectTable.expected[expected] = nil return false end -- Otherwise actual[k] was already matched against v = expected[k]. end -- all key match, we have a match ! - cycleDetectTable.actual[ actual ] = nil - cycleDetectTable.expected[ expected ] = nil + cycleDetectTable.actual[actual] = nil + cycleDetectTable.expected[expected] = nil return true end M.private._is_table_equals = _is_table_equals @@ -1293,8 +1312,8 @@ local function failure(main_msg, extra_msg_or_nil, level) -- for error() compatibility we adjust "level" here (by +1), to report the -- calling context local msg - if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then - msg = extra_msg_or_nil .. '\n' .. main_msg + if type(extra_msg_or_nil) == "string" and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. "\n" .. main_msg else msg = main_msg end @@ -1307,13 +1326,13 @@ end M.private.is_table_equals = is_table_equals local function fail_fmt(level, extra_msg_or_nil, ...) - -- failure with printf-style formatted message and given error level + -- failure with printf-style formatted message and given error level failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) end M.private.fail_fmt = fail_fmt local function error_fmt(level, ...) - -- printf-style error() + -- printf-style error() error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) end M.private.error_fmt = error_fmt @@ -1330,42 +1349,41 @@ local function errorMsgEquality(actual, expected, doDeepAnalysis, margin) if not M.ORDER_ACTUAL_EXPECTED then expected, actual = actual, expected end - if type(expected) == 'string' or type(expected) == 'table' then + if type(expected) == "string" or type(expected) == "table" then local strExpected, strActual = prettystrPairs(expected, actual) local result = string.format("expected: %s\nactual: %s", strExpected, strActual) if margin then - result = result .. '\nwere not equal by the margin of: '..prettystr(margin) + result = result .. "\nwere not equal by the margin of: " .. prettystr(margin) end -- extend with mismatch analysis if possible: local success, mismatchResult - success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis, margin ) + success, mismatchResult = tryMismatchFormatting(actual, expected, doDeepAnalysis, margin) if success then - result = table.concat( { result, mismatchResult }, '\n' ) + result = table.concat({ result, mismatchResult }, "\n") end return result end - return string.format("expected: %s, actual: %s", - prettystr(expected), prettystr(actual)) + return string.format("expected: %s, actual: %s", prettystr(expected), prettystr(actual)) end function M.assertError(f, ...) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error - if pcall( f, ... ) then - failure( "Expected an error when calling function but no error generated", nil, 2 ) + if pcall(f, ...) then + failure("Expected an error when calling function but no error generated", nil, 2) end end -function M.fail( msg ) +function M.fail(msg) -- stops a test due to a failure - failure( msg, nil, 2 ) + failure(msg, nil, 2) end -function M.failIf( cond, msg ) +function M.failIf(cond, msg) -- Fails a test with "msg" if condition is true if cond then - failure( msg, nil, 2 ) + failure(msg, nil, 2) end end @@ -1374,14 +1392,14 @@ function M.skip(msg) error_fmt(2, M.SKIP_PREFIX .. msg) end -function M.skipIf( cond, msg ) +function M.skipIf(cond, msg) -- skip a running test if condition is met if cond then error_fmt(2, M.SKIP_PREFIX .. msg) end end -function M.runOnlyIf( cond, msg ) +function M.runOnlyIf(cond, msg) -- continue a running test if condition is met, else skip it if not cond then error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) @@ -1393,66 +1411,81 @@ function M.success() error_fmt(2, M.SUCCESS_PREFIX) end -function M.successIf( cond ) +function M.successIf(cond) -- stops a test with a success if condition is met if cond then error_fmt(2, M.SUCCESS_PREFIX) end end - ------------------------------------------------------------------ -- Equality assertions ------------------------------------------------------------------ function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) - if type(actual) == 'table' and type(expected) == 'table' then + if type(actual) == "table" and type(expected) == "table" then if not is_table_equals(actual, expected) then - failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + failure(errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2) end elseif type(actual) ~= type(expected) then - failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2) elseif actual ~= expected then - failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2) end end -function M.almostEquals( actual, expected, margin ) - if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then - error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', - prettystr(actual), prettystr(expected), prettystr(margin)) +function M.almostEquals(actual, expected, margin) + if type(actual) ~= "number" or type(expected) ~= "number" or type(margin) ~= "number" then + error_fmt( + 3, + "almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s", + prettystr(actual), + prettystr(expected), + prettystr(margin) + ) end if margin < 0 then - error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + error_fmt(3, "almostEquals: margin must not be negative, current value is " .. margin) end return math.abs(expected - actual) <= margin end -function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) +function M.assertAlmostEquals(actual, expected, margin, extra_msg_or_nil) -- check that two floats are close by margin margin = margin or M.EPS - if type(margin) ~= 'number' then - error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin)) + if type(margin) ~= "number" then + error_fmt(2, "almostEquals: margin must be a number, not %s", prettystr(margin)) end - if type(actual) == 'table' and type(expected) == 'table' then + if type(actual) == "table" and type(expected) == "table" then -- handle almost equals for table if not is_table_equals(actual, expected, margin) then - failure( errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2 ) + failure(errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2) end - elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then + elseif type(actual) == "number" and type(expected) == "number" and type(margin) == "number" then if not M.almostEquals(actual, expected, margin) then if not M.ORDER_ACTUAL_EXPECTED then expected, actual = actual, expected end local delta = math.abs(actual - expected) - fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. - 'Actual: %s, expected: %s, delta %s above margin of %s', - actual, expected, delta, margin) + fail_fmt( + 2, + extra_msg_or_nil, + "Values are not almost equal\n" .. "Actual: %s, expected: %s, delta %s above margin of %s", + actual, + expected, + delta, + margin + ) end else - error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s', - prettystr(actual), prettystr(expected), prettystr(margin)) + error_fmt( + 3, + "almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s", + prettystr(actual), + prettystr(expected), + prettystr(margin) + ) end end @@ -1461,17 +1494,17 @@ function M.assertNotEquals(actual, expected, extra_msg_or_nil) return end - if type(actual) == 'table' and type(expected) == 'table' then + if type(actual) == "table" and type(expected) == "table" then if not is_table_equals(actual, expected) then return end elseif actual ~= expected then return end - fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) + fail_fmt(2, extra_msg_or_nil, "Received the not expected value: %s", prettystr(actual)) end -function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) +function M.assertNotAlmostEquals(actual, expected, margin, extra_msg_or_nil) -- check that two floats are not close by margin margin = margin or M.EPS if M.almostEquals(actual, expected, margin) then @@ -1479,9 +1512,15 @@ function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) expected, actual = actual, expected end local delta = math.abs(actual - expected) - fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. - ', delta %s below margin of %s', - actual, expected, delta, margin) + fail_fmt( + 2, + extra_msg_or_nil, + "Values are almost equal\nActual: %s, expected: %s" .. ", delta %s below margin of %s", + actual, + expected, + delta, + margin + ) end end @@ -1489,10 +1528,15 @@ function M.assertItemsEquals(actual, expected, extra_msg_or_nil) -- checks that the items of table expected -- are contained in table actual. Warning, this function -- is at least O(n^2) - if not _is_table_items_equals(actual, expected ) then + if not _is_table_items_equals(actual, expected) then expected, actual = prettystrPairs(expected, actual) - fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', - expected, actual) + fail_fmt( + 2, + extra_msg_or_nil, + "Content of the tables are not identical:\nExpected: %s\nActual: %s", + expected, + actual + ) end end @@ -1500,61 +1544,70 @@ end -- String assertion ------------------------------------------------------------------ -function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) +function M.assertStrContains(str, sub, isPattern, extra_msg_or_nil) -- this relies on lua string.find function -- a string always contains the empty string -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) if not string.find(str, sub, 1, not isPattern) then - sub, str = prettystrPairs(sub, str, '\n') - fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', - isPattern and 'pattern' or 'substring', sub, str) + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt( + 2, + extra_msg_or_nil, + "Could not find %s %s in string %s", + isPattern and "pattern" or "substring", + sub, + str + ) end end -function M.assertStrIContains( str, sub, extra_msg_or_nil ) +function M.assertStrIContains(str, sub, extra_msg_or_nil) -- this relies on lua string.find function -- a string always contains the empty string if not string.find(str:lower(), sub:lower(), 1, true) then - sub, str = prettystrPairs(sub, str, '\n') - fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', - sub, str) + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Could not find (case insensitively) substring %s in string %s", sub, str) end end -function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) +function M.assertNotStrContains(str, sub, isPattern, extra_msg_or_nil) -- this relies on lua string.find function -- a string always contains the empty string if string.find(str, sub, 1, not isPattern) then - sub, str = prettystrPairs(sub, str, '\n') - fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', - isPattern and 'pattern' or 'substring', sub, str) + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt( + 2, + extra_msg_or_nil, + "Found the not expected %s %s in string %s", + isPattern and "pattern" or "substring", + sub, + str + ) end end -function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) +function M.assertNotStrIContains(str, sub, extra_msg_or_nil) -- this relies on lua string.find function -- a string always contains the empty string if string.find(str:lower(), sub:lower(), 1, true) then - sub, str = prettystrPairs(sub, str, '\n') - fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', - sub, str) + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Found (case insensitively) the not expected substring %s in string %s", sub, str) end end -function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) +function M.assertStrMatches(str, pattern, start, final, extra_msg_or_nil) -- Verify a full match for the string - if not strMatch( str, pattern, start, final ) then - pattern, str = prettystrPairs(pattern, str, '\n') - fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', - pattern, str) + if not strMatch(str, pattern, start, final) then + pattern, str = prettystrPairs(pattern, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Could not match pattern %s with string %s", pattern, str) end end -local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) - local no_error, error_msg = pcall( func, ... ) +local function _assertErrorMsgEquals(stripFileAndLine, expectedMsg, func, ...) + local no_error, error_msg = pcall(func, ...) if no_error then - failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + failure("No error generated when calling function but expected error: " .. M.prettystr(expectedMsg), nil, 3) end if type(expectedMsg) == "string" and type(error_msg) ~= "string" then -- table are converted to string automatically @@ -1569,69 +1622,76 @@ local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) if error_msg ~= expectedMsg then local tr = type(error_msg) local te = type(expectedMsg) - if te == 'table' then - if tr ~= 'table' then + if te == "table" then + if tr ~= "table" then differ = true else - local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) - if not ok then - differ = true - end + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end end else - differ = true + differ = true end end end if differ then error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) - fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', - expectedMsg, error_msg) + fail_fmt(3, nil, "Error message expected: %s\nError message received: %s\n", expectedMsg, error_msg) end end -function M.assertErrorMsgEquals( expectedMsg, func, ... ) +function M.assertErrorMsgEquals(expectedMsg, func, ...) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error _assertErrorMsgEquals(false, expectedMsg, func, ...) end function M.assertErrorMsgContentEquals(expectedMsg, func, ...) - _assertErrorMsgEquals(true, expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) end -function M.assertErrorMsgContains( partialMsg, func, ... ) +function M.assertErrorMsgContains(partialMsg, func, ...) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error - local no_error, error_msg = pcall( func, ... ) + local no_error, error_msg = pcall(func, ...) if no_error then - failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + failure( + "No error generated when calling function but expected error containing: " .. prettystr(partialMsg), + nil, + 2 + ) end if type(error_msg) ~= "string" then error_msg = tostring(error_msg) end - if not string.find( error_msg, partialMsg, nil, true ) then + if not string.find(error_msg, partialMsg, nil, true) then error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) - fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', - partialMsg, error_msg) + fail_fmt(2, nil, "Error message does not contain: %s\nError message received: %s\n", partialMsg, error_msg) end end -function M.assertErrorMsgMatches( expectedMsg, func, ... ) +function M.assertErrorMsgMatches(expectedMsg, func, ...) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error - local no_error, error_msg = pcall( func, ... ) + local no_error, error_msg = pcall(func, ...) if no_error then - failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + failure('No error generated when calling function but expected error matching: "' .. expectedMsg .. '"', nil, 2) end if type(error_msg) ~= "string" then error_msg = tostring(error_msg) end - if not strMatch( error_msg, expectedMsg ) then + if not strMatch(error_msg, expectedMsg) then expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) - fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', - expectedMsg, error_msg) + fail_fmt( + 2, + nil, + "Error message does not match pattern: %s\nError message received: %s\n", + expectedMsg, + error_msg + ) end end @@ -1641,43 +1701,43 @@ end function M.assertEvalToTrue(value, extra_msg_or_nil) if not value then - failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: a value evaluating to true, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertEvalToFalse(value, extra_msg_or_nil) if value then - failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: false or nil, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsTrue(value, extra_msg_or_nil) if value ~= true then - failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: true, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsTrue(value, extra_msg_or_nil) if value == true then - failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: not true, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsFalse(value, extra_msg_or_nil) if value ~= false then - failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: false, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsFalse(value, extra_msg_or_nil) if value == false then - failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: not false, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsNil(value, extra_msg_or_nil) if value ~= nil then - failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: nil, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end @@ -1694,23 +1754,40 @@ expected string (derived from the function name): M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" ]] -for _, funcName in ipairs( - {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', - 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} -) do +for _, funcName in ipairs({ + "assertIsNumber", + "assertIsString", + "assertIsTable", + "assertIsBoolean", + "assertIsFunction", + "assertIsUserdata", + "assertIsThread", +}) do local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") -- Lua type() always returns lowercase, also make sure the match() succeeded typeExpected = typeExpected and typeExpected:lower() - or error("bad function name '"..funcName.."' for type assertion") + or error("bad function name '" .. funcName .. "' for type assertion") M[funcName] = function(value, extra_msg_or_nil) if type(value) ~= typeExpected then - if type(value) == 'nil' then - fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', - typeExpected, type(value), prettystrPairs(value)) + if type(value) == "nil" then + fail_fmt( + 2, + extra_msg_or_nil, + "expected: a %s value, actual: nil", + typeExpected, + type(value), + prettystrPairs(value) + ) else - fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', - typeExpected, type(value), prettystrPairs(value)) + fail_fmt( + 2, + extra_msg_or_nil, + "expected: a %s value, actual: type %s, value %s", + typeExpected, + type(value), + prettystrPairs(value) + ) end end end @@ -1720,16 +1797,13 @@ end Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) M.isXxx(value) -> returns true if type(value) conforms to "xxx" ]] -for _, typeExpected in ipairs( - {'Number', 'String', 'Table', 'Boolean', - 'Function', 'Userdata', 'Thread', 'Nil' } -) do +for _, typeExpected in ipairs({ "Number", "String", "Table", "Boolean", "Function", "Userdata", "Thread", "Nil" }) do local typeExpectedLower = typeExpected:lower() local isType = function(value) return (type(value) == typeExpectedLower) end - M['is'..typeExpected] = isType - M['is_'..typeExpectedLower] = isType + M["is" .. typeExpected] = isType + M["is_" .. typeExpectedLower] = isType end --[[ @@ -1739,19 +1813,29 @@ expected string (derived from the function name): M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" ]] -for _, funcName in ipairs( - {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', - 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} -) do +for _, funcName in ipairs({ + "assertNotIsNumber", + "assertNotIsString", + "assertNotIsTable", + "assertNotIsBoolean", + "assertNotIsFunction", + "assertNotIsUserdata", + "assertNotIsThread", +}) do local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") -- Lua type() always returns lowercase, also make sure the match() succeeded typeUnexpected = typeUnexpected and typeUnexpected:lower() - or error("bad function name '"..funcName.."' for type assertion") + or error("bad function name '" .. funcName .. "' for type assertion") M[funcName] = function(value, extra_msg_or_nil) if type(value) == typeUnexpected then - fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', - typeUnexpected, prettystrPairs(value)) + fail_fmt( + 2, + extra_msg_or_nil, + "expected: not a %s type, actual: value %s", + typeUnexpected, + prettystrPairs(value) + ) end end end @@ -1763,10 +1847,15 @@ function M.assertIs(actual, expected, extra_msg_or_nil) end local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG M.PRINT_TABLE_REF_IN_ERROR_MSG = true - expected, actual = prettystrPairs(expected, actual, '\n', '') + expected, actual = prettystrPairs(expected, actual, "\n", "") M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg - fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', - expected, actual) + fail_fmt( + 2, + extra_msg_or_nil, + "expected and actual object should not be different\nExpected: %s\nReceived: %s", + expected, + actual + ) end end @@ -1781,19 +1870,17 @@ function M.assertNotIs(actual, expected, extra_msg_or_nil) s_expected = prettystrPairs(expected) end M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg - fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + fail_fmt(2, extra_msg_or_nil, "expected and actual object should be different: %s", s_expected) end end - ------------------------------------------------------------------ -- Scientific assertions ------------------------------------------------------------------ - function M.assertIsNaN(value, extra_msg_or_nil) if type(value) ~= "number" or value == value then - failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: NaN, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end @@ -1805,19 +1892,19 @@ end function M.assertIsInf(value, extra_msg_or_nil) if type(value) ~= "number" or math.abs(value) ~= math.huge then - failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsPlusInf(value, extra_msg_or_nil) if type(value) ~= "number" or value ~= math.huge then - failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsMinusInf(value, extra_msg_or_nil) if type(value) ~= "number" or value ~= -math.huge then - failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: -#Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end @@ -1840,41 +1927,45 @@ function M.assertNotIsInf(value, extra_msg_or_nil) end function M.assertIsPlusZero(value, extra_msg_or_nil) - if type(value) ~= 'number' or value ~= 0 then - failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) - else if (1/value == -math.huge) then + if type(value) ~= "number" or value ~= 0 then + failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) + else + if 1 / value == -math.huge then -- more precise error diagnosis failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) - else if (1/value ~= math.huge) then + else + if 1 / value ~= math.huge then -- strange, case should have already been covered - failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end end end function M.assertIsMinusZero(value, extra_msg_or_nil) - if type(value) ~= 'number' or value ~= 0 then - failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) - else if (1/value == math.huge) then + if type(value) ~= "number" or value ~= 0 then + failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) + else + if 1 / value == math.huge then -- more precise error diagnosis failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) - else if (1/value ~= -math.huge) then + else + if 1 / value ~= -math.huge then -- strange, case should have already been covered - failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end end end function M.assertNotIsPlusZero(value, extra_msg_or_nil) - if type(value) == 'number' and (1/value == math.huge) then + if type(value) == "number" and (1 / value == math.huge) then failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) end end function M.assertNotIsMinusZero(value, extra_msg_or_nil) - if type(value) == 'number' and (1/value == -math.huge) then + if type(value) == "number" and (1 / value == -math.huge) then failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) end end @@ -1883,8 +1974,7 @@ function M.assertTableContains(t, expected, extra_msg_or_nil) -- checks that table t contains the expected element if table_findkeyof(t, expected) == nil then t, expected = prettystrPairs(t, expected) - fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s', - t, expected) + fail_fmt(2, extra_msg_or_nil, "Table %s does NOT contain the expected element %s", t, expected) end end @@ -1893,8 +1983,14 @@ function M.assertNotTableContains(t, expected, extra_msg_or_nil) local k = table_findkeyof(t, expected) if k ~= nil then t, expected = prettystrPairs(t, expected) - fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)', - t, expected, prettystr(k)) + fail_fmt( + 2, + extra_msg_or_nil, + "Table %s DOES contain the unwanted element %s (at key %s)", + t, + expected, + prettystr(k) + ) end end @@ -1908,161 +2004,161 @@ function M.wrapFunctions() -- a test function inside the global test suite. Nowadays, the functions -- are simply run directly as part of the test discovery process. -- so just do nothing ! - io.stderr:write[[Use of WrapFunctions() is no longer needed. + io.stderr:write([[Use of WrapFunctions() is no longer needed. Just prefix your test function names with "test" or "Test" and they will be picked up and run by LuaUnit. -]] +]]) end local list_of_funcs = { -- { official function name , alias } -- general assertions - { 'assertEquals' , 'assert_equals' }, - { 'assertItemsEquals' , 'assert_items_equals' }, - { 'assertNotEquals' , 'assert_not_equals' }, - { 'assertAlmostEquals' , 'assert_almost_equals' }, - { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, - { 'assertEvalToTrue' , 'assert_eval_to_true' }, - { 'assertEvalToFalse' , 'assert_eval_to_false' }, - { 'assertStrContains' , 'assert_str_contains' }, - { 'assertStrIContains' , 'assert_str_icontains' }, - { 'assertNotStrContains' , 'assert_not_str_contains' }, - { 'assertNotStrIContains' , 'assert_not_str_icontains' }, - { 'assertStrMatches' , 'assert_str_matches' }, - { 'assertError' , 'assert_error' }, - { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, - { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, - { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, - { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, - { 'assertIs' , 'assert_is' }, - { 'assertNotIs' , 'assert_not_is' }, - { 'assertTableContains' , 'assert_table_contains' }, - { 'assertNotTableContains' , 'assert_not_table_contains' }, - { 'wrapFunctions' , 'WrapFunctions' }, - { 'wrapFunctions' , 'wrap_functions' }, + { "assertEquals", "assert_equals" }, + { "assertItemsEquals", "assert_items_equals" }, + { "assertNotEquals", "assert_not_equals" }, + { "assertAlmostEquals", "assert_almost_equals" }, + { "assertNotAlmostEquals", "assert_not_almost_equals" }, + { "assertEvalToTrue", "assert_eval_to_true" }, + { "assertEvalToFalse", "assert_eval_to_false" }, + { "assertStrContains", "assert_str_contains" }, + { "assertStrIContains", "assert_str_icontains" }, + { "assertNotStrContains", "assert_not_str_contains" }, + { "assertNotStrIContains", "assert_not_str_icontains" }, + { "assertStrMatches", "assert_str_matches" }, + { "assertError", "assert_error" }, + { "assertErrorMsgEquals", "assert_error_msg_equals" }, + { "assertErrorMsgContains", "assert_error_msg_contains" }, + { "assertErrorMsgMatches", "assert_error_msg_matches" }, + { "assertErrorMsgContentEquals", "assert_error_msg_content_equals" }, + { "assertIs", "assert_is" }, + { "assertNotIs", "assert_not_is" }, + { "assertTableContains", "assert_table_contains" }, + { "assertNotTableContains", "assert_not_table_contains" }, + { "wrapFunctions", "WrapFunctions" }, + { "wrapFunctions", "wrap_functions" }, -- type assertions: assertIsXXX -> assert_is_xxx - { 'assertIsNumber' , 'assert_is_number' }, - { 'assertIsString' , 'assert_is_string' }, - { 'assertIsTable' , 'assert_is_table' }, - { 'assertIsBoolean' , 'assert_is_boolean' }, - { 'assertIsNil' , 'assert_is_nil' }, - { 'assertIsTrue' , 'assert_is_true' }, - { 'assertIsFalse' , 'assert_is_false' }, - { 'assertIsNaN' , 'assert_is_nan' }, - { 'assertIsInf' , 'assert_is_inf' }, - { 'assertIsPlusInf' , 'assert_is_plus_inf' }, - { 'assertIsMinusInf' , 'assert_is_minus_inf' }, - { 'assertIsPlusZero' , 'assert_is_plus_zero' }, - { 'assertIsMinusZero' , 'assert_is_minus_zero' }, - { 'assertIsFunction' , 'assert_is_function' }, - { 'assertIsThread' , 'assert_is_thread' }, - { 'assertIsUserdata' , 'assert_is_userdata' }, + { "assertIsNumber", "assert_is_number" }, + { "assertIsString", "assert_is_string" }, + { "assertIsTable", "assert_is_table" }, + { "assertIsBoolean", "assert_is_boolean" }, + { "assertIsNil", "assert_is_nil" }, + { "assertIsTrue", "assert_is_true" }, + { "assertIsFalse", "assert_is_false" }, + { "assertIsNaN", "assert_is_nan" }, + { "assertIsInf", "assert_is_inf" }, + { "assertIsPlusInf", "assert_is_plus_inf" }, + { "assertIsMinusInf", "assert_is_minus_inf" }, + { "assertIsPlusZero", "assert_is_plus_zero" }, + { "assertIsMinusZero", "assert_is_minus_zero" }, + { "assertIsFunction", "assert_is_function" }, + { "assertIsThread", "assert_is_thread" }, + { "assertIsUserdata", "assert_is_userdata" }, -- type assertions: assertIsXXX -> assertXxx - { 'assertIsNumber' , 'assertNumber' }, - { 'assertIsString' , 'assertString' }, - { 'assertIsTable' , 'assertTable' }, - { 'assertIsBoolean' , 'assertBoolean' }, - { 'assertIsNil' , 'assertNil' }, - { 'assertIsTrue' , 'assertTrue' }, - { 'assertIsFalse' , 'assertFalse' }, - { 'assertIsNaN' , 'assertNaN' }, - { 'assertIsInf' , 'assertInf' }, - { 'assertIsPlusInf' , 'assertPlusInf' }, - { 'assertIsMinusInf' , 'assertMinusInf' }, - { 'assertIsPlusZero' , 'assertPlusZero' }, - { 'assertIsMinusZero' , 'assertMinusZero'}, - { 'assertIsFunction' , 'assertFunction' }, - { 'assertIsThread' , 'assertThread' }, - { 'assertIsUserdata' , 'assertUserdata' }, + { "assertIsNumber", "assertNumber" }, + { "assertIsString", "assertString" }, + { "assertIsTable", "assertTable" }, + { "assertIsBoolean", "assertBoolean" }, + { "assertIsNil", "assertNil" }, + { "assertIsTrue", "assertTrue" }, + { "assertIsFalse", "assertFalse" }, + { "assertIsNaN", "assertNaN" }, + { "assertIsInf", "assertInf" }, + { "assertIsPlusInf", "assertPlusInf" }, + { "assertIsMinusInf", "assertMinusInf" }, + { "assertIsPlusZero", "assertPlusZero" }, + { "assertIsMinusZero", "assertMinusZero" }, + { "assertIsFunction", "assertFunction" }, + { "assertIsThread", "assertThread" }, + { "assertIsUserdata", "assertUserdata" }, -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) - { 'assertIsNumber' , 'assert_number' }, - { 'assertIsString' , 'assert_string' }, - { 'assertIsTable' , 'assert_table' }, - { 'assertIsBoolean' , 'assert_boolean' }, - { 'assertIsNil' , 'assert_nil' }, - { 'assertIsTrue' , 'assert_true' }, - { 'assertIsFalse' , 'assert_false' }, - { 'assertIsNaN' , 'assert_nan' }, - { 'assertIsInf' , 'assert_inf' }, - { 'assertIsPlusInf' , 'assert_plus_inf' }, - { 'assertIsMinusInf' , 'assert_minus_inf' }, - { 'assertIsPlusZero' , 'assert_plus_zero' }, - { 'assertIsMinusZero' , 'assert_minus_zero' }, - { 'assertIsFunction' , 'assert_function' }, - { 'assertIsThread' , 'assert_thread' }, - { 'assertIsUserdata' , 'assert_userdata' }, + { "assertIsNumber", "assert_number" }, + { "assertIsString", "assert_string" }, + { "assertIsTable", "assert_table" }, + { "assertIsBoolean", "assert_boolean" }, + { "assertIsNil", "assert_nil" }, + { "assertIsTrue", "assert_true" }, + { "assertIsFalse", "assert_false" }, + { "assertIsNaN", "assert_nan" }, + { "assertIsInf", "assert_inf" }, + { "assertIsPlusInf", "assert_plus_inf" }, + { "assertIsMinusInf", "assert_minus_inf" }, + { "assertIsPlusZero", "assert_plus_zero" }, + { "assertIsMinusZero", "assert_minus_zero" }, + { "assertIsFunction", "assert_function" }, + { "assertIsThread", "assert_thread" }, + { "assertIsUserdata", "assert_userdata" }, -- type assertions: assertNotIsXXX -> assert_not_is_xxx - { 'assertNotIsNumber' , 'assert_not_is_number' }, - { 'assertNotIsString' , 'assert_not_is_string' }, - { 'assertNotIsTable' , 'assert_not_is_table' }, - { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, - { 'assertNotIsNil' , 'assert_not_is_nil' }, - { 'assertNotIsTrue' , 'assert_not_is_true' }, - { 'assertNotIsFalse' , 'assert_not_is_false' }, - { 'assertNotIsNaN' , 'assert_not_is_nan' }, - { 'assertNotIsInf' , 'assert_not_is_inf' }, - { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, - { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, - { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, - { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, - { 'assertNotIsFunction' , 'assert_not_is_function' }, - { 'assertNotIsThread' , 'assert_not_is_thread' }, - { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + { "assertNotIsNumber", "assert_not_is_number" }, + { "assertNotIsString", "assert_not_is_string" }, + { "assertNotIsTable", "assert_not_is_table" }, + { "assertNotIsBoolean", "assert_not_is_boolean" }, + { "assertNotIsNil", "assert_not_is_nil" }, + { "assertNotIsTrue", "assert_not_is_true" }, + { "assertNotIsFalse", "assert_not_is_false" }, + { "assertNotIsNaN", "assert_not_is_nan" }, + { "assertNotIsInf", "assert_not_is_inf" }, + { "assertNotIsPlusInf", "assert_not_plus_inf" }, + { "assertNotIsMinusInf", "assert_not_minus_inf" }, + { "assertNotIsPlusZero", "assert_not_plus_zero" }, + { "assertNotIsMinusZero", "assert_not_minus_zero" }, + { "assertNotIsFunction", "assert_not_is_function" }, + { "assertNotIsThread", "assert_not_is_thread" }, + { "assertNotIsUserdata", "assert_not_is_userdata" }, -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) - { 'assertNotIsNumber' , 'assertNotNumber' }, - { 'assertNotIsString' , 'assertNotString' }, - { 'assertNotIsTable' , 'assertNotTable' }, - { 'assertNotIsBoolean' , 'assertNotBoolean' }, - { 'assertNotIsNil' , 'assertNotNil' }, - { 'assertNotIsTrue' , 'assertNotTrue' }, - { 'assertNotIsFalse' , 'assertNotFalse' }, - { 'assertNotIsNaN' , 'assertNotNaN' }, - { 'assertNotIsInf' , 'assertNotInf' }, - { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, - { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, - { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, - { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, - { 'assertNotIsFunction' , 'assertNotFunction' }, - { 'assertNotIsThread' , 'assertNotThread' }, - { 'assertNotIsUserdata' , 'assertNotUserdata' }, + { "assertNotIsNumber", "assertNotNumber" }, + { "assertNotIsString", "assertNotString" }, + { "assertNotIsTable", "assertNotTable" }, + { "assertNotIsBoolean", "assertNotBoolean" }, + { "assertNotIsNil", "assertNotNil" }, + { "assertNotIsTrue", "assertNotTrue" }, + { "assertNotIsFalse", "assertNotFalse" }, + { "assertNotIsNaN", "assertNotNaN" }, + { "assertNotIsInf", "assertNotInf" }, + { "assertNotIsPlusInf", "assertNotPlusInf" }, + { "assertNotIsMinusInf", "assertNotMinusInf" }, + { "assertNotIsPlusZero", "assertNotPlusZero" }, + { "assertNotIsMinusZero", "assertNotMinusZero" }, + { "assertNotIsFunction", "assertNotFunction" }, + { "assertNotIsThread", "assertNotThread" }, + { "assertNotIsUserdata", "assertNotUserdata" }, -- type assertions: assertNotIsXXX -> assert_not_xxx - { 'assertNotIsNumber' , 'assert_not_number' }, - { 'assertNotIsString' , 'assert_not_string' }, - { 'assertNotIsTable' , 'assert_not_table' }, - { 'assertNotIsBoolean' , 'assert_not_boolean' }, - { 'assertNotIsNil' , 'assert_not_nil' }, - { 'assertNotIsTrue' , 'assert_not_true' }, - { 'assertNotIsFalse' , 'assert_not_false' }, - { 'assertNotIsNaN' , 'assert_not_nan' }, - { 'assertNotIsInf' , 'assert_not_inf' }, - { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, - { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, - { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, - { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, - { 'assertNotIsFunction' , 'assert_not_function' }, - { 'assertNotIsThread' , 'assert_not_thread' }, - { 'assertNotIsUserdata' , 'assert_not_userdata' }, + { "assertNotIsNumber", "assert_not_number" }, + { "assertNotIsString", "assert_not_string" }, + { "assertNotIsTable", "assert_not_table" }, + { "assertNotIsBoolean", "assert_not_boolean" }, + { "assertNotIsNil", "assert_not_nil" }, + { "assertNotIsTrue", "assert_not_true" }, + { "assertNotIsFalse", "assert_not_false" }, + { "assertNotIsNaN", "assert_not_nan" }, + { "assertNotIsInf", "assert_not_inf" }, + { "assertNotIsPlusInf", "assert_not_plus_inf" }, + { "assertNotIsMinusInf", "assert_not_minus_inf" }, + { "assertNotIsPlusZero", "assert_not_plus_zero" }, + { "assertNotIsMinusZero", "assert_not_minus_zero" }, + { "assertNotIsFunction", "assert_not_function" }, + { "assertNotIsThread", "assert_not_thread" }, + { "assertNotIsUserdata", "assert_not_userdata" }, -- all assertions with Coroutine duplicate Thread assertions - { 'assertIsThread' , 'assertIsCoroutine' }, - { 'assertIsThread' , 'assertCoroutine' }, - { 'assertIsThread' , 'assert_is_coroutine' }, - { 'assertIsThread' , 'assert_coroutine' }, - { 'assertNotIsThread' , 'assertNotIsCoroutine' }, - { 'assertNotIsThread' , 'assertNotCoroutine' }, - { 'assertNotIsThread' , 'assert_not_is_coroutine' }, - { 'assertNotIsThread' , 'assert_not_coroutine' }, + { "assertIsThread", "assertIsCoroutine" }, + { "assertIsThread", "assertCoroutine" }, + { "assertIsThread", "assert_is_coroutine" }, + { "assertIsThread", "assert_coroutine" }, + { "assertNotIsThread", "assertNotIsCoroutine" }, + { "assertNotIsThread", "assertNotCoroutine" }, + { "assertNotIsThread", "assert_not_is_coroutine" }, + { "assertNotIsThread", "assert_not_coroutine" }, } -- Create all aliases in M -for _,v in ipairs( list_of_funcs ) do +for _, v in ipairs(list_of_funcs) do local funcname, alias = v[1], v[2] M[alias] = M[funcname] @@ -2081,7 +2177,7 @@ end -- A common "base" class for outputters -- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html -local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput = { __class__ = "genericOutput" } -- class local genericOutput_MT = { __index = genericOutput } -- metatable M.genericOutput = genericOutput -- publish, so that custom classes may derive from it @@ -2095,7 +2191,7 @@ function genericOutput.new(runner, default_verbosity) else t.verbosity = default_verbosity end - return setmetatable( t, genericOutput_MT) + return setmetatable(t, genericOutput_MT) end -- abstract ("empty") methods @@ -2130,57 +2226,55 @@ function genericOutput:endSuite() -- called at the end of the test suite execution end - ---------------------------------------------------------------- -- class TapOutput ---------------------------------------------------------------- local TapOutput = genericOutput.new() -- derived class local TapOutput_MT = { __index = TapOutput } -- metatable -TapOutput.__class__ = 'TapOutput' +TapOutput.__class__ = "TapOutput" - -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html +-- For a good reference for TAP format, check: http://testanything.org/tap-specification.html - function TapOutput.new(runner) - local t = genericOutput.new(runner, M.VERBOSITY_LOW) - return setmetatable( t, TapOutput_MT) +function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable(t, TapOutput_MT) +end +function TapOutput:startSuite() + print("1.." .. self.result.selectedCount) + print("# Started on " .. self.result.startDate) +end +function TapOutput:startClass(className) + if className ~= "[TestFunctions]" then + print("# Starting class: " .. className) end - function TapOutput:startSuite() - print("1.."..self.result.selectedCount) - print('# Started on '..self.result.startDate) - end - function TapOutput:startClass(className) - if className ~= '[TestFunctions]' then - print('# Starting class: '..className) - end +end + +function TapOutput:updateStatus(node) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n") + return end - function TapOutput:updateStatus( node ) - if node:isSkipped() then - io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) - return - end - - io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") - if self.verbosity > M.VERBOSITY_LOW then - print( prefixString( '# ', node.msg ) ) - end - if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then - print( prefixString( '# ', node.stackTrace ) ) - end + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print(prefixString("# ", node.msg)) end - - function TapOutput:endTest( node ) - if node:isSuccess() then - io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") - end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print(prefixString("# ", node.stackTrace)) end +end - function TapOutput:endSuite() - print( '# '..M.LuaUnit.statusLine( self.result ) ) - return self.result.notSuccessCount +function TapOutput:endTest(node) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") end +end +function TapOutput:endSuite() + print("# " .. M.LuaUnit.statusLine(self.result)) + return self.result.notSuccessCount +end -- class TapOutput end @@ -2191,83 +2285,96 @@ TapOutput.__class__ = 'TapOutput' -- See directory junitxml for more information about the junit format local JUnitOutput = genericOutput.new() -- derived class local JUnitOutput_MT = { __index = JUnitOutput } -- metatable -JUnitOutput.__class__ = 'JUnitOutput' +JUnitOutput.__class__ = "JUnitOutput" - function JUnitOutput.new(runner) - local t = genericOutput.new(runner, M.VERBOSITY_LOW) - t.testList = {} - return setmetatable( t, JUnitOutput_MT ) +function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable(t, JUnitOutput_MT) +end + +function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error("With Junit, an output filename must be supplied with --name!") + end + if string.sub(self.fname, -4) ~= ".xml" then + self.fname = self.fname .. ".xml" + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: " .. self.fname) end - function JUnitOutput:startSuite() - -- open xml file early to deal with errors - if self.fname == nil then - error('With Junit, an output filename must be supplied with --name!') - end - if string.sub(self.fname,-4) ~= '.xml' then - self.fname = self.fname..'.xml' - end - self.fd = io.open(self.fname, "w") - if self.fd == nil then - error("Could not open file for writing: "..self.fname) - end - - print('# XML output to '..self.fname) - print('# Started on '..self.result.startDate) - end - function JUnitOutput:startClass(className) - if className ~= '[TestFunctions]' then - print('# Starting class: '..className) - end - end - function JUnitOutput:startTest(testName) - print('# Starting test: '..testName) + print("# XML output to " .. self.fname) + print("# Started on " .. self.result.startDate) +end +function JUnitOutput:startClass(className) + if className ~= "[TestFunctions]" then + print("# Starting class: " .. className) end +end +function JUnitOutput:startTest(testName) + print("# Starting test: " .. testName) +end - function JUnitOutput:updateStatus( node ) - if node:isFailure() then - print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) - -- print('# ' .. node.stackTrace) - elseif node:isError() then - print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) - -- print('# ' .. node.stackTrace) - end +function JUnitOutput:updateStatus(node) + if node:isFailure() then + print("# Failure: " .. prefixString("# ", node.msg):sub(4, nil)) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print("# Error: " .. prefixString("# ", node.msg):sub(4, nil)) + -- print('# ' .. node.stackTrace) end +end - function JUnitOutput:endSuite() - print( '# '..M.LuaUnit.statusLine(self.result)) +function JUnitOutput:endSuite() + print("# " .. M.LuaUnit.statusLine(self.result)) - -- XML file writing - self.fd:write('\n') - self.fd:write('\n') - self.fd:write(string.format( + -- XML file writing + self.fd:write('\n') + self.fd:write("\n") + self.fd:write( + string.format( ' \n', - self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) - self.fd:write(" \n") - self.fd:write(string.format(' \n', _VERSION ) ) - self.fd:write(string.format(' \n', M.VERSION) ) - -- XXX please include system name and version if possible - self.fd:write(" \n") + self.result.runCount, + self.result.startIsodate, + self.result.duration, + self.result.errorCount, + self.result.failureCount, + self.result.skippedCount + ) + ) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION)) + self.fd:write(string.format(' \n', M.VERSION)) + -- XXX please include system name and version if possible + self.fd:write(" \n") - for i,node in ipairs(self.result.allTests) do - self.fd:write(string.format(' \n', - node.className, node.testName, node.duration ) ) - if node:isNotSuccess() then - self.fd:write(node:statusXML()) - end - self.fd:write(' \n') + for i, node in ipairs(self.result.allTests) do + self.fd:write( + string.format( + ' \n', + node.className, + node.testName, + node.duration + ) + ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) end - - -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: - self.fd:write(' \n') - self.fd:write(' \n') - - self.fd:write(' \n') - self.fd:write('\n') - self.fd:close() - return self.result.notSuccessCount + self.fd:write(" \n") end + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(" \n") + self.fd:write(" \n") + + self.fd:write(" \n") + self.fd:write("\n") + self.fd:close() + return self.result.notSuccessCount +end -- class TapOutput end @@ -2377,96 +2484,95 @@ then OK or FAILED (failures=1, error=1) local TextOutput = genericOutput.new() -- derived class local TextOutput_MT = { __index = TextOutput } -- metatable -TextOutput.__class__ = 'TextOutput' +TextOutput.__class__ = "TextOutput" - function TextOutput.new(runner) - local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) - t.errorList = {} - return setmetatable( t, TextOutput_MT ) +function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable(t, TextOutput_MT) +end + +function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("Started on " .. self.result.startDate) end +end - function TextOutput:startSuite() +function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write(" ", self.result.currentNode.testName, " ... ") + end +end + +function TextOutput:endTest(node) + if node:isSuccess() then if self.verbosity > M.VERBOSITY_DEFAULT then - print( 'Started on '.. self.result.startDate ) - end - end - - function TextOutput:startTest(testName) - if self.verbosity > M.VERBOSITY_DEFAULT then - io.stdout:write( " ", self.result.currentNode.testName, " ... " ) - end - end - - function TextOutput:endTest( node ) - if node:isSuccess() then - if self.verbosity > M.VERBOSITY_DEFAULT then - io.stdout:write("Ok\n") - else - io.stdout:write(".") - io.stdout:flush() - end + io.stdout:write("Ok\n") else - if self.verbosity > M.VERBOSITY_DEFAULT then - print( node.status ) - print( node.msg ) - --[[ + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print(node.status) + print(node.msg) + --[[ -- find out when to do this: if self.verbosity > M.VERBOSITY_DEFAULT then print( node.stackTrace ) end ]] - else - -- write only the first character of status E, F or S - io.stdout:write(string.sub(node.status, 1, 1)) - io.stdout:flush() - end + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() end end +end - function TextOutput:displayOneFailedTest( index, fail ) - print(index..") "..fail.testName ) - print( fail.msg ) - print( fail.stackTrace ) +function TextOutput:displayOneFailedTest(index, fail) + print(index .. ") " .. fail.testName) + print(fail.msg) + print(fail.stackTrace) + print() +end + +function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end +end + +function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end +end + +function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else print() end - - function TextOutput:displayErroredTests() - if #self.result.errorTests ~= 0 then - print("Tests with errors:") - print("------------------") - for i, v in ipairs(self.result.errorTests) do - self:displayOneFailedTest(i, v) - end - end - end - - function TextOutput:displayFailedTests() - if #self.result.failedTests ~= 0 then - print("Failed tests:") - print("-------------") - for i, v in ipairs(self.result.failedTests) do - self:displayOneFailedTest(i, v) - end - end - end - - function TextOutput:endSuite() - if self.verbosity > M.VERBOSITY_DEFAULT then - print("=========================================================") - else - print() - end - self:displayErroredTests() - self:displayFailedTests() - print( M.LuaUnit.statusLine( self.result ) ) - if self.result.notSuccessCount == 0 then - print('OK') - end + self:displayErroredTests() + self:displayFailedTests() + print(M.LuaUnit.statusLine(self.result)) + if self.result.notSuccessCount == 0 then + print("OK") end +end -- class TextOutput end - ---------------------------------------------------------------- -- class NilOutput ---------------------------------------------------------------- @@ -2476,11 +2582,11 @@ local function nopCallable() return nopCallable end -local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput = { __class__ = "NilOuptut" } -- class local NilOutput_MT = { __index = nopCallable } -- metatable function NilOutput.new(runner) - return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) + return setmetatable({ __class__ = "NilOutput" }, NilOutput_MT) end ---------------------------------------------------------------- @@ -2492,8 +2598,8 @@ end M.LuaUnit = { outputType = TextOutput, verbosity = M.VERBOSITY_DEFAULT, - __class__ = 'LuaUnit', - instances = {} + __class__ = "LuaUnit", + instances = {}, } local LuaUnit_MT = { __index = M.LuaUnit } @@ -2501,22 +2607,22 @@ if EXPORT_ASSERT_TO_GLOBALS then LuaUnit = M.LuaUnit end - function M.LuaUnit.new() - local newInstance = setmetatable( {}, LuaUnit_MT ) - return newInstance +function M.LuaUnit.new() + local newInstance = setmetatable({}, LuaUnit_MT) + return newInstance +end + +-----------------[[ Utility methods ]]--------------------- + +function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if "function" == type(aObject) then + return aObject end +end - -----------------[[ Utility methods ]]--------------------- - - function M.LuaUnit.asFunction(aObject) - -- return "aObject" if it is a function, and nil otherwise - if 'function' == type(aObject) then - return aObject - end - end - - function M.LuaUnit.splitClassMethod(someName) - --[[ +function M.LuaUnit.splitClassMethod(someName) + --[[ Return a pair of className, methodName strings for a name in the form "class.method". If no class part (or separator) is found, will return nil, someName instead (the latter being unchanged). @@ -2524,368 +2630,369 @@ end This convention thus also replaces the older isClassMethod() test: You just have to check for a non-nil className (return) value. ]] - local separator = string.find(someName, '.', 1, true) - if separator then - return someName:sub(1, separator - 1), someName:sub(separator + 1) - end - return nil, someName + local separator = string.find(someName, ".", 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) end + return nil, someName +end - function M.LuaUnit.isMethodTestName( s ) - -- return true is the name matches the name of a test method - -- default rule is that is starts with 'Test' or with 'test' - return string.sub(s, 1, 4):lower() == 'test' +function M.LuaUnit.isMethodTestName(s) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == "test" +end + +function M.LuaUnit.isTestName(s) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == "test" +end + +function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName(k) then + table.insert(testNames, k) + end end + table.sort(testNames) + return testNames +end - function M.LuaUnit.isTestName( s ) - -- return true is the name matches the name of a test - -- default rule is that is starts with 'Test' or with 'test' - return string.sub(s, 1, 4):lower() == 'test' - end +function M.LuaUnit.parseCmdLine(cmdLine) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns - function M.LuaUnit.collectTests() - -- return a list of all test names in the global namespace - -- that match LuaUnit.isTestName - - local testNames = {} - for k, _ in pairs(_G) do - if type(k) == "string" and M.LuaUnit.isTestName( k ) then - table.insert( testNames , k ) - end - end - table.sort( testNames ) - return testNames - end - - function M.LuaUnit.parseCmdLine( cmdLine ) - -- parse the command line - -- Supported command line parameters: - -- --verbose, -v: increase verbosity - -- --quiet, -q: silence output - -- --error, -e: treat errors as fatal (quit program) - -- --output, -o, + name: select output type - -- --pattern, -p, + pattern: run test matching pattern, may be repeated - -- --exclude, -x, + pattern: run test not matching pattern, may be repeated - -- --shuffle, -s, : shuffle tests before reunning them - -- --name, -n, + fname: name of output file for junit, default to stdout - -- --repeat, -r, + num: number of times to execute each test - -- [testnames, ...]: run selected test names - -- - -- Returns a table with the following fields: - -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE - -- output: nil, 'tap', 'junit', 'text', 'nil' - -- testNames: nil or a list of test names to run - -- exeRepeat: num or 1 - -- pattern: nil or a list of patterns - -- exclude: nil or a list of patterns - - local result, state = {}, nil - local SET_OUTPUT = 1 - local SET_PATTERN = 2 - local SET_EXCLUDE = 3 - local SET_FNAME = 4 - local SET_REPEAT = 5 - - if cmdLine == nil then - return result - end - - local function parseOption( option ) - if option == '--help' or option == '-h' then - result['help'] = true - return - elseif option == '--version' then - result['version'] = true - return - elseif option == '--verbose' or option == '-v' then - result['verbosity'] = M.VERBOSITY_VERBOSE - return - elseif option == '--quiet' or option == '-q' then - result['verbosity'] = M.VERBOSITY_QUIET - return - elseif option == '--error' or option == '-e' then - result['quitOnError'] = true - return - elseif option == '--failure' or option == '-f' then - result['quitOnFailure'] = true - return - elseif option == '--shuffle' or option == '-s' then - result['shuffle'] = true - return - elseif option == '--output' or option == '-o' then - state = SET_OUTPUT - return state - elseif option == '--name' or option == '-n' then - state = SET_FNAME - return state - elseif option == '--repeat' or option == '-r' then - state = SET_REPEAT - return state - elseif option == '--pattern' or option == '-p' then - state = SET_PATTERN - return state - elseif option == '--exclude' or option == '-x' then - state = SET_EXCLUDE - return state - end - error('Unknown option: '..option,3) - end - - local function setArg( cmdArg, state ) - if state == SET_OUTPUT then - result['output'] = cmdArg - return - elseif state == SET_FNAME then - result['fname'] = cmdArg - return - elseif state == SET_REPEAT then - result['exeRepeat'] = tonumber(cmdArg) - or error('Malformed -r argument: '..cmdArg) - return - elseif state == SET_PATTERN then - if result['pattern'] then - table.insert( result['pattern'], cmdArg ) - else - result['pattern'] = { cmdArg } - end - return - elseif state == SET_EXCLUDE then - local notArg = '!'..cmdArg - if result['pattern'] then - table.insert( result['pattern'], notArg ) - else - result['pattern'] = { notArg } - end - return - end - error('Unknown parse state: '.. state) - end - - - for i, cmdArg in ipairs(cmdLine) do - if state ~= nil then - setArg( cmdArg, state, result ) - state = nil - else - if cmdArg:sub(1,1) == '-' then - state = parseOption( cmdArg ) - else - if result['testNames'] then - table.insert( result['testNames'], cmdArg ) - else - result['testNames'] = { cmdArg } - end - end - end - end - - if result['help'] then - M.LuaUnit.help() - end - - if result['version'] then - M.LuaUnit.version() - end - - if state ~= nil then - error('Missing argument after '..cmdLine[ #cmdLine ],2 ) - end + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + if cmdLine == nil then return result end - function M.LuaUnit.help() - print(M.USAGE) - os.exit(0) + local function parseOption(option) + if option == "--help" or option == "-h" then + result["help"] = true + return + elseif option == "--version" then + result["version"] = true + return + elseif option == "--verbose" or option == "-v" then + result["verbosity"] = M.VERBOSITY_VERBOSE + return + elseif option == "--quiet" or option == "-q" then + result["verbosity"] = M.VERBOSITY_QUIET + return + elseif option == "--error" or option == "-e" then + result["quitOnError"] = true + return + elseif option == "--failure" or option == "-f" then + result["quitOnFailure"] = true + return + elseif option == "--shuffle" or option == "-s" then + result["shuffle"] = true + return + elseif option == "--output" or option == "-o" then + state = SET_OUTPUT + return state + elseif option == "--name" or option == "-n" then + state = SET_FNAME + return state + elseif option == "--repeat" or option == "-r" then + state = SET_REPEAT + return state + elseif option == "--pattern" or option == "-p" then + state = SET_PATTERN + return state + elseif option == "--exclude" or option == "-x" then + state = SET_EXCLUDE + return state + end + error("Unknown option: " .. option, 3) end - function M.LuaUnit.version() - print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') - os.exit(0) + local function setArg(cmdArg, state) + if state == SET_OUTPUT then + result["output"] = cmdArg + return + elseif state == SET_FNAME then + result["fname"] = cmdArg + return + elseif state == SET_REPEAT then + result["exeRepeat"] = tonumber(cmdArg) or error("Malformed -r argument: " .. cmdArg) + return + elseif state == SET_PATTERN then + if result["pattern"] then + table.insert(result["pattern"], cmdArg) + else + result["pattern"] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = "!" .. cmdArg + if result["pattern"] then + table.insert(result["pattern"], notArg) + else + result["pattern"] = { notArg } + end + return + end + error("Unknown parse state: " .. state) end + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg(cmdArg, state, result) + state = nil + else + if cmdArg:sub(1, 1) == "-" then + state = parseOption(cmdArg) + else + if result["testNames"] then + table.insert(result["testNames"], cmdArg) + else + result["testNames"] = { cmdArg } + end + end + end + end + + if result["help"] then + M.LuaUnit.help() + end + + if result["version"] then + M.LuaUnit.version() + end + + if state ~= nil then + error("Missing argument after " .. cmdLine[#cmdLine], 2) + end + + return result +end + +function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) +end + +function M.LuaUnit.version() + print("LuaUnit v" .. M.VERSION .. " by Philippe Fremy ") + os.exit(0) +end + ---------------------------------------------------------------- -- class NodeStatus ---------------------------------------------------------------- - local NodeStatus = { __class__ = 'NodeStatus' } -- class - local NodeStatus_MT = { __index = NodeStatus } -- metatable - M.NodeStatus = NodeStatus +local NodeStatus = { __class__ = "NodeStatus" } -- class +local NodeStatus_MT = { __index = NodeStatus } -- metatable +M.NodeStatus = NodeStatus - -- values of status - NodeStatus.SUCCESS = 'SUCCESS' - NodeStatus.SKIP = 'SKIP' - NodeStatus.FAIL = 'FAIL' - NodeStatus.ERROR = 'ERROR' +-- values of status +NodeStatus.SUCCESS = "SUCCESS" +NodeStatus.SKIP = "SKIP" +NodeStatus.FAIL = "FAIL" +NodeStatus.ERROR = "ERROR" - function NodeStatus.new( number, testName, className ) - -- default constructor, test are PASS by default - local t = { number = number, testName = testName, className = className } - setmetatable( t, NodeStatus_MT ) - t:success() - return t +function NodeStatus.new(number, testName, className) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable(t, NodeStatus_MT) + t:success() + return t +end + +function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil +end + +function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil +end + +function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace +end + +function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace +end + +function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS +end + +function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) +end + +function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP +end + +function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL +end + +function NodeStatus:isError() + return self.status == NodeStatus.ERROR +end + +function NodeStatus:statusXML() + if self:isError() then + return table.concat({ + ' \n', + " \n", + }) + elseif self:isFailure() then + return table.concat({ + ' \n', + " \n", + }) + elseif self:isSkipped() then + return table.concat({ " ", xmlEscape(self.msg), "\n" }) end + return " \n" -- (not XSD-compliant! normally shouldn't get here) +end - function NodeStatus:success() - self.status = self.SUCCESS - -- useless because lua does this for us, but it helps me remembering the relevant field names - self.msg = nil - self.stackTrace = nil +--------------[[ Output methods ]]------------------------- + +local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = "" + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == "ss") and "es" or "s" end + return string.format("%d %s%s", number, singular, suffix) +end - function NodeStatus:skip(msg) - self.status = self.SKIP - self.msg = msg - self.stackTrace = nil - end - - function NodeStatus:fail(msg, stackTrace) - self.status = self.FAIL - self.msg = msg - self.stackTrace = stackTrace - end - - function NodeStatus:error(msg, stackTrace) - self.status = self.ERROR - self.msg = msg - self.stackTrace = stackTrace - end - - function NodeStatus:isSuccess() - return self.status == NodeStatus.SUCCESS - end - - function NodeStatus:isNotSuccess() - -- Return true if node is either failure or error or skip - return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) - end - - function NodeStatus:isSkipped() - return self.status == NodeStatus.SKIP - end - - function NodeStatus:isFailure() - return self.status == NodeStatus.FAIL - end - - function NodeStatus:isError() - return self.status == NodeStatus.ERROR - end - - function NodeStatus:statusXML() - if self:isError() then - return table.concat( - {' \n', - ' \n'}) - elseif self:isFailure() then - return table.concat( - {' \n', - ' \n'}) - elseif self:isSkipped() then - return table.concat({' ', xmlEscape(self.msg),'\n' } ) +function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format("Ran %d tests in %0.3f seconds", result.runCount, result.duration), + conditional_plural(result.successCount, "success"), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, "failure")) end - return ' \n' -- (not XSD-compliant! normally shouldn't get here) - end - - --------------[[ Output methods ]]------------------------- - - local function conditional_plural(number, singular) - -- returns a grammatically well-formed string "%d " - local suffix = '' - if number ~= 1 then -- use plural - suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, "error")) end - return string.format('%d %s%s', number, singular, suffix) + else + table.insert(s, "0 failures") + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ", ") +end + +function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv("LUAUNIT_DATEFMT")), + startIsodate = os.date("%Y-%m-%dT%H:%M:%S"), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() +end + +function M.LuaUnit:startClass(className, classInstance) + self.result.currentClassName = className + self.output:startClass(className) + self:setupClass(className, classInstance) +end + +function M.LuaUnit:startTest(testName) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new(self.result.currentTestNumber, testName, self.result.currentClassName) + self.result.currentNode.startTime = os.clock() + table.insert(self.result.allTests, self.result.currentNode) + self.output:startTest(testName) +end + +function M.LuaUnit:updateStatus(err) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return end - function M.LuaUnit.statusLine(result) - -- return status line string according to results - local s = { - string.format('Ran %d tests in %0.3f seconds', - result.runCount, result.duration), - conditional_plural(result.successCount, 'success'), - } - if result.notSuccessCount > 0 then - if result.failureCount > 0 then - table.insert(s, conditional_plural(result.failureCount, 'failure')) - end - if result.errorCount > 0 then - table.insert(s, conditional_plural(result.errorCount, 'error')) - end - else - table.insert(s, '0 failures') - end - if result.skippedCount > 0 then - table.insert(s, string.format("%d skipped", result.skippedCount)) - end - if result.nonSelectedCount > 0 then - table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) - end - return table.concat(s, ', ') - end + local node = self.result.currentNode - function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) - self.result = { - selectedCount = selectedCount, - nonSelectedCount = nonSelectedCount, - successCount = 0, - runCount = 0, - currentTestNumber = 0, - currentClassName = "", - currentNode = nil, - suiteStarted = true, - startTime = os.clock(), - startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), - startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), - patternIncludeFilter = self.patternIncludeFilter, - - -- list of test node status - allTests = {}, - failedTests = {}, - errorTests = {}, - skippedTests = {}, - - failureCount = 0, - errorCount = 0, - notSuccessCount = 0, - skippedCount = 0, - } - - self.outputType = self.outputType or TextOutput - self.output = self.outputType.new(self) - self.output:startSuite() - end - - function M.LuaUnit:startClass( className, classInstance ) - self.result.currentClassName = className - self.output:startClass( className ) - self:setupClass( className, classInstance ) - end - - function M.LuaUnit:startTest( testName ) - self.result.currentTestNumber = self.result.currentTestNumber + 1 - self.result.runCount = self.result.runCount + 1 - self.result.currentNode = NodeStatus.new( - self.result.currentTestNumber, - testName, - self.result.currentClassName - ) - self.result.currentNode.startTime = os.clock() - table.insert( self.result.allTests, self.result.currentNode ) - self.output:startTest( testName ) - end - - function M.LuaUnit:updateStatus( err ) - -- "err" is expected to be a table / result from protectedCall() - if err.status == NodeStatus.SUCCESS then - return - end - - local node = self.result.currentNode - - --[[ As a first approach, we will report only one error or one failure for one test. + --[[ As a first approach, we will report only one error or one failure for one test. However, we can have the case where the test is in failure, and the teardown is in error. In such case, it's a good idea to report both a failure and an error in the test suite. This is @@ -2895,241 +3002,244 @@ end We will do this more intelligent version later. ]] - -- if the node is already in failure/error, just don't report the new error (see above) - if node.status ~= NodeStatus.SUCCESS then - return - end - - if err.status == NodeStatus.FAIL then - node:fail( err.msg, err.trace ) - table.insert( self.result.failedTests, node ) - elseif err.status == NodeStatus.ERROR then - node:error( err.msg, err.trace ) - table.insert( self.result.errorTests, node ) - elseif err.status == NodeStatus.SKIP then - node:skip( err.msg ) - table.insert( self.result.skippedTests, node ) - else - error('No such status: ' .. prettystr(err.status)) - end - - self.output:updateStatus( node ) + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return end - function M.LuaUnit:endTest() - local node = self.result.currentNode - -- print( 'endTest() '..prettystr(node)) - -- print( 'endTest() '..prettystr(node:isNotSuccess())) - node.duration = os.clock() - node.startTime - node.startTime = nil - self.output:endTest( node ) - - if node:isSuccess() then - self.result.successCount = self.result.successCount + 1 - elseif node:isError() then - if self.quitOnError or self.quitOnFailure then - -- Runtime error - abort test execution as requested by - -- "--error" option. This is done by setting a special - -- flag that gets handled in internalRunSuiteByInstances(). - print("\nERROR during LuaUnit test execution:\n" .. node.msg) - self.result.aborted = true - end - elseif node:isFailure() then - if self.quitOnFailure then - -- Failure - abort test execution as requested by - -- "--failure" option. This is done by setting a special - -- flag that gets handled in internalRunSuiteByInstances(). - print("\nFailure during LuaUnit test execution:\n" .. node.msg) - self.result.aborted = true - end - elseif node:isSkipped() then - self.result.runCount = self.result.runCount - 1 - else - error('No such node status: ' .. prettystr(node.status)) - end - self.result.currentNode = nil + if err.status == NodeStatus.FAIL then + node:fail(err.msg, err.trace) + table.insert(self.result.failedTests, node) + elseif err.status == NodeStatus.ERROR then + node:error(err.msg, err.trace) + table.insert(self.result.errorTests, node) + elseif err.status == NodeStatus.SKIP then + node:skip(err.msg) + table.insert(self.result.skippedTests, node) + else + error("No such status: " .. prettystr(err.status)) end - function M.LuaUnit:endClass() - self:teardownClass( self.lastClassName, self.lastClassInstance ) - self.output:endClass() + self.output:updateStatus(node) +end + +function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest(node) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error("No such node status: " .. prettystr(node.status)) + end + self.result.currentNode = nil +end + +function M.LuaUnit:endClass() + self:teardownClass(self.lastClassName, self.lastClassInstance) + self.output:endClass() +end + +function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error("LuaUnit:endSuite() -- suite was already ended") + end + self.result.duration = os.clock() - self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() +end + +function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error("No such format: " .. outputType, 2) +end + +--------------[[ Runner ]]----------------- + +function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2), + } end - function M.LuaUnit:endSuite() - if self.result.suiteStarted == false then - error('LuaUnit:endSuite() -- suite was already ended' ) - end - self.result.duration = os.clock()-self.result.startTime - self.result.suiteStarted = false + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall(function() + methodInstance(classInstance) + end, err_handler) + else + ok, err = xpcall(function() + methodInstance() + end, err_handler) + end + if ok then + return { status = NodeStatus.SUCCESS } + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') - -- Expose test counts for outputter's endSuite(). This could be managed - -- internally instead by using the length of the lists of failed tests - -- but unit tests rely on these fields being present. - self.result.failureCount = #self.result.failedTests - self.result.errorCount = #self.result.errorTests - self.result.notSuccessCount = self.result.failureCount + self.result.errorCount - self.result.skippedCount = #self.result.skippedTests + local iter_msg + iter_msg = self.exeRepeat and "iteration " .. self.currentCount - self.output:endSuite() + err.msg, err.status = M.adjust_err_msg_with_iter(err.msg, iter_msg) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err end - function M.LuaUnit:setOutputType(outputType, fname) - -- Configures LuaUnit runner output - -- outputType is one of: NIL, TAP, JUNIT, TEXT - -- when outputType is junit, the additional argument fname is used to set the name of junit output file - -- for other formats, fname is ignored - if outputType:upper() == "NIL" then - self.outputType = NilOutput - return - end - if outputType:upper() == "TAP" then - self.outputType = TapOutput - return - end - if outputType:upper() == "JUNIT" then - self.outputType = JUnitOutput - if fname then - self.fname = fname - end - return - end - if outputType:upper() == "TEXT" then - self.outputType = TextOutput - return - end - error( 'No such format: '..outputType,2) + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '" .. prettyFuncName .. "'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) end - --------------[[ Runner ]]----------------- + return err -- return the error "object" (table) +end - function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) - -- if classInstance is nil, this is just a function call - -- else, it's method of a class being called. +function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set - local function err_handler(e) - -- transform error into a table, adding the traceback information - return { - status = NodeStatus.ERROR, - msg = e, - trace = string.sub(debug.traceback("", 1), 2) - } + if type(methodInstance) ~= "function" then + self:unregisterSuite() + error(tostring(methodName) .. " must be a function, not " .. type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = "[TestFunctions]" + prettyFuncName = methodName + else + prettyFuncName = className .. "." .. methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() end + self:startClass(className, classInstance) + self.lastClassName = className + self.lastClassInstance = classInstance + end - local ok, err + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) if classInstance then - -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround - ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) - else - ok, err = xpcall( function () methodInstance() end, err_handler ) - end - if ok then - return {status = NodeStatus.SUCCESS} - end - -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') - - local iter_msg - iter_msg = self.exeRepeat and 'iteration '..self.currentCount - - err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) - - if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then - err.trace = nil - return err + local func = self.asFunction(classInstance.setUp) + or self.asFunction(classInstance.Setup) + or self.asFunction(classInstance.setup) + or self.asFunction(classInstance.SetUp) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className .. ".setUp")) + end end - -- reformat / improve the stack trace - if prettyFuncName then -- we do have the real method name - err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") - end - if STRIP_LUAUNIT_FROM_STACKTRACE then - err.trace = stripLuaunitTrace2(err.trace, err.msg) + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) end - return err -- return the error "object" (table) + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction(classInstance.tearDown) + or self.asFunction(classInstance.TearDown) + or self.asFunction(classInstance.teardown) + or self.asFunction(classInstance.Teardown) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className .. ".tearDown")) + end + end end + self:endTest() +end - function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) - -- When executing a test function, className and classInstance must be nil - -- When executing a class method, all parameters must be set - - if type(methodInstance) ~= 'function' then - self:unregisterSuite() - error( tostring(methodName)..' must be a function, not '..type(methodInstance)) - end - - local prettyFuncName - if className == nil then - className = '[TestFunctions]' - prettyFuncName = methodName - else - prettyFuncName = className..'.'..methodName - end - - if self.lastClassName ~= className then - if self.lastClassName ~= nil then - self:endClass() - end - self:startClass( className, classInstance ) - self.lastClassName = className - self.lastClassInstance = classInstance - end - - self:startTest(prettyFuncName) - - local node = self.result.currentNode - for iter_n = 1, self.exeRepeat or 1 do - if node:isNotSuccess() then - break - end - self.currentCount = iter_n - - -- run setUp first (if any) - if classInstance then - local func = self.asFunction( classInstance.setUp ) or - self.asFunction( classInstance.Setup ) or - self.asFunction( classInstance.setup ) or - self.asFunction( classInstance.SetUp ) - if func then - self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) - end - end - - -- run testMethod() - if node:isSuccess() then - self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) - end - - -- lastly, run tearDown (if any) - if classInstance then - local func = self.asFunction( classInstance.tearDown ) or - self.asFunction( classInstance.TearDown ) or - self.asFunction( classInstance.teardown ) or - self.asFunction( classInstance.Teardown ) - if func then - self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) - end - end - end - - self:endTest() - end - - function M.LuaUnit.expandOneClass( result, className, classInstance ) - --[[ +function M.LuaUnit.expandOneClass(result, className, classInstance) + --[[ Input: a list of { name, instance }, a class name, a class instance Ouptut: modify result to add all test method instance in the form: { className.methodName, classInstance } ]] - for methodName, methodInstance in sortedPairs(classInstance) do - if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then - table.insert( result, { className..'.'..methodName, classInstance } ) - end + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName(methodName) then + table.insert(result, { className .. "." .. methodName, classInstance }) end end +end - function M.LuaUnit.expandClasses( listOfNameAndInst ) - --[[ +function M.LuaUnit.expandClasses(listOfNameAndInst) + --[[ -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} -- functions and methods remain untouched @@ -3140,87 +3250,97 @@ end * { class.method name, class instance }: do nothing * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) ]] - local result = {} + local result = {} - for i,v in ipairs( listOfNameAndInst ) do - local name, instance = v[1], v[2] - if M.LuaUnit.asFunction(instance) then - table.insert( result, { name, instance } ) - else - if type(instance) ~= 'table' then - error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + for i, v in ipairs(listOfNameAndInst) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert(result, { name, instance }) + else + if type(instance) ~= "table" then + error( + "Instance must be a table or a function, not a " + .. type(instance) + .. " with value " + .. prettystr(instance) + ) + end + local className, methodName = M.LuaUnit.splitClassMethod(name) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( + "Could not find method in class " + .. tostring(className) + .. " for method " + .. tostring(methodName) + ) end - local className, methodName = M.LuaUnit.splitClassMethod( name ) - if className then - local methodInstance = instance[methodName] - if methodInstance == nil then - error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) - end - table.insert( result, { name, instance } ) - else - M.LuaUnit.expandOneClass( result, name, instance ) - end - end - end - - return result - end - - function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) - local included, excluded = {}, {} - for i, v in ipairs( listOfNameAndInst ) do - -- local name, instance = v[1], v[2] - if patternFilter( patternIncFilter, v[1] ) then - table.insert( included, v ) + table.insert(result, { name, instance }) else - table.insert( excluded, v ) + M.LuaUnit.expandOneClass(result, name, instance) end end - return included, excluded end - local function getKeyInListWithGlobalFallback( key, listOfNameAndInst ) - local result = nil - for i,v in ipairs( listOfNameAndInst ) do - if(listOfNameAndInst[i][1] == key) then - result = listOfNameAndInst[i][2] - break - end - end - if(not M.LuaUnit.asFunction( result ) ) then - result = _G[key] - end - return result - end + return result +end - function M.LuaUnit:setupSuite( listOfNameAndInst ) - local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) - if self.asFunction( setupSuite ) then - self:updateStatus( self:protectedCall( nil, setupSuite, 'setupSuite' ) ) +function M.LuaUnit.applyPatternFilter(patternIncFilter, listOfNameAndInst) + local included, excluded = {}, {} + for i, v in ipairs(listOfNameAndInst) do + -- local name, instance = v[1], v[2] + if patternFilter(patternIncFilter, v[1]) then + table.insert(included, v) + else + table.insert(excluded, v) end end + return included, excluded +end - function M.LuaUnit:teardownSuite(listOfNameAndInst) - local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) - if self.asFunction( teardownSuite ) then - self:updateStatus( self:protectedCall( nil, teardownSuite, 'teardownSuite') ) +local function getKeyInListWithGlobalFallback(key, listOfNameAndInst) + local result = nil + for i, v in ipairs(listOfNameAndInst) do + if listOfNameAndInst[i][1] == key then + result = listOfNameAndInst[i][2] + break end end - - function M.LuaUnit:setupClass( className, instance ) - if type( instance ) == 'table' and self.asFunction( instance.setupClass ) then - self:updateStatus( self:protectedCall( instance, instance.setupClass, className..'.setupClass' ) ) - end + if not M.LuaUnit.asFunction(result) then + result = _G[key] end + return result +end - function M.LuaUnit:teardownClass( className, instance ) - if type( instance ) == 'table' and self.asFunction( instance.teardownClass ) then - self:updateStatus( self:protectedCall( instance, instance.teardownClass, className..'.teardownClass' ) ) - end +function M.LuaUnit:setupSuite(listOfNameAndInst) + local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) + if self.asFunction(setupSuite) then + self:updateStatus(self:protectedCall(nil, setupSuite, "setupSuite")) end +end - function M.LuaUnit:internalRunSuiteByInstances( listOfNameAndInst ) - --[[ Run an explicit list of tests. Each item of the list must be one of: +function M.LuaUnit:teardownSuite(listOfNameAndInst) + local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) + if self.asFunction(teardownSuite) then + self:updateStatus(self:protectedCall(nil, teardownSuite, "teardownSuite")) + end +end + +function M.LuaUnit:setupClass(className, instance) + if type(instance) == "table" and self.asFunction(instance.setupClass) then + self:updateStatus(self:protectedCall(instance, instance.setupClass, className .. ".setupClass")) + end +end + +function M.LuaUnit:teardownClass(className, instance) + if type(instance) == "table" and self.asFunction(instance.teardownClass) then + self:updateStatus(self:protectedCall(instance, instance.teardownClass, className .. ".teardownClass")) + end +end + +function M.LuaUnit:internalRunSuiteByInstances(listOfNameAndInst) + --[[ Run an explicit list of tests. Each item of the list must be one of: * { function name, function instance } * { class name, class instance } * { class.method name, class instance } @@ -3228,196 +3348,195 @@ end This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances() ]] - local expandedList = self.expandClasses( listOfNameAndInst ) - if self.shuffle then - randomizeTable( expandedList ) + local expandedList = self.expandClasses(listOfNameAndInst) + if self.shuffle then + randomizeTable(expandedList) + end + local filteredList, filteredOutList = self.applyPatternFilter(self.patternIncludeFilter, expandedList) + + self:startSuite(#filteredList, #filteredOutList) + self:setupSuite(listOfNameAndInst) + + for i, v in ipairs(filteredList) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction(nil, name, nil, instance) + else + -- expandClasses() should have already taken care of sanitizing the input + assert(type(instance) == "table") + local className, methodName = M.LuaUnit.splitClassMethod(name) + assert(className ~= nil) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction(className, methodName, instance, methodInstance) end - local filteredList, filteredOutList = self.applyPatternFilter( - self.patternIncludeFilter, expandedList ) - - self:startSuite( #filteredList, #filteredOutList ) - self:setupSuite( listOfNameAndInst ) - - for i,v in ipairs( filteredList ) do - local name, instance = v[1], v[2] - if M.LuaUnit.asFunction(instance) then - self:execOneFunction( nil, name, nil, instance ) - else - -- expandClasses() should have already taken care of sanitizing the input - assert( type(instance) == 'table' ) - local className, methodName = M.LuaUnit.splitClassMethod( name ) - assert( className ~= nil ) - local methodInstance = instance[methodName] - assert(methodInstance ~= nil) - self:execOneFunction( className, methodName, instance, methodInstance ) - end - if self.result.aborted then - break -- "--error" or "--failure" option triggered - end - end - - if self.lastClassName ~= nil then - self:endClass() - end - - self:teardownSuite( listOfNameAndInst ) - self:endSuite() - if self.result.aborted then - print("LuaUnit ABORTED (as requested by --error or --failure option)") - self:unregisterSuite() - os.exit(-2) + break -- "--error" or "--failure" option triggered end end - function M.LuaUnit:internalRunSuiteByNames( listOfName ) - --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + if self.lastClassName ~= nil then + self:endClass() + end + + self:teardownSuite(listOfNameAndInst) + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + self:unregisterSuite() + os.exit(-2) + end +end + +function M.LuaUnit:internalRunSuiteByNames(listOfName) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global namespace analysis. Convert the list into a list of (name, valid instances (table or function)) and calls internalRunSuiteByInstances. ]] - local instanceName, instance - local listOfNameAndInst = {} + local instanceName, instance + local listOfNameAndInst = {} - for i,name in ipairs( listOfName ) do - local className, methodName = M.LuaUnit.splitClassMethod( name ) - if className then - instanceName = className - instance = _G[instanceName] - - if instance == nil then - self:unregisterSuite() - error( "No such name in global space: "..instanceName ) - end - - if type(instance) ~= 'table' then - self:unregisterSuite() - error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) - end - - local methodInstance = instance[methodName] - if methodInstance == nil then - self:unregisterSuite() - error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) - end - - else - -- for functions and classes - instanceName = name - instance = _G[instanceName] - end + for i, name in ipairs(listOfName) do + local className, methodName = M.LuaUnit.splitClassMethod(name) + if className then + instanceName = className + instance = _G[instanceName] if instance == nil then self:unregisterSuite() - error( "No such name in global space: "..instanceName ) + error("No such name in global space: " .. instanceName) end - if (type(instance) ~= 'table' and type(instance) ~= 'function') then + if type(instance) ~= "table" then self:unregisterSuite() - error( 'Name must match a function or a table: '..instanceName ) + error("Instance of " .. instanceName .. " must be a table, not " .. type(instance)) end - table.insert( listOfNameAndInst, { name, instance } ) - end - - self:internalRunSuiteByInstances( listOfNameAndInst ) - end - - function M.LuaUnit.run(...) - -- Run some specific test classes. - -- If no arguments are passed, run the class names specified on the - -- command line. If no class name is specified on the command line - -- run all classes whose name starts with 'Test' - -- - -- If arguments are passed, they must be strings of the class names - -- that you want to run or generic command line arguments (-o, -p, -v, ...) - local runner = M.LuaUnit.new() - return runner:runSuite(...) - end - - function M.LuaUnit:registerSuite() - -- register the current instance into our global array of instances - -- print('-> Register suite') - M.LuaUnit.instances[ #M.LuaUnit.instances+1 ] = self - end - - function M.unregisterCurrentSuite() - -- force unregister the last registered suite - table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) - end - - function M.LuaUnit:unregisterSuite() - -- print('<- Unregister suite') - -- remove our current instqances from the global array of instances - local instanceIdx = nil - for i, instance in ipairs(M.LuaUnit.instances) do - if instance == self then - instanceIdx = i - break + local methodInstance = instance[methodName] + if methodInstance == nil then + self:unregisterSuite() + error( + "Could not find method in class " .. tostring(className) .. " for method " .. tostring(methodName) + ) end + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] end - if instanceIdx ~= nil then - table.remove(M.LuaUnit.instances, instanceIdx) - -- print('Unregister done') + if instance == nil then + self:unregisterSuite() + error("No such name in global space: " .. instanceName) end + if type(instance) ~= "table" and type(instance) ~= "function" then + self:unregisterSuite() + error("Name must match a function or a table: " .. instanceName) + end + + table.insert(listOfNameAndInst, { name, instance }) end - function M.LuaUnit:initFromArguments( ... ) - --[[Parses all arguments from either command-line or direct call and set internal + self:internalRunSuiteByInstances(listOfNameAndInst) +end + +function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + local runner = M.LuaUnit.new() + return runner:runSuite(...) +end + +function M.LuaUnit:registerSuite() + -- register the current instance into our global array of instances + -- print('-> Register suite') + M.LuaUnit.instances[#M.LuaUnit.instances + 1] = self +end + +function M.unregisterCurrentSuite() + -- force unregister the last registered suite + table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) +end + +function M.LuaUnit:unregisterSuite() + -- print('<- Unregister suite') + -- remove our current instqances from the global array of instances + local instanceIdx = nil + for i, instance in ipairs(M.LuaUnit.instances) do + if instance == self then + instanceIdx = i + break + end + end + + if instanceIdx ~= nil then + table.remove(M.LuaUnit.instances, instanceIdx) + -- print('Unregister done') + end +end + +function M.LuaUnit:initFromArguments(...) + --[[Parses all arguments from either command-line or direct call and set internal flags of LuaUnit runner according to it. Return the list of names which were possibly passed on the command-line or as arguments ]] - local args = {...} - if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then - -- run was called with the syntax M.LuaUnit:runSuite() - -- we support both M.LuaUnit.run() and M.LuaUnit:run() - -- strip out the first argument self to make it a command-line argument list - table.remove(args,1) - end - - if #args == 0 then - args = cmdline_argv - end - - local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) - - -- We expect these option fields to be either `nil` or contain - -- valid values, so it's safe to always copy them directly. - self.verbosity = options.verbosity - self.quitOnError = options.quitOnError - self.quitOnFailure = options.quitOnFailure - - self.exeRepeat = options.exeRepeat - self.patternIncludeFilter = options.pattern - self.shuffle = options.shuffle - - options.output = options.output or os.getenv('LUAUNIT_OUTPUT') - options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME') - - if options.output then - if options.output:lower() == 'junit' and options.fname == nil then - print('With junit output, a filename must be supplied with -n or --name') - os.exit(-1) - end - pcall_or_abort(self.setOutputType, self, options.output, options.fname) - end - - return options.testNames + local args = { ... } + if type(args[1]) == "table" and args[1].__class__ == "LuaUnit" then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument self to make it a command-line argument list + table.remove(args, 1) end - function M.LuaUnit:runSuite( ... ) - local testNames = self:initFromArguments(...) - self:registerSuite() - self:internalRunSuiteByNames( testNames or M.LuaUnit.collectTests() ) - self:unregisterSuite() - return self.result.notSuccessCount + if #args == 0 then + args = cmdline_argv end - function M.LuaUnit:runSuiteByInstances( listOfNameAndInst, commandLineArguments ) - --[[ + local options = pcall_or_abort(M.LuaUnit.parseCmdLine, args) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + options.output = options.output or os.getenv("LUAUNIT_OUTPUT") + options.fname = options.fname or os.getenv("LUAUNIT_JUNIT_FNAME") + + if options.output then + if options.output:lower() == "junit" and options.fname == nil then + print("With junit output, a filename must be supplied with -n or --name") + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + return options.testNames +end + +function M.LuaUnit:runSuite(...) + local testNames = self:initFromArguments(...) + self:registerSuite() + self:internalRunSuiteByNames(testNames or M.LuaUnit.collectTests()) + self:unregisterSuite() + return self.result.notSuccessCount +end + +function M.LuaUnit:runSuiteByInstances(listOfNameAndInst, commandLineArguments) + --[[ Run all test functions or tables provided as input. Input: a list of { name, instance } @@ -3425,15 +3544,13 @@ end return the number of failures and errors, 0 meaning success ]] - -- parse the command-line arguments - local testNames = self:initFromArguments( commandLineArguments ) - self:registerSuite() - self:internalRunSuiteByInstances( listOfNameAndInst ) - self:unregisterSuite() - return self.result.notSuccessCount - end - - + -- parse the command-line arguments + local testNames = self:initFromArguments(commandLineArguments) + self:registerSuite() + self:internalRunSuiteByInstances(listOfNameAndInst) + self:unregisterSuite() + return self.result.notSuccessCount +end -- class LuaUnit @@ -3441,12 +3558,11 @@ end M.run = M.LuaUnit.run M.Run = M.LuaUnit.run -function M:setVerbosity( verbosity ) +function M:setVerbosity(verbosity) -- set the verbosity value (as integer) M.LuaUnit.verbosity = verbosity end M.set_verbosity = M.setVerbosity M.SetVerbosity = M.setVerbosity - -return M \ No newline at end of file +return M diff --git a/lib/preprocess-cl.lua b/lib/preprocess-cl.lua index c4cae39..b862461 100644 --- a/lib/preprocess-cl.lua +++ b/lib/preprocess-cl.lua @@ -1,7 +1,7 @@ #!/bin/sh -_=[[ +_ = [[ exec lua "$0" "$@" -]]and nil +]] and nil --============================================================== --= --= LuaPreprocess command line program @@ -175,35 +175,35 @@ Handler messages: ]] --============================================================== - - -local startTime = os.time() +local startTime = os.time() local startClock = os.clock() local args = arg -if not args[0] then error("Expected to run from the Lua interpreter.") end +if not args[0] then + error("Expected to run from the Lua interpreter.") +end local pp = dofile((args[0]:gsub("[^/\\]+$", "preprocess.lua"))) -- From args: -local addLineNumbers = false +local addLineNumbers = false local allowBacktickStrings = false -local allowJitSyntax = false -local canOutputNil = true -local customData = nil -local fastStrings = false -local hasOutputExtension = false -local hasOutputPaths = false -local isDebug = false -local outputExtension = "lua" -local outputMeta = false -- flag|path -local processingInfoPath = "" -local silent = false -local validate = true -local macroPrefix = "" -local macroSuffix = "" -local releaseMode = false -local maxLogLevel = "trace" +local allowJitSyntax = false +local canOutputNil = true +local customData = nil +local fastStrings = false +local hasOutputExtension = false +local hasOutputPaths = false +local isDebug = false +local outputExtension = "lua" +local outputMeta = false -- flag|path +local processingInfoPath = "" +local silent = false +local validate = true +local macroPrefix = "" +local macroSuffix = "" +local releaseMode = false +local maxLogLevel = "trace" local strictMacroArguments = true --============================================================== @@ -212,46 +212,50 @@ local strictMacroArguments = true local F = string.format local function formatBytes(n) - if n >= 1024*1024*1024 then - return F("%.2f GiB", n/(1024*1024*1024)) - elseif n >= 1024*1024 then - return F("%.2f MiB", n/(1024*1024)) - elseif n >= 1024 then - return F("%.2f KiB", n/(1024)) - elseif n == 1 then - return F("1 byte", n) - else - return F("%d bytes", n) - end + if n >= 1024 * 1024 * 1024 then + return F("%.2f GiB", n / (1024 * 1024 * 1024)) + elseif n >= 1024 * 1024 then + return F("%.2f MiB", n / (1024 * 1024)) + elseif n >= 1024 then + return F("%.2f KiB", n / 1024) + elseif n == 1 then + return F("1 byte", n) + else + return F("%d bytes", n) + end end local function printfNoise(s, ...) - print(s:format(...)) + print(s:format(...)) end local function printError(s) - io.stderr:write(s, "\n") + io.stderr:write(s, "\n") end local function printfError(s, ...) - io.stderr:write(s:format(...), "\n") + io.stderr:write(s:format(...), "\n") end local function errorLine(err) - printError(pp.tryToFormatError(err)) - os.exit(1) + printError(pp.tryToFormatError(err)) + os.exit(1) 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 + (_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 + if env then + setfenv(chunk, env) + end - return chunk - end + return chunk + end ) --============================================================== @@ -264,367 +268,339 @@ io.stderr:setvbuf("no") math.randomseed(os.time()) -- In case math.random() is used anywhere. math.random() -- Must kickstart... -local processOptions = true +local processOptions = true local messageHandlerPath = "" -local pathsIn = {} -local pathsOut = {} +local pathsIn = {} +local pathsOut = {} for _, arg in ipairs(args) do - if processOptions and (arg:find"^%-%-?help$" or arg == "/?" or arg:find"^/[Hh][Ee][Ll][Pp]$") then - print("LuaPreprocess v"..pp.VERSION) - print((help:gsub("\t", " "))) - os.exit() + if processOptions and (arg:find("^%-%-?help$") or arg == "/?" or arg:find("^/[Hh][Ee][Ll][Pp]$")) then + print("LuaPreprocess v" .. pp.VERSION) + print((help:gsub("\t", " "))) + os.exit() + elseif not (processOptions and arg:find("^%-.")) then + local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn + table.insert(paths, arg) - elseif not (processOptions and arg:find"^%-.") then - local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn - table.insert(paths, arg) - - if arg == "-" and (not hasOutputPaths or paths == pathsOut) then - silent = true - end - - elseif arg == "--" then - processOptions = false - - elseif arg:find"^%-%-data=" or arg:find"^%-d=" then - customData = arg:gsub("^.-=", "") - - elseif arg == "--backtickstrings" then - allowBacktickStrings = true - - elseif arg == "--debug" then - isDebug = true - outputMeta = outputMeta or true - - elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then - messageHandlerPath = arg:gsub("^.-=", "") - - elseif arg == "--jitsyntax" then - allowJitSyntax = true - - elseif arg == "--linenumbers" then - addLineNumbers = true - - elseif arg == "--meta" then - outputMeta = true - elseif arg:find"^%-%-meta=" then - outputMeta = arg:gsub("^.-=", "") - - elseif arg == "--nonil" then - canOutputNil = false - - elseif arg == "--novalidate" then - validate = false - - elseif arg:find"^%-%-outputextension=" then - if hasOutputPaths then - errorLine("Cannot specify both --outputextension and --outputpaths") - end - hasOutputExtension = true - outputExtension = arg:gsub("^.-=", "") - - elseif arg == "--outputpaths" or arg == "-o" then - if hasOutputExtension then - errorLine("Cannot specify both --outputpaths and --outputextension") - elseif pathsIn[1] then - errorLine(arg.." must appear before any input path.") - end - hasOutputPaths = true - - elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then - processingInfoPath = arg:gsub("^.-=", "") - - elseif arg == "--silent" then - silent = true - - elseif arg == "--faststrings" then - fastStrings = true - - elseif arg == "--nogc" then - collectgarbage("stop") - - elseif arg:find"^%-%-macroprefix=" then - macroPrefix = arg:gsub("^.-=", "") - - elseif arg:find"^%-%-macrosuffix=" then - macroSuffix = arg:gsub("^.-=", "") - - elseif arg == "--release" then - releaseMode = true - - elseif arg:find"^%-%-loglevel=" then - maxLogLevel = arg:gsub("^.-=", "") - - elseif arg == "--version" then - io.stdout:write(pp.VERSION) - os.exit() - - elseif arg == "--nostrictmacroarguments" then - strictMacroArguments = false - - else - errorLine("Unknown option '"..arg:gsub("=.*", "").."'.") - end + if arg == "-" and (not hasOutputPaths or paths == pathsOut) then + silent = true + end + elseif arg == "--" then + processOptions = false + elseif arg:find("^%-%-data=") or arg:find("^%-d=") then + customData = arg:gsub("^.-=", "") + elseif arg == "--backtickstrings" then + allowBacktickStrings = true + elseif arg == "--debug" then + isDebug = true + outputMeta = outputMeta or true + elseif arg:find("^%-%-handler=") or arg:find("^%-h=") then + messageHandlerPath = arg:gsub("^.-=", "") + elseif arg == "--jitsyntax" then + allowJitSyntax = true + elseif arg == "--linenumbers" then + addLineNumbers = true + elseif arg == "--meta" then + outputMeta = true + elseif arg:find("^%-%-meta=") then + outputMeta = arg:gsub("^.-=", "") + elseif arg == "--nonil" then + canOutputNil = false + elseif arg == "--novalidate" then + validate = false + elseif arg:find("^%-%-outputextension=") then + if hasOutputPaths then + errorLine("Cannot specify both --outputextension and --outputpaths") + end + hasOutputExtension = true + outputExtension = arg:gsub("^.-=", "") + elseif arg == "--outputpaths" or arg == "-o" then + if hasOutputExtension then + errorLine("Cannot specify both --outputpaths and --outputextension") + elseif pathsIn[1] then + errorLine(arg .. " must appear before any input path.") + end + hasOutputPaths = true + elseif arg:find("^%-%-saveinfo=") or arg:find("^%-i=") then + processingInfoPath = arg:gsub("^.-=", "") + elseif arg == "--silent" then + silent = true + elseif arg == "--faststrings" then + fastStrings = true + elseif arg == "--nogc" then + collectgarbage("stop") + elseif arg:find("^%-%-macroprefix=") then + macroPrefix = arg:gsub("^.-=", "") + elseif arg:find("^%-%-macrosuffix=") then + macroSuffix = arg:gsub("^.-=", "") + elseif arg == "--release" then + releaseMode = true + elseif arg:find("^%-%-loglevel=") then + maxLogLevel = arg:gsub("^.-=", "") + elseif arg == "--version" then + io.stdout:write(pp.VERSION) + os.exit() + elseif arg == "--nostrictmacroarguments" then + strictMacroArguments = false + else + errorLine("Unknown option '" .. arg:gsub("=.*", "") .. "'.") + end end if silent then - printfNoise = function()end + printfNoise = function() end end -local header = "= LuaPreprocess v"..pp.VERSION..os.date(", %Y-%m-%d %H:%M:%S =", startTime) +local header = "= LuaPreprocess v" .. pp.VERSION .. os.date(", %Y-%m-%d %H:%M:%S =", startTime) printfNoise(("="):rep(#header)) printfNoise("%s", header) printfNoise(("="):rep(#header)) if hasOutputPaths and #pathsOut < #pathsIn then - errorLine("Missing output path for "..pathsIn[#pathsIn]) + errorLine("Missing output path for " .. pathsIn[#pathsIn]) end - - -- Prepare metaEnvironment. pp.metaEnvironment.dataFromCommandLine = customData -- May be nil. - - -- Load message handler. local messageHandler = nil local function hasMessageHandler(message) - if not messageHandler then - return false - - elseif type(messageHandler) == "function" then - return true - - elseif type(messageHandler) == "table" then - return messageHandler[message] ~= nil - - else - assert(false) - end + if not messageHandler then + return false + elseif type(messageHandler) == "function" then + return true + elseif type(messageHandler) == "table" then + return messageHandler[message] ~= nil + else + assert(false) + end end local function sendMessage(message, ...) - if not messageHandler then - return + if not messageHandler then + return + elseif type(messageHandler) == "function" then + local returnValues = pp.pack(messageHandler(message, ...)) + return pp.unpack(returnValues, 1, returnValues.n) + elseif type(messageHandler) == "table" then + local _messageHandler = messageHandler[message] + if not _messageHandler then + return + end - elseif type(messageHandler) == "function" then - local returnValues = pp.pack(messageHandler(message, ...)) - return pp.unpack(returnValues, 1, returnValues.n) - - elseif type(messageHandler) == "table" then - local _messageHandler = messageHandler[message] - if not _messageHandler then return end - - local returnValues = pp.pack(_messageHandler(...)) - return pp.unpack(returnValues, 1, returnValues.n) - - else - assert(false) - end + local returnValues = pp.pack(_messageHandler(...)) + return pp.unpack(returnValues, 1, returnValues.n) + else + assert(false) + end end if messageHandlerPath ~= "" then - -- Make the message handler and the metaprogram share the same environment. - -- This way the message handler can easily define globals that the metaprogram uses. - local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment) - if not mainChunk then - errorLine("Could not load message handler...\n"..pp.tryToFormatError(err)) - end + -- Make the message handler and the metaprogram share the same environment. + -- This way the message handler can easily define globals that the metaprogram uses. + local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment) + if not mainChunk then + errorLine("Could not load message handler...\n" .. pp.tryToFormatError(err)) + end - messageHandler = mainChunk() + messageHandler = mainChunk() - if type(messageHandler) == "function" then - -- void - elseif type(messageHandler) == "table" then - for message, _messageHandler in pairs(messageHandler) do - if type(message) ~= "string" then - errorLine(messageHandlerPath..": Table of handlers must only contain messages as keys.") - elseif type(_messageHandler) ~= "function" then - errorLine(messageHandlerPath..": Table of handlers must only contain functions as values.") - end - end - else - errorLine(messageHandlerPath..": File did not return a table or a function.") - end + if type(messageHandler) == "function" then + -- void + elseif type(messageHandler) == "table" then + for message, _messageHandler in pairs(messageHandler) do + if type(message) ~= "string" then + errorLine(messageHandlerPath .. ": Table of handlers must only contain messages as keys.") + elseif type(_messageHandler) ~= "function" then + errorLine(messageHandlerPath .. ": Table of handlers must only contain functions as values.") + end + end + else + errorLine(messageHandlerPath .. ": File did not return a table or a function.") + end end - - -- Init stuff. sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better? if not hasOutputPaths then - for i, pathIn in ipairs(pathsIn) do - pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "").."."..outputExtension - end + for i, pathIn in ipairs(pathsIn) do + pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "") .. "." .. outputExtension + end end if not pathsIn[1] then - errorLine("No path(s) specified.") + errorLine("No path(s) specified.") elseif #pathsIn ~= #pathsOut then - errorLine(F("Number of input and output paths differ. (%d in, %d out)", #pathsIn, #pathsOut)) + errorLine(F("Number of input and output paths differ. (%d in, %d out)", #pathsIn, #pathsOut)) end -local pathsSetIn = {} +local pathsSetIn = {} local pathsSetOut = {} for i = 1, #pathsIn do - if pathsSetIn [pathsIn [i]] then errorLine("Duplicate input path: " ..pathsIn [i]) end - if pathsSetOut[pathsOut[i]] then errorLine("Duplicate output path: "..pathsOut[i]) end + if pathsSetIn[pathsIn[i]] then + errorLine("Duplicate input path: " .. pathsIn[i]) + end + if pathsSetOut[pathsOut[i]] then + errorLine("Duplicate output path: " .. pathsOut[i]) + end - pathsSetIn [pathsIn [i]] = true - pathsSetOut[pathsOut[i]] = true + pathsSetIn[pathsIn[i]] = true + pathsSetOut[pathsOut[i]] = true - if pathsIn [i] ~= "-" and pathsSetOut[pathsIn [i]] then errorLine("Path is both input and output: "..pathsIn [i]) end - if pathsOut[i] ~= "-" and pathsSetIn [pathsOut[i]] then errorLine("Path is both input and output: "..pathsOut[i]) end + if pathsIn[i] ~= "-" and pathsSetOut[pathsIn[i]] then + errorLine("Path is both input and output: " .. pathsIn[i]) + end + if pathsOut[i] ~= "-" and pathsSetIn[pathsOut[i]] then + errorLine("Path is both input and output: " .. pathsOut[i]) + end end - - -- Process files. -- :SavedInfo local processingInfo = { - date = os.date("%Y-%m-%d %H:%M:%S", startTime), - files = {}, + date = os.date("%Y-%m-%d %H:%M:%S", startTime), + files = {}, } -local byteCount = 0 -local lineCount = 0 +local byteCount = 0 +local lineCount = 0 local lineCountCode = 0 -local tokenCount = 0 +local tokenCount = 0 for i, pathIn in ipairs(pathsIn) do - local startClockForPath = os.clock() - printfNoise("Processing '%s'...", pathIn) + local startClockForPath = os.clock() + printfNoise("Processing '%s'...", pathIn) - local pathOut = pathsOut[i] - local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "")..".meta.lua" + local pathOut = pathsOut[i] + local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "") .. ".meta.lua" - if not outputMeta or pathOut == "-" then - pathMeta = nil - end + if not outputMeta or pathOut == "-" then + pathMeta = nil + end - local info, err = pp.processFile{ - pathIn = pathIn, - pathMeta = pathMeta, - pathOut = pathOut, + local info, err = pp.processFile({ + pathIn = pathIn, + pathMeta = pathMeta, + pathOut = pathOut, - debug = isDebug, - addLineNumbers = addLineNumbers, + debug = isDebug, + addLineNumbers = addLineNumbers, - backtickStrings = allowBacktickStrings, - jitSyntax = allowJitSyntax, - canOutputNil = canOutputNil, - fastStrings = fastStrings, - validate = validate, - strictMacroArguments = strictMacroArguments, + backtickStrings = allowBacktickStrings, + jitSyntax = allowJitSyntax, + canOutputNil = canOutputNil, + fastStrings = fastStrings, + validate = validate, + strictMacroArguments = strictMacroArguments, - macroPrefix = macroPrefix, - macroSuffix = macroSuffix, + macroPrefix = macroPrefix, + macroSuffix = macroSuffix, - release = releaseMode, - logLevel = maxLogLevel, + release = releaseMode, + logLevel = maxLogLevel, - onInsert = (hasMessageHandler("insert") or nil) and function(name) - local lua = sendMessage("insert", pathIn, name) + onInsert = (hasMessageHandler("insert") or nil) and function(name) + local lua = sendMessage("insert", pathIn, name) - -- onInsert() is expected to return a Lua code string and so is the message - -- handler. However, if the handler is a single catch-all function we allow - -- the message to not be handled and we fall back to the default behavior of - -- treating 'name' as a path to a file to be inserted. If we didn't allow this - -- then it would be required for the "insert" message to be handled. I think - -- it's better if the user can choose whether to handle a message or not! - -- - if lua == nil and type(messageHandler) == "function" then - return assert(pp.readFile(name)) - end + -- onInsert() is expected to return a Lua code string and so is the message + -- handler. However, if the handler is a single catch-all function we allow + -- the message to not be handled and we fall back to the default behavior of + -- treating 'name' as a path to a file to be inserted. If we didn't allow this + -- then it would be required for the "insert" message to be handled. I think + -- it's better if the user can choose whether to handle a message or not! + -- + if lua == nil and type(messageHandler) == "function" then + return assert(pp.readFile(name)) + end - return lua - end, + return lua + end, - onBeforeMeta = messageHandler and function(lua) - sendMessage("beforemeta", pathIn, lua) - end, + onBeforeMeta = messageHandler and function(lua) + sendMessage("beforemeta", pathIn, lua) + end, - onAfterMeta = messageHandler and function(lua) - local luaModified = sendMessage("aftermeta", pathIn, lua) + onAfterMeta = messageHandler + and function(lua) + local luaModified = sendMessage("aftermeta", pathIn, lua) - if type(luaModified) == "string" then - lua = luaModified + if type(luaModified) == "string" then + lua = luaModified + elseif luaModified ~= nil then + error( + F( + "%s: Message handler did not return a string for 'aftermeta'. (Got %s)", + messageHandlerPath, + type(luaModified) + ) + ) + end - elseif luaModified ~= nil then - error(F( - "%s: Message handler did not return a string for 'aftermeta'. (Got %s)", - messageHandlerPath, type(luaModified) - )) - end + return lua + end, - return lua - end, + onDone = messageHandler and function(info) + sendMessage("filedone", pathIn, pathOut, info) + end, - onDone = messageHandler and function(info) - sendMessage("filedone", pathIn, pathOut, info) - end, + onError = function(err) + xpcall(function() + sendMessage("fileerror", pathIn, err) + end, function(err) + printfError("Additional error in 'fileerror' message handler...\n%s", pp.tryToFormatError(err)) + end) + os.exit(1) + end, + }) + assert(info, err) -- The onError() handler above should have been called and we should have exited already. - onError = function(err) - xpcall(function() - sendMessage("fileerror", pathIn, err) - end, function(err) - printfError("Additional error in 'fileerror' message handler...\n%s", pp.tryToFormatError(err)) - end) - os.exit(1) - end, - } - assert(info, err) -- The onError() handler above should have been called and we should have exited already. + byteCount = byteCount + info.processedByteCount + lineCount = lineCount + info.lineCount + lineCountCode = lineCountCode + info.linesOfCode + tokenCount = tokenCount + info.tokenCount - byteCount = byteCount + info.processedByteCount - lineCount = lineCount + info.lineCount - lineCountCode = lineCountCode + info.linesOfCode - tokenCount = tokenCount + info.tokenCount + if processingInfoPath ~= "" then + -- :SavedInfo + table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains. + end - if processingInfoPath ~= "" then - - -- :SavedInfo - table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains. - - end - - printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock()-startClockForPath) - printfNoise(("-"):rep(#header)) + printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock() - startClockForPath) + printfNoise(("-"):rep(#header)) end - - -- Finalize stuff. if processingInfoPath ~= "" then - printfNoise("Saving processing info to '%s'.", processingInfoPath) + printfNoise("Saving processing info to '%s'.", processingInfoPath) - local luaParts = {"return"} - assert(pp.serialize(luaParts, processingInfo)) - local lua = table.concat(luaParts) + local luaParts = { "return" } + assert(pp.serialize(luaParts, processingInfo)) + local lua = table.concat(luaParts) - local file = assert(io.open(processingInfoPath, "wb")) - file:write(lua) - file:close() + local file = assert(io.open(processingInfoPath, "wb")) + file:write(lua) + file:close() end printfNoise( - "All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)", - os.clock()-startClock, - #pathsIn, (#pathsIn == 1) and "" or "s", - lineCountCode, - lineCount, (lineCount == 1) and "" or "s", - tokenCount, (tokenCount == 1) and "" or "s", - formatBytes(byteCount) + "All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)", + os.clock() - startClock, + #pathsIn, + (#pathsIn == 1) and "" or "s", + lineCountCode, + lineCount, + (lineCount == 1) and "" or "s", + tokenCount, + (tokenCount == 1) and "" or "s", + formatBytes(byteCount) ) sendMessage("alldone") -- @Incomplete: Use pcall and format error message better? - - --[[!=========================================================== Copyright © 2018-2022 Marcus 'ReFreezed' Thunström @@ -648,4 +624,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ==============================================================]] - diff --git a/lib/preprocess.lua b/lib/preprocess.lua index 52d8a5e..54ca5fb 100644 --- a/lib/preprocess.lua +++ b/lib/preprocess.lua @@ -130,89 +130,145 @@ --============================================================]] - - local PP_VERSION = "1.21.0-dev" -local MAX_DUPLICATE_FILE_INSERTS = 1000 -- @Incomplete: Make this a parameter for processFile()/processString(). +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 + "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 + "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 + "+", + "-", + "*", + "/", + "%", + "^", + "#", + "==", + "~=", + "<=", + ">=", + "<", + ">", + "=", + "(", + ")", + "{", + "}", + "[", + "]", + ";", + ":", + ",", + ".", + "..", + "...", + -- 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]], - ["\\"] = [[\\]], + ["\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 + ['"'] = [[\"]], + ["'"] = [[\']], +} +for k, v in pairs(ESCAPE_SEQUENCES_EXCEPT_QUOTES) do + ESCAPE_SEQUENCES[k] = v +end -local USELESS_TOKENS = {whitespace=true, comment=true} +local USELESS_TOKENS = { whitespace = true, comment = true } local LOG_LEVELS = { - ["off" ] = 0, - ["error" ] = 1, - ["warning"] = 2, - ["info" ] = 3, - ["debug" ] = 4, - ["trace" ] = 5, + ["off"] = 0, + ["error"] = 1, + ["warning"] = 2, + ["info"] = 3, + ["debug"] = 4, + ["trace"] = 5, } -local metaEnv = nil +local metaEnv = nil local dummyEnv = {} -- Controlled by processFileOrString(): local current_parsingAndMeta_isProcessing = false -local current_parsingAndMeta_isDebug = 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_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 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 ============================================ @@ -227,124 +283,125 @@ local sortNatural local tableInsert, tableRemove, tableInsertFormat local utf8GetCodepointAndLength - - local F = string.format local function tryToFormatError(err0) - local err, path, ln = nil + 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 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 + 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, ...)) + 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 + 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") + io.stderr:write(s, "\n") end local function printfError(s, ...) - printError(F(s, ...)) + printError(F(s, ...)) end -- message = formatTraceback( [ level=1 ] ) local function formatTraceback(level) - local buffer = {} - tableInsert(buffer, "stack traceback:\n") + local buffer = {} + tableInsert(buffer, "stack traceback:\n") - level = 1 + (level or 1) - local stack = {} + level = 1 + (level or 1) + local stack = {} - while level < 1/0 do - local info = debug.getinfo(level, "nSl") - if not info then break end + 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 isFile = info.source:find("^@") ~= nil + local sourceName = (isFile and info.source:sub(2) or info.short_src) - local subBuffer = {"\t"} - tableInsertFormat(subBuffer, "%s:", sourceName) + local subBuffer = { "\t" } + tableInsertFormat(subBuffer, "%s:", sourceName) - if info.currentline > 0 then - tableInsertFormat(subBuffer, "%d:", info.currentline) - end + 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 + 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 + tableInsert(stack, table.concat(subBuffer)) + level = level + 1 + end - while stack[#stack] == "\t[C]: ?" do - stack[#stack] = nil - end + while stack[#stack] == "\t[C]: ?" do + stack[#stack] = nil + end - for _, s in ipairs(stack) do - tableInsert(buffer, s) - tableInsert(buffer, "\n") - end + for _, s in ipairs(stack) do + tableInsert(buffer, s) + tableInsert(buffer, "\n") + end - return table.concat(buffer) + return table.concat(buffer) end -- printErrorTraceback( message [, level=1 ] ) local function printErrorTraceback(message, level) - printError(tryToFormatError(message)) - printError(formatTraceback(1+(level or 1))) + 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) + 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 + 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. @@ -352,722 +409,805 @@ 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. + 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 + 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 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, ...) + 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) + 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 + 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 + 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 + -- 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 + -- 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, ...) + -- 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, ...) + -- 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, ...) + -- 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) + 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 + 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+", " ") + 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 + 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 + return lua end - - local ERROR_UNFINISHED_STRINGLIKE = 1 local function parseStringlikeToken(s, ptr) - local reprStart = ptr - local reprEnd + local reprStart = ptr + local reprEnd - local valueStart - local valueEnd + local valueStart + local valueEnd - local longEqualSigns = s:match("^%[(=*)%[", ptr) - local isLong = longEqualSigns ~= nil + local longEqualSigns = s:match("^%[(=*)%[", ptr) + local isLong = longEqualSigns ~= nil - -- Single line. - if not isLong then - valueStart = ptr + -- 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 + 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 + -- 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 + 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 + 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} + 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 + 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_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(" +", "") +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 + 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 = {} + 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 + 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 - else - for i = (i1 or 1), (i2 or #tokens) do - tableInsert(parts, tokens[i].representation) - end - end - - return table.concat(parts) + return table.concat(parts) end local function insertTokenRepresentations(parts, tokens, i1, i2) - for i = i1, i2 do - tableInsert(parts, tokens[i].representation) - end + 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") + 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 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 + 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") + assertarg(1, path, "string") - if type(isTextFile) == "boolean" then - assertarg(3, contents, "string") - else - isTextFile, contents = false, isTextFile - assertarg(2, contents, "string") - end + 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 + 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 + file:write(contents) + file:close() + return true end local function fileExists(path) - assertarg(1, path, "string") + assertarg(1, path, "string") - local file = io.open(path, "r") - if not file then return false end + local file = io.open(path, "r") + if not file then + return false + end - file:close() - return true + file:close() + return true end - - -- assertarg( argumentNumber, value, expectedValueType1, ... ) ---[[local]] function assertarg(n, v, ...) - local vType = type(v) +--[[local]] +function assertarg(n, v, ...) + local vType = type(v) - for i = 1, select("#", ...) do - if vType == select(i, ...) then return end - end + 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 ") + local fName = debug.getinfo(2, "n").name + local expects = table.concat({ ... }, " or ") - if fName == "" then fName = "?" end + if fName == "" then + fName = "?" + end - errorf(3, "bad argument #%d to '%s' (%s expected, got %s)", n, fName, expects, vType) + 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 _ +--[[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 + while true do + _, i = s:find(needle, i + 1, plain) + if not i then + return count + end - count = count+1 - 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 +--[[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 + 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 + 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 -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 - + 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}, + { 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 + 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 + --[[ :SerializationCache if cache[v] then tableInsert(buffer, cache[v]) return true @@ -1075,183 +1215,199 @@ local function serialize(buffer, v) local bufferStart = #buffer + 1 --]] - local vType = type(v) + local vType = type(v) - if vType == "table" then - local first = true - tableInsert(buffer, "{") + 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 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 + local ok, err = serialize(buffer, item) + if not ok then + return false, err + end - indices[i] = true - 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 + 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) + table.sort(keys, function(a, b) + return tostring(a) < tostring(b) + end) - for _, k in ipairs(keys) do - local item = v[k] + for _, k in ipairs(keys) do + local item = v[k] - if not first then tableInsert(buffer, ",") end - first = false + 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, "=") + if not KEYWORDS[k] and type(k) == "string" and k:find("^[%a_][%w_]*$") then + tableInsert(buffer, k) + tableInsert(buffer, "=") + else + tableInsert(buffer, "[") - else - tableInsert(buffer, "[") + local ok, err = serialize(buffer, k) + if not ok then + return false, err + end - local ok, err = serialize(buffer, k) - if not ok then return false, err end + tableInsert(buffer, "]=") + end - tableInsert(buffer, "]=") - end + local ok, err = serialize(buffer, item) + if not ok then + return false, err + end + 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 - tableInsert(buffer, "}") + local useApostrophe = v:find('"', 1, true) and not v:find("'", 1, true) + local quote = useApostrophe and "'" or '"' - elseif vType == "string" then - if v == "" then - tableInsert(buffer, '""') - return true - end + tableInsert(buffer, quote) - local useApostrophe = v:find('"', 1, true) and not v:find("'", 1, true) - local quote = useApostrophe and "'" or '"' + if current_anytime_fastStrings or not v:find("[^\32-\126\t\n]") then + -- print(">> FAST", #v) -- DEBUG - tableInsert(buffer, quote) + 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 - if current_anytime_fastStrings or not v:find"[^\32-\126\t\n]" then - -- print(">> FAST", #v) -- DEBUG + -- @Speed: There are optimizations to be made here! + while pos <= #v do + local c = v:sub(pos, pos) + local cp, len = utf8GetCodepointAndLength(v, pos) - 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) + -- 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 - else - -- print(">> SLOW", #v) -- DEBUG - local 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 - -- @Speed: There are optimizations to be made here! - while pos <= #v do - local c = v:sub(pos, pos) - local cp, len = utf8GetCodepointAndLength(v, pos) + -- Anything else. + else + tableInsert(buffer, F((v:find("^%d", pos + 1) and "\\%03d" or "\\%d"), v:byte(pos))) + pos = pos + 1 + end + end + end - -- 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 + 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 - -- 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 + --[[ :SerializationCache if v ~= nil then cache[v] = table.concat(buffer, "", bufferStart, #buffer) end --]] - return true + return true end -- luaString = toLua( value ) -- Returns nil and a message on error. local function toLua(v) - local buffer = {} + local buffer = {} - local ok, err = serialize(buffer, v) - if not ok then return nil, err end + local ok, err = serialize(buffer, v) + if not ok then + return nil, err + end - return table.concat(buffer) + 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)", "@", (env or getfenv(2))) - if not chunk then - return nil, F("Invalid expression '%s'. (%s)", expr, (err:gsub("^:%d+: ", ""))) - end + local chunk, err = loadLuaString("return(" .. expr .. "\n)", "@", (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 + local ok, valueOrErr = pcall(chunk) + if not ok then + return nil, valueOrErr + end - return valueOrErr -- May be nil or false! + return valueOrErr -- May be nil or false! end - - local function escapePattern(s) - return (s:gsub("[-+*^?$.%%()[%]]", "%%%0")) + return (s:gsub("[-+*^?$.%%()[%]]", "%%%0")) end - - local function outputLineNumber(parts, ln) - tableInsert(parts, "--[[@") - tableInsert(parts, ln) - tableInsert(parts, "]]") + 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 +--[[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 + outputLineNumber(parts, tok.line) + return tok.line end --[=[ --[[local]] function maybeOutputLineNumber(parts, tok, lastLn, fromMetaToOutput) @@ -1266,338 +1422,380 @@ end end ]=] - - local function isAny(v, ...) - for i = 1, select("#", ...) do - if v == select(i, ...) then return true end - end - return false + 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 + 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 + 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] + 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 + 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 - else - copy[k] = v - end - end - return copy - end + -- copy = copyTable( table [, deep=false ] ) + --[[local]] + function copyTable(t, deep) + local copy = {} - -- copy = copyTable( table [, deep=false ] ) - --[[local]] function copyTable(t, deep) - local copy = {} + if deep then + return deepCopy(t, copy, { [t] = copy }) + end - if deep then - return deepCopy(t, copy, {[t]=copy}) - end + for k, v in pairs(t) do + copy[k] = v + end - for k, v in pairs(t) do copy[k] = v end - - return copy - 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 + (_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 ---[[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 + 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 + (_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 + if env then + setfenv(chunk, env) + end - return chunk - end + return chunk + end ) local function isLuaStringValidExpression(lua) - return loadLuaString("return("..lua.."\n)", "@", nil) ~= nil + 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) - ) + 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 + for i = iStart, iLimit, dir do + if not USELESS_TOKENS[tokens[i].type] then + return tokens[i], i + end + end - return nil + 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) + 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) + 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) +--[[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 type(otherFilename) == "table" then + return getRelativeLocationText(tokOfInterest, otherFilename.file, otherFilename.line) + end - if not (tokOfInterest.file and tokOfInterest.line) then - return "at " - end + if not (tokOfInterest.file and tokOfInterest.line) then + return "at " + 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" + 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]] tableInsert = table.insert ---[[local]] tableRemove = table.remove - ---[[local]] function tableInsertFormat(t, s, ...) - tableInsert(t, F(s, ...)) +--[[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) + pos = pos or 1 + local b1, b2, b3, b4 = s:byte(pos, pos + 3) - if b1 > 0 and b1 <= 127 then - return 1 + 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 - 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. + 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 +--[[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 + -- 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 keys = {} + for k in pairs(t) do + tableInsert(keys, k) + end + sortNatural(keys) - local i = 0 + local i = 0 - return function() - i = i+1 - local k = keys[i] - if k ~= nil then return k, t[k] end - end + 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 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 + --[[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] + local lua = current_parsingAndMeta_resourceCache[resourceName] - if not lua then - if current_parsingAndMeta_onInsert then - lua = current_parsingAndMeta_onInsert(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 + 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) - 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 - 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 - 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 isParsing then - tableInsert(stats.insertedNames, resourceName) - end + 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 - 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 + return lua end - - --============================================================== --= Preprocessor Functions ===================================== --============================================================== - - -- :EnvironmentTable ---------------------------------------------------------------- -metaEnv = copyTable(_G) -- Include all standard Lua stuff. +metaEnv = copyTable(_G) -- Include all standard Lua stuff. metaEnv._G = metaEnv local metaFuncs = {} @@ -1610,7 +1808,7 @@ 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.readFile = readFile metaFuncs.getFileContents = readFile -- @Deprecated -- writeFile() @@ -1691,14 +1889,16 @@ metaFuncs.compareNatural = compareNatural -- returnValue1, ... = run( path [, arg1, ... ] ) -- Execute a Lua file. Similar to dofile(). function metaFuncs.run(path, ...) - assertarg(1, path, "string") + assertarg(1, path, "string") - local main_chunk, err = loadLuaFile(path, metaEnv) - if not main_chunk then error(err, 0) end + 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) + -- 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() @@ -1707,32 +1907,37 @@ end -- 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) + errorIfNotRunningMeta(2) - local argCount = select("#", ...) - if argCount == 0 then - error("No values to output.", 2) - end + local argCount = select("#", ...) + if argCount == 0 then + error("No values to output.", 2) + end - for i = 1, argCount do - local v = select(i, ...) + 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 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 + if i > 1 then + tableInsert(current_meta_output, (current_parsingAndMeta_isDebug and ", " or ",")) + end - local ok, err = serialize(current_meta_output, v) + 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 + if not ok then + local ln = debug.getinfo(2, "l").currentline + errorOnLine(current_meta_pathForErrorMessages, ln, "MetaProgram", "%s", err) + end + end end -- outputLua() @@ -1740,18 +1945,18 @@ end -- 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) + errorIfNotRunningMeta(2) - local argCount = select("#", ...) - if argCount == 0 then - error("No Lua code to output.", 2) - end + 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 + for i = 1, argCount do + local lua = select(i, ...) + assertarg(i, lua, "string") + tableInsert(current_meta_output, lua) + end end -- outputLuaTemplate() @@ -1763,25 +1968,25 @@ end -- outputLuaTemplate("local name, age = ?, ?", "Harry", 48) -- outputLuaTemplate("dogs[?] = ?", "greyhound", {italian=false, count=5}) function metaFuncs.outputLuaTemplate(lua, ...) - errorIfNotRunningMeta(2) - assertarg(1, lua, "string") + errorIfNotRunningMeta(2) + assertarg(1, lua, "string") - local args = {...} -- @Memory - local n = 0 - local v, err + local args = { ... } -- @Memory + local n = 0 + local v, err - lua = lua:gsub("%?", function() - n = n + 1 - v, err = toLua(args[n]) + 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 + if not v then + errorf(3, "Bad argument %d: %s", 1 + n, err) + end - return v - end) + return v + end) - tableInsert(current_meta_output, lua) + tableInsert(current_meta_output, lua) end -- getOutputSoFar() @@ -1793,43 +1998,42 @@ end -- 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) + errorIfNotRunningMeta(2) - -- Should there be a way to get the contents of current_meta_output etc.? :GetMoreOutputFromStack + -- 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 + 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) + errorIfNotRunningMeta(2) - local len = 0 + 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] + -- 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 + if fragment:find("\n", 1, true) then + len = len + 1 + lineFragments[len] = fragment:gsub(".*\n", "") + break + end - len = len + 1 - lineFragments[len] = fragment - end + len = len + 1 + lineFragments[len] = fragment + end - return table.concat(lineFragments, 1, len) + return table.concat(lineFragments, 1, len) end -- getOutputSoFarOnLine() @@ -1843,50 +2047,50 @@ metaFuncs.getOutputSoFarOnLine = getOutputSoFarOnLine -- Get the amount of bytes outputted so far. -- Raises an error if no file or string is being processed. function metaFuncs.getOutputSizeSoFar() - errorIfNotRunningMeta(2) + errorIfNotRunningMeta(2) - local size = 0 + local size = 0 - for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack - size = size + #lua - end + for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack + size = size + #lua + end - return size + return size end -- getCurrentLineNumberInOutput() -- lineNumber = getCurrentLineNumberInOutput( ) -- Get the current line number in the output. function metaFuncs.getCurrentLineNumberInOutput() - errorIfNotRunningMeta(2) + errorIfNotRunningMeta(2) - local ln = 1 + local ln = 1 - for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack - ln = ln + countString(lua, "\n", true) - end + for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack + ln = ln + countString(lua, "\n", true) + end - return ln + return ln end local function getIndentation(line, tabWidth) - if not tabWidth then - return line:match"^[ \t]*" - end + if not tabWidth then + return line:match("^[ \t]*") + end - local indent = 0 + 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 + 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 + return indent end -- getIndentation() @@ -1900,22 +2104,22 @@ metaFuncs.getIndentation = getIndentation -- 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)) + 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 + 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 + return current_anytime_pathOut end -- tokenize() @@ -1927,51 +2131,55 @@ end -- } -- 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, "", allowPpCode, allowPpCode, true) -- @Incomplete: Make allowJitSyntax a parameter to tokenize()? - if not ok then - return nil, cleanError(errOrTokens) - end - return errOrTokens + local ok, errOrTokens = pcall(_tokenize, lua, "", 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 + 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, 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 + 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 + 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 + if ignoreUselessTokens then + return nextUsefulToken, tokens, 0 + else + return ipairs(tokens) + end end -- getNextUsefulToken() @@ -1979,41 +2187,67 @@ end -- 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) + 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 + 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 + 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 + 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, + 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() @@ -2057,157 +2291,145 @@ local numberFormatters = { -- "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") + 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 = "" + local repr + if long then + local equalSigns = "" - while comment:find(F("]%s]", equalSigns), 1, true) do - equalSigns = equalSigns.."=" - end + while comment:find(F("]%s]", equalSigns), 1, true) do + equalSigns = equalSigns .. "=" + end - repr = F("--[%s[%s]%s]", equalSigns, comment, equalSigns) + repr = F("--[%s[%s]%s]", equalSigns, comment, equalSigns) + else + repr = F("--%s\n", comment) + end - 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") - return {type="comment", representation=repr, value=comment, long=long} + 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 - elseif tokType == "identifier" then - local ident = ... - assertarg(2, ident, "string") + return { type = "identifier", representation = ident, value = ident } + elseif tokType == "keyword" then + local keyword = ... + assertarg(2, keyword, "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 + if not KEYWORDS[keyword] then + errorf(2, "Bad keyword '%s'.", keyword) + end - return {type="identifier", representation=ident, value=ident} + 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") - elseif tokType == "keyword" then - local keyword = ... - assertarg(2, keyword, "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) + ) - if not KEYWORDS[keyword] then - errorf(2, "Bad keyword '%s'.", keyword) - end + return { type = "number", representation = numStr, value = n } + elseif tokType == "punctuation" then + local symbol = ... + assertarg(2, symbol, "string") - return {type="keyword", representation=keyword, value=keyword} + -- Note: "!" and "!!" are of a different token type (pp_entry). + if not PUNCTUATION[symbol] then + errorf(2, "Bad symbol '%s'.", symbol) + end - elseif tokType == "number" then - local n, numberFormat = ... - numberFormat = numberFormat or "auto" - assertarg(2, n, "number") - assertarg(3, numberFormat, "string") + return { type = "punctuation", representation = symbol, value = symbol } + elseif tokType == "string" then + local s, long = ... + long = not not long + assertarg(2, s, "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) - ) + local repr - return {type="number", representation=numStr, value=n} + if long then + local equalSigns = "" - elseif tokType == "punctuation" then - local symbol = ... - assertarg(2, symbol, "string") + while s:find(F("]%s]", equalSigns), 1, true) do + equalSigns = equalSigns .. "=" + end - -- Note: "!" and "!!" are of a different token type (pp_entry). - if not PUNCTUATION[symbol] then - errorf(2, "Bad symbol '%s'.", symbol) - end + repr = F("[%s[%s]%s]", equalSigns, s, equalSigns) + else + repr = toLua(s) + end - return {type="punctuation", representation=symbol, value=symbol} + return { type = "string", representation = repr, value = s, long = long } + elseif tokType == "whitespace" then + local whitespace = ... + assertarg(2, whitespace, "string") - elseif tokType == "string" then - local s, long = ... - long = not not long - assertarg(2, s, "string") + if whitespace == "" then + error("String is empty.", 2) + elseif whitespace:find("%S") then + error("String contains non-whitespace characters.", 2) + end - local repr + return { type = "whitespace", representation = whitespace, value = whitespace } + elseif tokType == "pp_entry" then + local double = ... + assertarg(2, double, "boolean") - if long then - local equalSigns = "" + local symbol = double and "!!" or "!" - while s:find(F("]%s]", equalSigns), 1, true) do - equalSigns = equalSigns .. "=" - end + return { type = "pp_entry", representation = symbol, value = symbol, double = double } + elseif tokType == "pp_keyword" then + local keyword = ... + assertarg(2, keyword, "string") - repr = F("[%s[%s]%s]", equalSigns, s, equalSigns) + 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") - 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 + 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)) + return (_concatTokens(tokens, nil, false, nil, nil)) end local recycledArrays = {} @@ -2217,28 +2439,31 @@ local recycledArrays = {} -- Start intercepting output until stopInterceptingOutput() is called. -- The function can be called multiple times to intercept interceptions. function metaFuncs.startInterceptingOutput() - errorIfNotRunningMeta(2) + 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) + 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) + 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) + 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) + return table.concat(interceptedLua) end -- stopInterceptingOutput() -- luaString = stopInterceptingOutput( ) -- Stop intercepting output and retrieve collected code. function metaFuncs.stopInterceptingOutput() - return (_stopInterceptingOutput(2)) + return (_stopInterceptingOutput(2)) end -- loadResource() @@ -2246,16 +2471,16 @@ end -- 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) + errorIfNotRunningMeta(2) - return (_loadResource(resourceName, false, 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") + 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() @@ -2263,33 +2488,40 @@ end -- 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) + errorIfNotRunningMeta(2) - assertarg(1, nameOrFunc, "string","function") - local f + 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 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 + 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 - else - f = nameOrFunc - end - - return (metaEnv.__M()(f(...))) + 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 + return current_parsingAndMeta_isProcessing end -- :PredefinedMacros @@ -2299,28 +2531,32 @@ end -- 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 + 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 not isLuaStringValidExpression(conditionCode) then + -- errorf(2, "Invalid condition expression: %s", formatCodeForShortMessage(conditionCode)) + -- end - if current_meta_releaseMode then return 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(") + 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 + 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") + tableInsert(current_meta_output, ") end") end -- LOG() @@ -2334,35 +2570,49 @@ end -- (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 + 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 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 + 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 + if LOG_LEVELS[logLevel] > LOG_LEVELS[current_meta_maxLogLevel] then + return + end - tableInsert(current_meta_output, "print(") + 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 + 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, ")") + tableInsert(current_meta_output, ")") end -- Extra stuff used by the command line program: @@ -2370,1520 +2620,1812 @@ metaFuncs.tryToFormatError = tryToFormatError ---------------------------------------------------------------- - - -for k, v in pairs(metaFuncs) do metaEnv[k] = v end +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))) + return (assert(toLua(v))) end function metaEnv.__ISLUA(lua) - if type(lua) ~= "string" then - error("Value is not Lua code.", 2) - end - return 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 + 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 + 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 + 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 + 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 + return lua end function metaEnv.__EVAL(v) -- For symbols. - if isCallable(v) then - v = v() - end - return v + if isCallable(v) then + v = v() + end + return v end - - local function getLineCountWithCode(tokens) - local lineCount = 0 - local lastLine = 0 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 = {} + -- + -- 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 + for i = #tokensToExpand, 1, -1 do + tableInsert(tokenStack, tokensToExpand[i]) + end - while tokenStack[1] do - local tok = tokenStack[#tokenStack] + while tokenStack[1] do + local tok = tokenStack[#tokenStack] - -- Keyword. - if isToken(tok, "pp_keyword") then - local ppKeywordTok = tok + -- 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. + -- @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 - 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) - -- 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 - 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. (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)) - -- $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 - -- Anything else. - else - tableInsert(outTokens, tok) - tableRemove(tokenStack) -- anything - end - end--while tokenStack - - return outTokens + 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 = {} + -- + -- 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 + for i = #tokensToExpand, 1, -1 do + tableInsert(tokenStack, tokensToExpand[i]) + end - while tokenStack[1] do - local tok = tokenStack[#tokenStack] + 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) + -- 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 + -- @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) + 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 + 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) + 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) + -- @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 + 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' + -- 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 - 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 - ) + -- Anything else. + else + tableInsert(outTokens, tok) + tableRemove(tokenStack) -- anything + end + end --while tokenStack - 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 + 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 + 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 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 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 ppEntryTokIndex = tokens.nextI + local ppEntryTok = tokens[ppEntryTokIndex] + tokens.nextI = tokens.nextI + 2 -- '!(' or '!!(' - local outTokens = {} - local depthStack = {} + local outTokens = {} + local depthStack = {} - while true do - local tok = tokens[tokens.nextI] + 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 + 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 + -- 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)) + -- 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]]"}"}) + -- 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 - 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 - 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) - 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 - 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 + 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 ppEntryTok = tokens[tokens.nextI] + tokens.nextI = tokens.nextI + 1 -- '!' or '!!' - local isDual = ppEntryTok.double - local astOutNode = (isDual and AstDualCode or AstMetaprogram)(ppEntryTok) + local isDual = ppEntryTok.double + local astOutNode = (isDual and AstDualCode or AstMetaprogram)(ppEntryTok) - if astOutNode.type == "metaprogram" then - astOutNode.originIsLine = true - end + 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 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 + if isTokenAndNotNil(tokNext, "keyword", "local") then + astOutNode.isDeclaration = true - tokens.nextI = iNext + 1 -- after 'local' - tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) - end + tokens.nextI = iNext + 1 -- after 'local' + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + end - local usedNames = {} + 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) + 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 + 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 + 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 = {} + -- Find end of metaprogram line. + local outTokens = isDual and astOutNode.valueTokens or astOutNode.tokens + local depthStack = {} - while true do - local tok = tokens[tokens.nextI] + 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 + 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 + -- 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)) + -- 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]]"}"}) + -- 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 - 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 - tableInsert(outTokens, tok) - tokens.nextI = tokens.nextI + 1 -- after anything - end - end - - return astOutNode + return astOutNode end ---[[local]] function astParseMetaBlockOrLine(tokens) - return isTokenAndNotNil(tokens[tokens.nextI+1], "punctuation", "(") - and astParseMetaBlock(tokens) - or astParseMetaLine (tokens) +--[[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 macroStartTok = tokens[tokens.nextI] + tokens.nextI = tokens.nextI + 1 -- after '@insert' - local astMacro = AstMacro(macroStartTok) + local astMacro = AstMacro(macroStartTok) - -- - -- Callee. - -- + -- + -- 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 '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 + -- 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) + -- 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) + 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 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) + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) - if punctTok.value == ":" then break end + if punctTok.value == ":" then + break + end + elseif isToken(tokNext, "punctuation", "[") then + local punctTok = tokNext + tokens.nextI = iNext + 1 -- after '[' + tableInsert(astMacro.calleeTokens, tokNext) - elseif isToken(tokNext, "punctuation", "[") then - local punctTok = tokNext - tokens.nextI = iNext + 1 -- after '[' - tableInsert(astMacro.calleeTokens, tokNext) + local bracketBalance = 1 - 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) - 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 - 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) - tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + -- @UX: Validate that the contents form an expression. + else + break + end + end - -- @UX: Validate that the contents form an expression. + -- + -- Arguments. + -- - else - break - end - end + -- @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 - -- - -- Arguments. - -- + -- @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 - -- @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 + local astLuaInCurrentArg = AstLua(tokNext, { tokNext }) + tableInsert(macroArg.nodes, astLuaInCurrentArg) - -- @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 + tokens.nextI = iNext + 1 -- after '{' - local astLuaInCurrentArg = AstLua(tokNext, {tokNext}) - tableInsert(macroArg.nodes, astLuaInCurrentArg) + -- + -- (Similar code as `@insert identifier()` below.) + -- - tokens.nextI = iNext + 1 -- after '{' + -- Collect tokens for the table arg. + -- We're looking for the closing '}'. + local bracketDepth = 1 -- @Incomplete: Track all brackets! - -- - -- (Similar code as `@insert identifier()` below.) - -- + while true do + local tok = tokens[tokens.nextI] - -- Collect tokens for the table arg. - -- We're looking for the closing '}'. - local bracketDepth = 1 -- @Incomplete: Track all brackets! + if not tok then + errorAtToken( + macroArg.locationToken, + nil, + "Parser/MacroArgument", + "Could not find end of table constructor before EOF." + ) - while true do - local tok = tokens[tokens.nextI] + -- Preprocessor block in macro. + elseif tok.type == "pp_entry" then + tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens)) + astLuaInCurrentArg = nil - if not tok then - errorAtToken(macroArg.locationToken, nil, "Parser/MacroArgument", "Could not find end of table constructor before EOF.") + -- Nested macro. + elseif isToken(tok, "pp_keyword", "insert") then + tableInsert(macroArg.nodes, astParseMacro(params, tokens)) + astLuaInCurrentArg = nil - -- Preprocessor block in macro. - elseif tok.type == "pp_entry" then - tableInsert(macroArg.nodes, astParseMetaBlockOrLine(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) + ) - -- Nested macro. - elseif isToken(tok, "pp_keyword", "insert") then - tableInsert(macroArg.nodes, astParseMacro(params, tokens)) - astLuaInCurrentArg = nil + -- 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 - -- 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)) + -- Normal token. + else + if isToken(tok, "punctuation", "{") then + bracketDepth = bracketDepth + 1 + elseif isToken(tok, "punctuation", "}") then + bracketDepth = bracketDepth - 1 + end - -- 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 + if not astLuaInCurrentArg then + astLuaInCurrentArg = AstLua(tok) + tableInsert(macroArg.nodes, astLuaInCurrentArg) + end + tableInsert(astLuaInCurrentArg.tokens, tok) + advanceToken(tokens) -- anything + end + end - -- Normal token. - else - if isToken(tok, "punctuation", "{") then - bracketDepth = bracketDepth + 1 - elseif isToken(tok, "punctuation", "}") then - bracketDepth = bracketDepth - 1 - 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 - if not astLuaInCurrentArg then - astLuaInCurrentArg = AstLua(tok) - tableInsert(macroArg.nodes, astLuaInCurrentArg) - end - tableInsert(astLuaInCurrentArg.tokens, tok) - advanceToken(tokens) -- anything - end - end + local parensStartTok = tokNext + tokens.nextI = iNext + 1 -- after '(' + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) - -- @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 + 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 - local parensStartTok = tokNext - tokens.nextI = iNext + 1 -- after '(' - tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + advancePastUseless(tokens) -- Trim leading useless tokens. - if isTokenAndNotNil(tokNext, "punctuation", ")") then - tokens.nextI = iNext + 1 -- after ')' + local astLuaInCurrentArg = nil + local depthStack = {} - 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 + while true do + local tok = tokens[tokens.nextI] - advancePastUseless(tokens) -- Trim leading useless tokens. + if not tok then + errorAtToken( + parensStartTok, + nil, + "Parser/Macro", + "Could not find end of argument list before EOF." + ) - local astLuaInCurrentArg = nil - local depthStack = {} + -- Preprocessor block in macro. + elseif tok.type == "pp_entry" then + tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens)) + astLuaInCurrentArg = nil - while true do - local tok = tokens[tokens.nextI] + -- Nested macro. + elseif isToken(tok, "pp_keyword", "insert") then + tableInsert(macroArg.nodes, astParseMacro(params, tokens)) + astLuaInCurrentArg = nil - if not tok then - errorAtToken(parensStartTok, nil, "Parser/Macro", "Could not find end of argument list before EOF.") + -- 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) + ) - -- Preprocessor block in macro. - elseif tok.type == "pp_entry" then - tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens)) - astLuaInCurrentArg = nil + -- End of argument. + elseif + not depthStack[1] and (isToken(tok, "punctuation", ",") or isToken(tok, "punctuation", ")")) + then + break - -- Nested macro. - elseif isToken(tok, "pp_keyword", "insert") then - tableInsert(macroArg.nodes, astParseMacro(params, tokens)) - astLuaInCurrentArg = nil + -- 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 - -- 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)) + if not astLuaInCurrentArg then + astLuaInCurrentArg = AstLua(tok) + tableInsert(macroArg.nodes, astLuaInCurrentArg) + end + tableInsert(astLuaInCurrentArg.tokens, tok) + advanceToken(tokens) -- anything + end + end - -- End of argument. - elseif not depthStack[1] and (isToken(tok, "punctuation", ",") or isToken(tok, "punctuation", ")")) then - break + if astLuaInCurrentArg then + -- Trim trailing useless tokens. + popUseless(astLuaInCurrentArg.tokens) + if not astLuaInCurrentArg.tokens[1] then + assert(tableRemove(macroArg.nodes) == astLuaInCurrentArg) + end + end - -- 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"}) + 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 - 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 + -- Do next argument or finish arguments. + if isTokenAndNotNil(tokens[tokens.nextI], "punctuation", ")") then + tokens.nextI = tokens.nextI + 1 -- after ')' + break + end - if not astLuaInCurrentArg then - astLuaInCurrentArg = AstLua(tok) - tableInsert(macroArg.nodes, astLuaInCurrentArg) - end - tableInsert(astLuaInCurrentArg.tokens, tok) - advanceToken(tokens) -- anything - end - end + assert(isToken(advanceToken(tokens), "punctuation", ",")) -- The loop above should have continued otherwise! + end --for argNum + end - if astLuaInCurrentArg then - -- Trim trailing useless tokens. - popUseless(astLuaInCurrentArg.tokens) - if not astLuaInCurrentArg.tokens[1] then - assert(tableRemove(macroArg.nodes) == astLuaInCurrentArg) - end - 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 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 + if not isTokenAndNotNil(tokens[tokens.nextI + 1], "punctuation", "(") then + errorAfterToken(tokNext, "Parser/Macro", "Expected '(' after '%s'.", tokNext.value) + end - -- Do next argument or finish arguments. - if isTokenAndNotNil(tokens[tokens.nextI], "punctuation", ")") then - tokens.nextI = tokens.nextI + 1 -- after ')' - break - 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 - 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 + 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 + -- @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 + 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)) + 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_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) - -- elseif isToken(tok, "pp_symbol") then -- We currently expand these in doEarlyExpansions(). - -- errorAtToken(tok, nil, "Parser", "Internal error: @Incomplete: Handle symbols.") + while true do + tableInsert(astLua.tokens, tok) + advanceToken(tokens) - else - local astLua = AstLua(tok) - tableInsert(astSequence.nodes, astLua) + tok = tokens[tokens.nextI] + if not tok then + break + end + if tok.type:find("^pp_") then + break + end + end + end + end - 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 + 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 + 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 + -- + -- 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 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 + 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 + -- + -- !(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 + for _, tok in ipairs(ast.tokens) do + tableInsert(buffer, tok.representation) + end - if asMacroArgExpr - then tableInsert(buffer, ")") - else tableInsert(buffer, "))\n") 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 + -- + -- !!(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 + for _, tok in ipairs(ast.tokens) do + tableInsert(buffer, tok.representation) + end - if asMacroArgExpr - then tableInsert(buffer, ")") - else tableInsert(buffer, "))\n") 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 + -- + -- !(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 + 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 + 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 - else - for _, tok in ipairs(ast.tokens) do - tableInsert(buffer, tok.representation) - end - tableInsert(buffer, "\n") - end + -- + -- @@callee(argument1, ...) -> __LUA(__M(callee(__ARG(1,), ...))) + -- OR -> __LUA(__M(callee(__ARG(1,function()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 - -- - -- @@callee(argument1, ...) -> __LUA(__M(callee(__ARG(1,), ...))) - -- OR -> __LUA(__M(callee(__ARG(1,function()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, "(") - 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 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 - 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 - 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() - 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, ",") - 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 - 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") + 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 + tableInsert(buffer, "))") - else - tableInsert(buffer, '""') - end + if not asMacroArgExpr then + tableInsert(buffer, ")\n") + end - tableInsert(buffer, ")") - 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 - tableInsert(buffer, "))") + -- 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 - if not asMacroArgExpr then tableInsert(buffer, ")\n") 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 - -- - -- !!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 + 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 - -- 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 + -- 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 - -- 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 + if current_parsingAndMeta_isDebug then + tableInsert(buffer, '; __LUA("\\n")\n') + else + tableInsert(buffer, '__LUA"\\n"\n') + 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 + -- + -- ... + -- + elseif ast.type == "sequence" then + for _, astChild in ipairs(ast.nodes) do + ln, lnMeta = astNodeToMetaprogram(buffer, astChild, ln, lnMeta, false) + 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 + -- 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 - 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 + return ln, lnMeta end local function astToLua(ast) - local buffer = {} - astNodeToMetaprogram(buffer, ast, 0, 0, false) - return table.concat(buffer) + 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 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.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 + 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 - else - if not params.code then error("Missing 'code' in params.", 2) end - end + -- Read input. + local luaUnprocessed, virtualPathIn - -- Read input. - local luaUnprocessed, virtualPathIn + if isFile then + virtualPathIn = params.pathIn + local err - 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 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 - 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 = "" + luaUnprocessed = params.code + end - current_anytime_pathIn = params.pathIn - current_anytime_pathOut = params.pathOut + 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 = {} - else - virtualPathIn = "" - luaUnprocessed = params.code - end + 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 - 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 = {} + -- 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 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 + local tokens = _tokenize(luaUnprocessed, virtualPathIn, true, params.backtickStrings, params.jitSyntax) + -- printTokens(tokens) -- DEBUG - -- 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 + -- Gather info. + local lastTok = tokens[#tokens] - local tokens = _tokenize(luaUnprocessed, virtualPathIn, true, params.backtickStrings, params.jitSyntax) - -- printTokens(tokens) -- DEBUG + 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 = {}, + } - -- Gather info. - local lastTok = tokens[#tokens] + 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 - 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 = {}, - } + -- Generate and run metaprogram. + ---------------------------------------------------------------- - 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 + local shouldProcess = stats.hasPreprocessorCode or params.addLineNumbers - -- Generate and run metaprogram. - ---------------------------------------------------------------- + if shouldProcess then + tokens = doExpansions(params, tokens, stats) + end + stats.tokenCount = #tokens - local shouldProcess = stats.hasPreprocessorCode or params.addLineNumbers + 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 - if shouldProcess then - tokens = doExpansions(params, tokens, stats) - end - stats.tokenCount = #tokens + local lua - 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 + if shouldProcess then + local luaMeta = astToLua(astParse(params, tokens)) + --[[ DEBUG :PrintCode print("=META===============================") print(luaMeta) print("====================================") --]] - -- Run metaprogram. - current_meta_pathForErrorMessages = params.pathMeta or "" - current_meta_output = {} - current_meta_outputStack = {current_meta_output} - current_meta_canOutputNil = params.canOutputNil ~= false - current_meta_releaseMode = params.release + -- Run metaprogram. + current_meta_pathForErrorMessages = params.pathMeta or "" + 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 + 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 + file:write(luaMeta) + file:close() + end - if params.onBeforeMeta then params.onBeforeMeta(luaMeta) 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 + 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 + 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 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 + if current_meta_outputStack[2] then + error("Called startInterceptingOutput() more times than stopInterceptingOutput().") + end - lua = table.concat(current_meta_output) - --[[ DEBUG :PrintCode + 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 + 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 - else - -- @Copypaste from above. - if not current_parsingAndMeta_isDebug and params.pathMeta then - os.remove(params.pathMeta) - end + lua = luaUnprocessed + end - lua = luaUnprocessed - end + current_meta_maxLogLevel = "trace" + current_meta_locationTokens = nil - current_meta_maxLogLevel = "trace" - current_meta_locationTokens = nil + if params.onAfterMeta then + local luaModified = params.onAfterMeta(lua) - 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 - 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. + ---------------------------------------------------------------- - -- Write output file. - ---------------------------------------------------------------- + local pathOut = isFile and params.pathOut or "" - local pathOut = isFile and params.pathOut or "" + 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 - if isFile then - if pathOut == "-" then - io.stdout:write(specialFirstLine or "") - io.stdout:write(lua) + file:write(specialFirstLine or "") + file:write(lua) + file:close() + end + end - else - local file, err = io.open(pathOut, "wb") - if not file then errorf("Count not open '%s' for writing. (%s)", pathOut, err) 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) - file:write(specialFirstLine or "") - file:write(lua) - file:close() - end - end + 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 - -- Check if the output is valid Lua. - if params.validate ~= false then - local luaToCheck = lua:gsub("^#![^\n]*", "") - local chunk, err = loadLuaString(luaToCheck, "@"..pathOut, nil) + -- :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 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 + if params.onDone then + params.onDone(info) + 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, - } + 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 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 + 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 + 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 + -- local startTime = os.clock() -- :DebugMeasureTime @Incomplete: Add processing time to returned info. + local returnValues = nil - current_parsingAndMeta_isProcessing = true - current_parsingAndMeta_isDebug = params.debug + current_parsingAndMeta_isProcessing = true + current_parsingAndMeta_isDebug = params.debug - local xpcallOk, xpcallErr = xpcall( - function() - returnValues = pack(_processFileOrString(params, isFile)) - end, + 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 - 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 - 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) - return err - end - ) + current_parsingAndMeta_isProcessing = false + current_parsingAndMeta_isDebug = false - 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 - -- 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 + -- 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) + 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) + local returnValues = pack(processFileOrString(params, false)) + return unpack(returnValues, 1, returnValues.n) end - - -- :ExportTable local pp = { - -- Processing functions. - ---------------------------------------------------------------- + -- 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, + -- 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, + -- 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. - ---------------------------------------------------------------- + -- Values. + ---------------------------------------------------------------- - VERSION = PP_VERSION, -- The version of LuaPreprocess. - metaEnvironment = metaEnv, -- The environment used for metaprograms. + 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 +for k, v in pairs(metaFuncs) do + pp[k] = v +end return pp - - --[[!=========================================================== Copyright © 2018-2022 Marcus 'ReFreezed' Thunström @@ -3907,4 +4449,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ==============================================================]] - diff --git a/lib/tiny.lua b/lib/tiny.lua index edaf1f1..6e932fa 100644 --- a/lib/tiny.lua +++ b/lib/tiny.lua @@ -30,7 +30,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---@field indices table field is a table of Entity keys to their indices in the entities list. Most Systems can ignore this. ---@field modified boolean indicator for if the System has been modified in the last update. If so, the onModify callback will be called on the System in the next update, if it has one. This is usually managed by tiny-ecs, so users should mostly ignore this, too. - --- @module tiny-ecs -- @author Calvin Rose -- @license MIT @@ -103,14 +102,13 @@ local filterJoin -- A helper function to filters from string local filterBuildString - local function filterJoinRaw(invert, joining_op, ...) - local _args = {...} + local _args = { ... } return function(system, e) local acc local args = _args - if joining_op == 'or' then + if joining_op == "or" then acc = false for i = 1, #args do local v = args[i] @@ -119,7 +117,7 @@ local function filterJoinRaw(invert, joining_op, ...) elseif type(v) == "function" then acc = acc or v(system, e) else - error 'Filter token must be a string or a filter function.' + error("Filter token must be a string or a filter function.") end end else @@ -131,7 +129,7 @@ local function filterJoinRaw(invert, joining_op, ...) elseif type(v) == "function" then acc = acc and v(system, e) else - error 'Filter token must be a string or a filter function.' + error("Filter token must be a string or a filter function.") end end end @@ -146,69 +144,68 @@ local function filterJoinRaw(invert, joining_op, ...) end do - function filterJoin(...) local state, value = pcall(filterJoinRaw, ...) - if state then return value else return nil, value end + if state then + return value + else + return nil, value + end end local function buildPart(str) local accum = {} local subParts = {} - str = str:gsub('%b()', function(p) + str = str:gsub("%b()", function(p) subParts[#subParts + 1] = buildPart(p:sub(2, -2)) - return ('\255%d'):format(#subParts) + return ("\255%d"):format(#subParts) end) - for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do - if part:match('^\255%d+$') then + for invert, part, sep in str:gmatch("(%!?)([^%|%&%!]+)([%|%&]?)") do + if part:match("^\255%d+$") then local partIndex = tonumber(part:match(part:sub(2))) - accum[#accum + 1] = ('%s(%s)') - :format(invert == '' and '' or 'not', subParts[partIndex]) + accum[#accum + 1] = ("%s(%s)"):format(invert == "" and "" or "not", subParts[partIndex]) else - accum[#accum + 1] = ("(e[%s] %s nil)") - :format(make_safe(part), invert == '' and '~=' or '==') + accum[#accum + 1] = ("(e[%s] %s nil)"):format(make_safe(part), invert == "" and "~=" or "==") end - if sep ~= '' then - accum[#accum + 1] = (sep == '|' and ' or ' or ' and ') + if sep ~= "" then + accum[#accum + 1] = (sep == "|" and " or " or " and ") end end return table.concat(accum) end function filterBuildString(str) - local source = ("return function(_, e) return %s end") - :format(buildPart(str)) + local source = ("return function(_, e) return %s end"):format(buildPart(str)) local loader, err = loadstring(source) if err then error(err) end return loader() end - end --- Makes a Filter that selects Entities with all specified Components and -- Filters. function tiny.requireAll(...) - return filterJoin(false, 'and', ...) + return filterJoin(false, "and", ...) end --- Makes a Filter that selects Entities with at least one of the specified -- Components and Filters. function tiny.requireAny(...) - return filterJoin(false, 'or', ...) + return filterJoin(false, "or", ...) end --- Makes a Filter that rejects Entities with all specified Components and -- Filters, and selects all other Entities. function tiny.rejectAll(...) - return filterJoin(true, 'and', ...) + return filterJoin(true, "and", ...) end --- Makes a Filter that rejects Entities with at least one of the specified -- Components and Filters, and selects all other Entities. function tiny.rejectAny(...) - return filterJoin(true, 'or', ...) + return filterJoin(true, "or", ...) end --- Makes a Filter from a string. Syntax of `pattern` is as follows. @@ -224,7 +221,11 @@ end -- @param pattern function tiny.filter(pattern) local state, value = pcall(filterBuildString, pattern) - if state then return value else return nil, value end + if state then + return value + else + return nil, value + end end --- System functions. @@ -463,8 +464,7 @@ function tiny.world(...) entities = {}, -- List of Systems - systems = {} - + systems = {}, }, worldMetaTable) tiny_add(ret, ...) @@ -673,7 +673,6 @@ end -- Adds, removes, and changes Entities that have been marked. function tiny_manageEntities(world) - local e2r = world.entitiesToRemove local e2c = world.entitiesToChange @@ -793,7 +792,6 @@ end -- Systems. If `filter` is not supplied, all Systems are updated. Put this -- function in your main loop. function tiny.update(world, dt, filter) - tiny_manageSystems(world) tiny_manageEntities(world) @@ -809,8 +807,7 @@ function tiny.update(world, dt, filter) onModify(system, dt) end local preWrap = system.preWrap - if preWrap and - ((not filter) or filter(world, system)) then + if preWrap and ((not filter) or filter(world, system)) then preWrap(system, dt) end end @@ -853,12 +850,10 @@ function tiny.update(world, dt, filter) for i = 1, #systems do local system = systems[i] local postWrap = system.postWrap - if postWrap and system.active and - ((not filter) or filter(world, system)) then + if postWrap and system.active and ((not filter) or filter(world, system)) then postWrap(system, dt) end end - end --- Removes all Entities from the World. @@ -924,11 +919,11 @@ worldMetaTable = { clearSystems = tiny.clearSystems, getEntityCount = tiny.getEntityCount, getSystemCount = tiny.getSystemCount, - setSystemIndex = tiny.setSystemIndex + setSystemIndex = tiny.setSystemIndex, }, __tostring = function() return "" - end + end, } _G.tiny = tiny diff --git a/main.lua b/main.lua index 3616e6b..8f64446 100644 --- a/main.lua +++ b/main.lua @@ -9,25 +9,177 @@ require("generated/filter-types") require("generated/assets") require("generated/all-systems") -local scenarios = { - default = function() - -- TODO: Add default entities - end, - textTestScenario = function() +local width, height = love.graphics.getWidth(), love.graphics.getHeight() + +local squareSize = 80 +local tileSize = math.floor(squareSize * 1.2) +local marginSize = 10 + +CursorMask = 1 +BallMask = 2 + +local function emptyTile(unhighlightSiblings, gridPosition) + local size = { x = squareSize, y = squareSize } + return { + unhighlightSiblings = unhighlightSiblings, + canBeCollidedBy = CursorMask, + highlightOnMouseOver = T.marker, + size = size, + gridPosition = gridPosition, + drawAsRectangle = { size = size }, + } +end + +local function replaceAt(grid, y, x, entity) + local current = grid[y][x] + grid[y][x] = entity + + current.unhighlightSiblings[entity] = true + current.unhighlightSiblings[current] = nil + entity.unhighlightSiblings = current.unhighlightSiblings + + -- entity.position = current.position + entity.gridPosition = current.gridPosition + entity.toLeft = current.toLeft + entity.toRight = current.toRight + entity.above = current.above + entity.below = current.below + + current.toLeft.toRight = current.toRight + current.toRight.toLeft = current.toLeft + current.above.below = current.below + current.below.above = current.above + + World:removeEntity(current) + World:addEntity(entity) +end + +function PositionAtGridXy(x, y) + return { + x = 20 + ((x - 1) * tileSize), + y = 60 + ((y - 1) * tileSize), + } +end + +Scenarios = { + ---@return table xyGrid Where grid[y][x] is an Entity + emptyGrid = function() + local cursorSize = { x = 5, y = 5 } World:addEntity({ - position = { x = 0, y = 600 }, - drawAsText = { - text = "Hello, world!", - style = TextStyle.Inverted, + size = cursorSize, + moveWithCursor = T.marker, + canCollideWith = bit.bor(CursorMask, BallMask), + position = { x = -999, y = -999 }, + highlightsCollided = T.marker, + drawAsRectangle = { + size = cursorSize, + mode = "fill", + }, + }) + World:addEntity({ + position = { x = 0, y = 0 }, + drawAsSprite = Grass, + z = 0, + }) + + -- Temporary storages for connecting grid items + local previous + local unhighlightSiblings = {} + local yxGrid = {} + + local xCount = 8 + local yCount = 5 + + for y = 1, yCount do + yxGrid[y] = {} + for x = 1, xCount do + local current = World:addEntity(emptyTile(unhighlightSiblings, { x = x, y = y })) + yxGrid[y][x] = current + unhighlightSiblings[current] = true + current.toLeft = previous + if previous ~= nil then + if previous.toRight == nil then + previous.toRight = current + end + else + current.canReceiveButtons = T.marker + current.highlighted = T.marker + end + if yxGrid[y - 1] and yxGrid[y - 1][x] then + current.above = yxGrid[y - 1][x] + yxGrid[y - 1][x].below = current + end + if y == yCount then + -- Connect last row to first row + yxGrid[1][x].above = current + current.below = yxGrid[1][x] + end + if x == xCount then + -- Connect last entry to first Entry + current.toRight = yxGrid[y][1] + yxGrid[y][1].toLeft = current + end + previous = current + end + end + return yxGrid + end, + + firstLevel = function() + local yxGrid = Scenarios.emptyGrid() + replaceAt(yxGrid, 4, 7, { + drawAsSprite = Flag, + z = 1, + effectsToApply = { + endOfRound = T.marker, + } + }) + replaceAt(yxGrid, 2, 2, { + drawAsSprite = SmallSandTrap, + z = 1, + spriteAfterEffect = SmallSandTrapActivated, + effectsToApply = { + slowsBy = { x = 1, y = 1 }, + movement = { x = 0, y = 0, }, + }, + }) + replaceAt(yxGrid, 1, 1, { + pickedUpOnClick = T.marker, + drawAsSprite = GolferRight, + z = 1, + spriteAfterEffect = GolferRightActivated, + effectsToApply = { + movement = { x = 6, y = 0, }, + }, + }) + replaceAt(yxGrid, 1, 7, { + pickedUpOnClick = T.marker, + drawAsSprite = GolferRight, + z = 1, + spriteAfterEffect = GolferRightActivated, + effectsToApply = { + movement = { x = 0, y = 3 }, + }, + }) + World:addEntity({ + ballEffects = {}, + drawAsSprite = Ball, + gridPosition = { x = 1, y = 1 }, + z = 2, + }) + -- TODO: Make the start button start + World:addEntity({ + drawAsSprite = StartButton, + z = 3, + position = { + x = width - 120, + y = height - 50, }, - velocity = { x = 240, y = -500 }, - mass = 1, - decayAfterSeconds = 10, }) end, } -scenarios.textTestScenario() +Scenarios.firstLevel() function love.load() love.graphics.setBackgroundColor(1, 1, 1) @@ -35,5 +187,15 @@ function love.load() end function love.draw() - World:update(love.timer.getDelta()) + local dt = love.timer.getDelta() + if love.keyboard.isDown("r") then + World:clearEntities() + Scenarios.firstLevel() + end + World:update(dt, function(_, system) + return not system.isDrawSystem + end) + World:update(dt, function(_, system) + return system.isDrawSystem + end) end diff --git a/systems/collision-detection.lua b/systems/collision-detection.lua index 1d2f7a4..866bbe3 100644 --- a/systems/collision-detection.lua +++ b/systems/collision-detection.lua @@ -1,11 +1,24 @@ collidingEntities = filteredSystem("collidingEntitites", { - velocity = T.XyPair, position = T.XyPair, size = T.XyPair, canCollideWith = T.BitMask, isSolid = Maybe(T.bool), }) +local function intersects(rect, rectOther) + local left = rect.position.x + local right = rect.position.x + rect.size.x + local top = rect.position.y + local bottom = rect.position.y + rect.size.y + + local leftOther = rectOther.position.x + local rightOther = rectOther.position.x + rectOther.size.x + local topOther = rectOther.position.y + local bottomOther = rectOther.position.y + rectOther.size.y + + return leftOther < right and left < rightOther and topOther < bottom and top < bottomOther +end + filteredSystem( "collisionDetection", { position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) }, @@ -18,19 +31,7 @@ filteredSystem( and e.canBeCollidedBy and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0 then - local colliderTop = collider.position.y - local colliderBottom = collider.position.y + collider.size.y - local entityTop = e.position.y - local entityBottom = entityTop + e.size.y - - local withinY = (entityTop > colliderTop and entityTop < colliderBottom) - or (entityBottom > colliderTop and entityBottom < colliderBottom) - - if - withinY - and collider.position.x < e.position.x + e.size.x - and collider.position.x + collider.size.x > e.position.x - then + if intersects(e, collider) then system.world:addEntity({ collisionBetween = { e, collider } }) end end diff --git a/systems/collision-resolution.lua b/systems/collision-resolution.lua index 4a54df7..8ae0c32 100644 --- a/systems/collision-resolution.lua +++ b/systems/collision-resolution.lua @@ -1,4 +1,23 @@ -filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system) +Collisions = filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system) local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2] + if collider.highlightsCollided then + if collidedInto.unhighlightSiblings then + for sibling in pairs(collidedInto.unhighlightSiblings) do + sibling.highlighted = nil + sibling.canReceiveButtons = nil + system.world:addEntity(sibling) + end + end + if collidedInto.highlightOnMouseOver ~= nil then + collidedInto.highlighted = T.marker + collidedInto.canReceiveButtons = T.marker + system.world:addEntity(collidedInto) + end + if collidedInto.pickedUpOnClick ~= nil then -- and #HeldByCursor.entities == 0 and love.mouse.isDown(1) then + print("Click!") + collidedInto.moveWithCursor = T.marker + system.world:addEntity(collidedInto) + end + end system.world:removeEntity(e) end) diff --git a/systems/decay.lua b/systems/decay.lua index 1314221..e62b78f 100644 --- a/systems/decay.lua +++ b/systems/decay.lua @@ -3,4 +3,4 @@ filteredSystem("decay", { decayAfterSeconds = T.number }, function(e, dt, system if e.decayAfterSeconds <= 0 then system.world:removeEntity(e) end -end) \ No newline at end of file +end) diff --git a/systems/draw.lua b/systems/draw.lua index 601c9a3..d8cd5ef 100644 --- a/systems/draw.lua +++ b/systems/draw.lua @@ -1,23 +1,52 @@ +local floor = math.floor local gfx = love.graphics +--- +---@generic T +---@param shape T | fun() +---@param process fun(entity: T, dt: number, system: System) | nil +---@return System | { entities: T[] } +local function drawSystem(name, shape, process) + local system = filteredSystem(name, shape, process, function(_, a, b) + if a.z ~= nil and b.z ~= nil then + return a.z < b.z + end + if a.z ~= nil then + return true + end + return false + end) + system.isDrawSystem = true + return system +end -filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _) - gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) +filteredSystem("mapGridPositionToRealPosition", { gridPosition = T.XyPair }, function(e, _, system) + e.position = PositionAtGridXy(e.gridPosition.x, e.gridPosition.y) + system.world:addEntity(e) end) -filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e) - if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then - return +local spriteDrawSystem = drawSystem( + "drawSprites", + { position = T.XyPair, drawAsSprite = T.Drawable, rotation = Maybe(T.number) }, + function(e) + if not e.drawAsSprite then + return + end + local width, height = e.drawAsSprite:getDimensions() + gfx.draw(e.drawAsSprite, e.position.x, e.position.y) end - e.drawAsSprite:draw(e.position.x, e.position.y) -end) +) + +function spriteDrawSystem:preProcess() + gfx.setColor(1, 1, 1) +end local margin = 8 -filteredSystem( +drawSystem( "drawText", { position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } }, function(e) - local font = gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold + local font = e.font or gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold local textHeight = font:getHeight() local textWidth = font:getWidth(e.drawAsText.text) @@ -40,3 +69,9 @@ filteredSystem( gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin) end ) + +drawSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _) + gfx.setColor(1, 1, 1, 0.5) + local mode = e.drawAsRectangle.mode or (e.highlighted and "fill" or "line") + gfx.rectangle(mode, e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) +end) diff --git a/systems/input.lua b/systems/input.lua index 1ae747f..17cb0a9 100644 --- a/systems/input.lua +++ b/systems/input.lua @@ -1,3 +1,5 @@ +local isDown = love.keyboard.isDown + ---@type ButtonState local buttonState = {} @@ -6,8 +8,105 @@ buttonInputSystem = filteredSystem("buttonInput", { canReceiveButtons = T.marker system.world:addEntity(e) end) +HeldByCursor = filteredSystem("HeldByCursor", { pickedUpOnClick = T.marker, moveWithCursor = T.marker }) + function buttonInputSystem:preProcess() if #self.entities == 0 then return end -end \ No newline at end of file +end + +function love.keypressed(key, _, _) + buttonState[key] = true +end + +function ClearButtonState() + for key in pairs(buttonState) do + buttonState[key] = nil + end +end + +---@type System +local cursorTracking + +local mouseX, mouseY = -9999, -9999 +local mouseInControl = false + +function love.mousemoved(x, y) + mouseInControl = true + mouseX, mouseY = x, y +end + +local keyDebounceSec = 0.1 +local delay = 0 + +local menuSystem = filteredSystem( + "menu", + { canReceiveButtons = T.marker, highlighted = T.marker }, + function(e, dt, system) + if delay > 0 then + delay = delay - dt + return + end + + local function tryShiftMenu(target, keys) + if target == nil then + return false + end + for _, key in ipairs(keys) do + if isDown(key) then + e.highlighted = nil + e.canReceiveButtons = nil + target.canReceiveButtons = T.marker + target.highlighted = T.marker + system.world:addEntity(e) + system.world:addEntity(target) + e = target + return true + end + end + return false + end + + local pressed = tryShiftMenu(e.toRight, { "right", "d" }) + pressed = tryShiftMenu(e.toLeft, { "left", "a" }) or pressed + pressed = tryShiftMenu(e.below, { "down", "s" }) or pressed + pressed = tryShiftMenu(e.above, { "up", "w" }) or pressed + + if isDown("return") then + pressed = true + system.world:addEntity({ + round = "start", + }) + end + + if pressed then + mouseInControl = false + mouseX, mouseY = -9999, -9999 + for _, tracker in pairs(cursorTracking.entities) do + tracker.position.x = -9999 + tracker.position.y = -9999 + end + delay = keyDebounceSec + end + end +) + +cursorTracking = filteredSystem("cursorTracking", { moveWithCursor = T.marker, position = T.XyPair }, function(e) + if mouseInControl then + e.position.x = mouseX + e.position.y = mouseY + end +end) + +-- local allHighlighted = filteredSystem("allHighlighted", { highlighted = T.marker }) + +function cursorTracking:postProcess() + -- if mouseInControl and #Collisions.entities == 0 then + -- -- If the cursor is not colliding with anything, wipe all highlighted components + -- for _, highlighted in pairs(allHighlighted.entities) do + -- highlighted.highlighted = nil + -- self.world:addEntity(highlighted) + -- end + -- end +end diff --git a/systems/rounds.lua b/systems/rounds.lua new file mode 100644 index 0000000..ef640fd --- /dev/null +++ b/systems/rounds.lua @@ -0,0 +1,85 @@ +local gridElements = filteredSystem("gridElements", { gridPosition = T.XyPair, effectsToApply = T.AnyComponent }) + +local roundRunning = false + +filteredSystem("timers", { timerSec = T.number, callback = T.SelfFunction }, function(e, dt, system) + e.timerSec = e.timerSec - dt + if e.timerSec < 0 then + e:callback() + system.world:removeEntity(e) + end +end) + +local function sign(n) + return n > 0 and 1 or n < 0 and -1 or 0 +end + +local roundSystem = filteredSystem("rounds", { round = T.str }) + +local activeBallEffects = filteredSystem("activeBallEffects", { ballEffects = T.AnyComponent, gridPosition = T.XyPair }, function(e, dt, system) + local roundActive = false + for _, state in pairs(roundSystem.entities) do + if state.round == "start" then + roundActive = true + end + end + if not roundActive then + return + end + local gridPosition, effects = e.gridPosition, e.ballEffects + + -- Search for new effects from the current tile + for _, gridElement in pairs(gridElements.entities) do + if + gridPosition.x == gridElement.gridPosition.x + and gridPosition.y == gridElement.gridPosition.y + then + -- More direct-mutation-y than I'd like, + -- but offers a simple way to overwrite existing effects. + -- We're "setting InRelations" :D + for key, value in pairs(gridElement.effectsToApply) do + effects[key] = value + end + if gridElement.spriteAfterEffect then + gridElement.drawAsSprite = gridElement.spriteAfterEffect + gridElement.spriteAfterEffect = nil + end + gridElement.effectsToApply = nil + system.world:addEntity(gridElement) + end + end + + -- Apply any effects currently connected to this ball + if effects.movement ~= nil then + gridPosition.x = gridPosition.x + sign(effects.movement.x) + gridPosition.y = gridPosition.y + sign(effects.movement.y) + + effects.movement.x = effects.movement.x - sign(effects.movement.x) + effects.movement.y = effects.movement.y - sign(effects.movement.y) + + if effects.movement.x == 0 and effects.movement.y == 0 then + effects.movement = nil + end + end + + -- TODO: Trigger the round end + -- for _, _ in pairs(effects) do + -- -- Return if there are any effects left + -- return + -- end + + -- system.world:addEntity({ round = "end" }) +end) + +local stepTimeSec = 0.3 +local stepTimer = stepTimeSec + +function activeBallEffects:preProcess(dt) + stepTimer = stepTimer - dt + if stepTimer <= 0 then + stepTimer = stepTimeSec + return + end + return tiny.SKIP_PROCESS +end + diff --git a/systems/velocity.lua b/systems/velocity.lua index 53b1298..944bce9 100644 --- a/systems/velocity.lua +++ b/systems/velocity.lua @@ -13,4 +13,4 @@ filteredSystem("drag", { velocity = T.XyPair, drag = T.number }, function(e, dt, local currentDrag = e.drag * dt e.velocity.x = e.velocity.x - (e.velocity.x * currentDrag * dt) e.velocity.y = e.velocity.y - (e.velocity.y * currentDrag * dt) -end) \ No newline at end of file +end) diff --git a/tiny-debug.lua b/tiny-debug.lua index 793663a..c1c0956 100644 --- a/tiny-debug.lua +++ b/tiny-debug.lua @@ -39,4 +39,4 @@ if tinyWarnWhenNonDataOnEntities then return valType end end -end \ No newline at end of file +end diff --git a/tiny-tools.lua b/tiny-tools.lua index 10259c8..0b4a40c 100644 --- a/tiny-tools.lua +++ b/tiny-tools.lua @@ -1,13 +1,15 @@ ---@generic T ---@param shape T | fun() ---@param process fun(entity: T, dt: number, system: System) | nil +---@param compare nil | fun(system: System, entityA: T, entityB: T): boolean ---@return System | { entities: T[] } -function filteredSystem(name, shape, process) +function filteredSystem(name, shape, process, compare) assert(type(name) == "string") assert(type(shape) == "table" or type(shape) == "function") assert(process == nil or type(process) == "function") - local system = tiny.processingSystem() + local system = compare and tiny.sortedProcessingSystem() or tiny.processingSystem() + system.compare = compare system.name = name if type(shape) == "table" then local keys = {}