-- brintos-selftests payload: Lua 5.4 on wasm32-hwjs. -- -- Prints tagged R-lines the driver greps out, then ALL-LUA-OK iff every internal -- assertion held. The centre of gravity is deliberately pcall/error: Lua raises -- EVERY error -- error(), a failed assert, and the VM's own type errors -- through -- LUAI_THROW/LUAI_TRY, which on a C build is a plain setjmp/longjmp pair over a -- struct lua_longjmp chain (ldo.c). That is the known coroutinize hazard on this -- arm, it is not reachable by inspection, and a broken longjmp usually does not -- crash: it returns to the wrong frame and corrupts state quietly. So the cases -- below unwind from nested frames, across C frames, out of coroutines, and 2000 -- times in a row, and then require the interpreter to still be correct. local fail = 0 local function T(name, cond, got) if not cond then fail = fail + 1 io.write("FAIL ", name, " got=", tostring(got), "\n") end end local function R(tag, v) io.write(tag, " ", tostring(v), "\n") end -- R1 -- the interpreter is really 5.4, and its integers are real 64-bit ------ R("R1-version", _VERSION) T("maxinteger", math.maxinteger == 9223372036854775807, math.maxinteger) R("R1-maxint", math.maxinteger) R("R1-intdiv", 7 // 2) R("R1-mod", -7 % 3) R("R1-mathtype", math.type(1) .. "/" .. math.type(1.0)) T("int-wrap", math.maxinteger + 1 == math.mininteger) -- R2 -- tables, closures, the C sort ---------------------------------------- local t = {} for i = 1, 100 do t[i] = (i * 37) % 101 end table.sort(t) R("R2-sorted1", t[1]) R("R2-sorted100", t[100]) T("sort-ordered", (function() for i = 2, #t do if t[i - 1] > t[i] then return false end end return true end)()) local function counter() local n = 0 return function() n = n + 1; return n end end local c = counter(); c(); c() R("R2-closure", c()) R("R2-len", #t) -- R3 -- the string library -------------------------------------------------- R("R3-format", string.format("%s/%d/%.2f", "x", 42, 3.14159)) R("R3-gsub", (("hello world"):gsub("o", "0"))) local fs, fe = ("abcdef"):find("cd") R("R3-find", fs .. ":" .. fe) R("R3-byte", ("A"):byte()) R("R3-rep", ("ab"):rep(3, "-")) -- R4 -- pcall/error: the longjmp core --------------------------------------- local ok, err = pcall(function() error("boom") end) T("pcall-basic", ok == false and tostring(err):find("boom") ~= nil, err) R("R4-msg", (tostring(err):gsub("^.*:%s*", ""))) local function lvl3() error("deep") end local function lvl2() lvl3() end local function lvl1() lvl2() end ok, err = pcall(lvl1) T("pcall-nested", ok == false and tostring(err):find("deep") ~= nil, err) R("R4-nested", (ok == false) and 1 or 0) -- a non-string error value must survive the unwind BY IDENTITY, not by tostring local sentinel = { tag = "sentinel" } ok, err = pcall(function() error(sentinel) end) T("pcall-table", ok == false and err == sentinel, err) R("R4-tableid", (err == sentinel) and 1 or 0) ok, err = pcall(function() error("bare", 0) end) T("pcall-level0", err == "bare", err) -- raised by the VM itself, not by error() ok, err = pcall(function() return 1 + {} end) T("pcall-typeerr", ok == false and tostring(err):find("arithmetic") ~= nil, err) R("R4-typeerr", (ok == false) and 1 or 0) -- assert() raises its message through error() at level 1, so 5.4 prepends -- position info -- match on the message, not on equality. ok, err = pcall(function() assert(false, "asserted") end) T("pcall-assert", ok == false and tostring(err):find("asserted") ~= nil, err) -- THE SHARPEST CASE: the unwind must cross a C frame. table.sort is C, the -- comparator is Lua and raises from inside it. ok, err = pcall(function() table.sort({ 3, 1, 2 }, function(a, b) error("in-comparator") end) end) T("pcall-thru-sort", ok == false and tostring(err):find("in%-comparator") ~= nil, err) R("R4-thruC", (ok == false) and 1 or 0) -- same shape through string.gsub's C frame ok, err = pcall(function() return ("aaa"):gsub("a", function(x) error("in-gsub") end) end) T("pcall-thru-gsub", ok == false and tostring(err):find("in%-gsub") ~= nil, err) local ok2, tb = xpcall(lvl1, debug.traceback) T("xpcall-tb", ok2 == false and tostring(tb):find("stack traceback") ~= nil, tb) R("R4-tb", (tostring(tb):find("stack traceback") ~= nil) and 1 or 0) -- 2000 unwinds in a row, then the interpreter must still be correct. This is the -- case that catches a longjmp landing in the wrong frame, which does not fail on -- the first try -- it accumulates. local caught = 0 for i = 1, 2000 do local o = pcall(function() error("e" .. i) end) if o == false then caught = caught + 1 end end T("pcall-loop", caught == 2000, caught) R("R4-loop", caught) T("alive-after-loop", ("post" .. tostring(1 + 1)) == "post2") local pa, pb, pc = pcall(function() return 1, 2 end) T("pcall-ok-values", pa == true and pb == 1 and pc == 2) -- R5 -- coroutines, and errors crossing them -------------------------------- local co = coroutine.create(function(x) local y = coroutine.yield(x + 1) return y * 2 end) local _, v1 = coroutine.resume(co, 10) local _, v2 = coroutine.resume(co, 5) R("R5-yield", v1) R("R5-return", v2) R("R5-status", coroutine.status(co)) local ce = coroutine.create(function() error("co-boom") end) local cok, cerr = coroutine.resume(ce) T("co-error", cok == false and tostring(cerr):find("co%-boom") ~= nil, cerr) R("R5-coerr", (cok == false) and 1 or 0) -- yield ACROSS a pcall boundary (5.4 allows it) and then raise after resuming: -- the coroutine machinery and the longjmp chain interleaved, which is the pair -- this arm is most likely to get wrong. local cy = coroutine.create(function() local o, e = pcall(function() coroutine.yield("mid") error("after") end) return o, tostring(e) end) local _, mid = coroutine.resume(cy) local _, o3, e3 = coroutine.resume(cy) T("co-yield-across-pcall", mid == "mid" and o3 == false and tostring(e3):find("after") ~= nil, tostring(mid) .. "/" .. tostring(o3)) R("R5-yieldacross", tostring(mid) .. "/" .. tostring(o3)) -- R6 -- metatables ---------------------------------------------------------- local V = {} V.__index = V V.__add = function(a, b) return setmetatable({ x = a.x + b.x }, V) end V.__tostring = function(v) return "V(" .. v.x .. ")" end local p = setmetatable({ x = 3 }, V) local q = setmetatable({ x = 4 }, V) R("R6-add", tostring(p + q)) local base = { greet = function() return "hi" end } R("R6-index", setmetatable({}, { __index = base }).greet()) -- R7 -- io / os round trip -------------------------------------------------- local dir = os.getenv("TMPDIR") if dir == nil or dir == "" then dir = "/tmp" end local path = dir .. "/bst-lua-" .. tostring(os.time()) .. "-" .. tostring(math.random(100000)) .. ".txt" local f = assert(io.open(path, "w")) f:write("line1\nline2\n") f:close() local g = assert(io.open(path, "r")) local first = g:read("l") local rest = g:read("a") g:close() os.remove(path) R("R7-io", first .. "|" .. #rest) T("io-removed", io.open(path, "r") == nil) R("R7-clock", (type(os.clock()) == "number") and 1 or 0) R("R7-date", (os.date("!%Y") ~= nil) and 1 or 0) -- R8 -- varargs, select, goto ---------------------------------------------- local function sum(...) local s = 0 for i = 1, select("#", ...) do s = s + (select(i, ...)) end return s end R("R8-varargs", sum(1, 2, 3, 4, 5)) local n = 0 for i = 1, 10 do if i % 2 == 0 then goto cont end n = n + i ::cont:: end R("R8-goto", n) -- R9 -- the collector actually collects ------------------------------------- collectgarbage("collect") local junk = {} for i = 1, 20000 do junk[i] = { i } end local peak = collectgarbage("count") junk = nil collectgarbage("collect") local after = collectgarbage("count") T("gc-reclaims", after < peak, tostring(after) .. "<" .. tostring(peak)) R("R9-gc", (after < peak) and 1 or 0) local wt = setmetatable({}, { __mode = "v" }) do local tmp = {}; wt[1] = tmp end collectgarbage("collect") R("R9-weak", (wt[1] == nil) and 1 or 0) -- R10 -- load / string.dump (in-process bytecode) --------------------------- R("R10-load", load("return 6 * 7")()) local badc, lerr = load("return +", "=badchunk") T("load-syntax", badc == nil and lerr ~= nil, lerr) R("R10-loaderr", (badc == nil) and 1 or 0) R("R10-dump", load(string.dump(function() return 123 end))()) if fail == 0 then io.write("ALL-LUA-OK\n") else io.write("LUA-FAILURES ", tostring(fail), "\n") end os.exit(fail == 0 and 0 or 1)