Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions compiler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,21 @@ end

-- numeric literal needs care: keep integers exact
-- mtoint: PUC 5.3+ %d-format guard (string.format("%d", n) errors there for
-- an integral float outside int64 range). nil under LuaJIT/5.1: path unchanged.
-- an integral float outside int64 range). nil under LuaJIT/5.1.
--
-- TWO63: the %d path is only sound for integral values that fit in an int64.
-- LuaJIT/5.1 have no mtoint to fall back on, and there string.format("%d", n)
-- SATURATES instead of erroring -- 1e300 renders as 9223372036854775807, so
-- the literal was destroyed in the generated source, before evaluation:
-- (> 1e300 1e100) compiled to APP(S(">"), 9223372036854775807, ...) => false.
-- Range-check up front on every host; anything outside int64 falls through to
-- the round-tripping %.17g form that catom already uses for the same job.
-- Non-finite values (+-inf, NaN) fail the comparisons and take that same
-- fall-through path they took before -- unchanged, deliberately out of scope.
local mtoint = math.tointeger
local TWO63 = 9223372036854775808.0 -- 2^63, exact as a double
local function cnum(n)
if n == math.floor(n) and n ~= math.huge and n ~= -math.huge then
if n == math.floor(n) and n > -TWO63 and n < TWO63 then
if mtoint then
local i = mtoint(n)
if i then return string.format("%d", i) end
Expand Down
1 change: 1 addition & 0 deletions scripts/run-tests.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ local specs = {
"test/io_spec.lua",
"test/jit_spec.lua",
"test/library_spec.lua",
"test/numeric_literal_spec.lua",
"test/primitives_spec.lua",
"test/prolog_semantics_spec.lua",
"test/reader_spec.lua",
Expand Down
151 changes: 151 additions & 0 deletions test/numeric_literal_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
-- test/numeric_literal_spec.lua — PORT-AUTHORED coverage of numeric LITERAL
-- codegen (compiler.lua `cnum`) and the arithmetic that rides on it.
--
-- Regression guard for the int64 saturation bug: `cnum` rendered every
-- integral float with string.format("%d", n). On LuaJIT/5.1 (no
-- math.tointeger to guard with) that SATURATES at 2^63-1 instead of erroring,
-- so a literal outside int64 range was destroyed in the emitted Lua source,
-- at COMPILE time, before evaluation ever ran:
--
-- (> 1e300 1e100) compiled to APP(S(">"), 9223372036854775807,
-- 9223372036854775807) => false
-- (/ 1e300 1e290) => 1 (both operands saturated to the same value)
-- (= 1e19 (* 1e18 10)) => false
--
-- Silent wrong arithmetic on ordinary FINITE literals. shen-go / shen-cl /
-- shen-rust all answer true / 10000000000 / true.
--
-- OUT OF SCOPE here (known open cross-port questions, deliberately untested):
-- * how a large finite value is RENDERED by the printer — shen-lua and
-- shen-go saturate on the print path, shen-cl and shen-rust print the full
-- positional integer;
-- * non-finite (+-inf, NaN) literals and their rendering.
-- This spec asserts only that the compiled VALUE is right.
--
-- luajit test/numeric_literal_spec.lua
local shen = require("shen")
shen.boot{ quiet = true }
local R = require("runtime")
local C = require("compiler")

local loadstr = loadstring or load

local npass, nfail = 0, 0
local function check(cond, name)
if cond then npass = npass + 1
else
nfail = nfail + 1
io.write("FAIL: ", name, "\n")
end
end
local function evs(src)
return R.to_str(shen.eval(src))
end
local function checkeq(src, want)
local ok, got = pcall(evs, src)
if not ok then
nfail = nfail + 1
io.write("FAIL: ", src, " (raised: ", tostring(got), ")\n")
elseif got == want then
npass = npass + 1
else
nfail = nfail + 1
io.write("FAIL: ", src, "\n want: ", want, "\n got: ", got, "\n")
end
end
-- Bit-exact identity for doubles (%.17g distinguishes every distinct double).
local function same(a, b)
return string.format("%.17g", a) == string.format("%.17g", b)
end

-- ---------------------------------------------------------------------------
-- cnum round-trip: whatever literal we emit must be VALID Lua source and must
-- parse back to the exact same double.
-- ---------------------------------------------------------------------------
local ROUNDTRIP = {
0, 1, -1, 7, -7, 42, -42, 1000000, -1000000,
2^31, 2^52, 2^53, -2^53,
9223372036854774784, -- largest double strictly below 2^63
-9223372036854774784,
2^63, -2^63, 2^64, -- first values the %d path cannot represent
1e19, -1e19, 1e100, 1e300, -1e300, 1e308,
1e-300, 0.1, -0.5, 1.5, 3.5, 1/3, math.pi,
}
for _, v in ipairs(ROUNDTRIP) do
local src = C.cexpr(v, {})
local f = loadstr("return " .. src)
if not f then
nfail = nfail + 1
io.write("FAIL: cnum(", string.format("%.17g", v), ") emitted invalid Lua: ", src, "\n")
else
local back = f()
if same(back, v) then npass = npass + 1
else
nfail = nfail + 1
io.write("FAIL: cnum round-trip for ", string.format("%.17g", v), "\n",
" emitted: ", src, "\n",
" parses back as: ", string.format("%.17g", back), "\n")
end
end
end

-- The specific corruption: no in-range literal may be emitted as the int64
-- saturation sentinel unless it genuinely IS that value.
for _, v in ipairs{ 1e19, 1e100, 1e300, 1e308, 2^63, 2^64 } do
check(C.cexpr(v, {}) ~= "9223372036854775807",
"cnum(" .. string.format("%.17g", v) .. ") must not saturate to 2^63-1")
end

-- ---------------------------------------------------------------------------
-- No regression on ordinary integer literals: they must still print exactly,
-- with no ".0" and no exponent form.
-- ---------------------------------------------------------------------------
local EXACT = {
{0, "0"}, {1, "1"}, {-1, "-1"}, {42, "42"}, {-42, "-42"},
{1000000, "1000000"}, {2^31, "2147483648"},
{2^53, "9007199254740992"}, {-2^53, "-9007199254740992"},
{9223372036854774784, "9223372036854774784"},
}
for _, p in ipairs(EXACT) do
local got = C.cexpr(p[1], {})
check(got == p[2],
"cnum exact integer rendering: want " .. p[2] .. ", got " .. got)
end

-- catom and cnum are two literal paths for the same job; they must agree on
-- the value they denote (they may differ in surface form).
for _, v in ipairs(ROUNDTRIP) do
local a = loadstr("return " .. (string.format("%.17g", v):gsub("%.0$", "")))
local b = loadstr("return " .. C.cexpr(v, {}))
check(a ~= nil and b ~= nil and same(a(), b()),
"catom/cnum agree on " .. string.format("%.17g", v))
end

-- ---------------------------------------------------------------------------
-- End to end: comparisons and arithmetic over large finite literals.
-- Ground truth cross-checked against shen-go, shen-cl and shen-rust.
-- ---------------------------------------------------------------------------
checkeq("(> 1e300 1e100)", "true")
checkeq("(< 1e300 1e100)", "false")
checkeq("(< 1e100 1e300)", "true")
checkeq("(= 1e300 1e100)", "false")
checkeq("(>= 1e300 1e100)", "true")
checkeq("(<= 1e100 1e300)", "true")
checkeq("(/ 1e300 1e290)", "10000000000")
checkeq("(= 1e19 (* 1e18 10))", "true")
checkeq("(- 1e300 1e300)", "0")
checkeq("(/ 1e19 1e10)", "1000000000")
checkeq("(> 1e19 1e18)", "true")
checkeq("(/ 1e100 1e100)", "1")

-- ...and no regression on small / ordinary values.
checkeq("(+ 2 3)", "5")
checkeq("(- 5 3)", "2")
checkeq("(* 6 7)", "42")
checkeq("(= 0 0)", "true")
checkeq("(> 0.2 0.1)", "true")
checkeq("(* 1.5 2)", "3")
checkeq("(= 9007199254740992 (* 4503599627370496 2))", "true")

io.write(string.format("numeric_literal_spec: %d pass, %d fail\n", npass, nfail))
os.exit(nfail == 0 and 0 or 1)
Loading