Repository navigation
return Nothing from the Luax int getters for inexact numbers - #13
Conversation
maybe-get-int, get-int-global and get-int-field read through lua_tointeger, which gives 0 for a float with a fractional part and narrows its 64-bit result into Int: 2.5, NaN and inf came back as (Just 0), 2^31 as (Just -2147483648), 4294967338 as (Just 42). maybe-get-int now reads the number as a double and returns Just only for a whole value in Int range. Every Int is exact as a double and the conversion from a Lua integer rounds monotonically, so no out-of-range integer can round into range. get-*-global and get-*-field read through the matching maybe-get-*, so all three int getters share the check.
There was a problem hiding this comment.
Build & Tests
I built and ran 62b7f6b on the Pi (armhf, Lua 5.4.4, local carp with the compat core). Each suite ran from the repo root, as CI runs them:
| suite | branch | master |
|---|---|---|
test/lua.carp |
76/0 | 76/0 |
test/midlevel.carp |
86/0 | 64/0 |
test/cfunction.carp |
10/0 | not run |
test/metatable.carp |
22/0 | not run |
test/coroutine.carp |
25/0 | not run |
CI is green on ubuntu-latest and macos-latest.
- Teeth: the branch's
test/midlevel.carpagainst master'slua.carpgives 74/12. The 12 failures are exactly the 12rejectassertions. - Docs:
gendocs.carpruns on both sides. The generateddocs/differ only in the three int docstrings, as the body says. Each one reads correctly ("ReturnsNothingif the global is nil or not a whole number that fits in anInt").
Findings
I found no defects. What I checked:
-
Before-values. On master, all three getters return exactly what the body's table says:
(Just 0)for 2.5, 0/0, 1/0 and 2^53;(Just -2147483648)for 2^31 and(Just -1294967296)for 3e9;(Just -1)formath.maxintegerand(Just 42)for 4294967338;(Just 2)for 2.0.
The branch returns
(Nothing)for all of them except 2.0. -
Independent sweep. I ran 9,083 Lua values through all three getters: 3,532 of integer subtype and 5,551 floats.
- Inputs: ±2^31±k as both integers and floats, ±(2^31 - 0.5), the doubles next to both bounds, 2^53±1 and
math.mininteger/maxinteger. Also9223372036854775808(Lua parses it as a float), -0.0, subnormals, ±NaN, ±inf and 1e308. Plus random int64s, random bit-pattern doubles, and whole and fractional doubles within ±2^33. - Oracle: computed in Python from Lua's own exact value for each input (
math.typeplus%a/tostring), not from the getters. It expectsJust nexactly when the value is finite, whole and in [-2^31, 2^31 - 1]. - Result: the branch matches on all 9,083 rows, through all three getters, and the stack ends at its starting height every time. Master gets all 1,289
Justrows right and all 7,794Nothingrows wrong.
- Inputs: ±2^31±k as both integers and floats, ±(2^31 - 0.5), the doubles next to both bounds, 2^53±1 and
-
The other getters.
maybe-get-double,-float,-booland-stringrun the same type check and read as the inline code they replace (luax--def-maybe-get, lua.carp:42-56).- Inputs: 36 mixed values: nil, numeric strings like
'42'and'0x10', booleans, tables, a function,io.stdout(userdata), a thread, and edge numbers. - Calls: every
maybe-get-*,get-*-globalandget-*-fieldfor int, double, float, bool and string, on master and on the branch. - Result: only the int column differs, and only on the 15 numbers that aren't whole or don't fit in an
Int. Stack heights match, and no value changes type: strings stay strings, and numbers are not converted to strings in place.
- Inputs: 36 mixed values: nil, numeric strings like
-
Mutants. One env-gated build ran the full midlevel suite, which is 86/0 unmutated:
mutant tests failed drop the Int.MINbound3 drop the Int.MAXbound6 >=made strict2 <=made strict1 drop the floortest2 get-int-globalalone reverted to the oldLua.get-intread12 get-int-fieldalone reverted to the old read12 maybe-get-intreverted to the old read12 get-int-globalskips its popsuite aborts (rc 134) get-int-fieldskips its pop4 floorreplaced byceil(an equivalent mutant, as a control)0 So the tests pin both call sites, not just the new helper, and the body's "1 to 6" per clause holds.
-
Callers. None of the 46 other carpentry clones on this machine load lua or use these getters or macros. The repo's examples only read whole numbers through them (
port8080,hp100, 42), and those come out the same. -
LP64. The Pi has no arm64
liblua5.4, so I did not cross-run the suite as 64-bit. The Pi's Lua uses 64-bit integers like CI's (math.maxintegerreads 9223372036854775807), Carp'sIntis 32-bit on both, and CI is green on two 64-bit runners.
Verdict: merge
The fix is minimal and correct. Every before-value and every test and mutation claim in the body reproduced, and an independent 9,083-value sweep found no mismatch on the branch.
Luax.maybe-get-int,get-int-globalandget-int-fieldcheck that the value is a number, then read it withlua_tointeger. That function gives 0 for a float with a fractional part, and its 64-bit result gets narrowed intoInt. So the safe layer handed back confident wrong integers, even though theLuaxdoc promises "you never get garbage". I measured this on master against Lua 5.4, and all three getters agree:2.5,0/0,1/0,2^53(Just 0)(Nothing)2^31(Just -2147483648)(Nothing)3e9(Just -1294967296)(Nothing)math.maxinteger(Just -1)(Nothing)4294967338(Just 42)(Nothing)2.0(Just 2)(Just 2)maybe-get-intnow reads the value as a double, throughmaybe-get-double, and returnsJustonly for a whole number inIntrange. That check is exact for Lua integers as well: everyIntis representable as a double, and the integer-to-double conversion rounds monotonically, so nothing outsideIntrange can round into it. It is pure Carp, with no new C binding.get-*-globalandget-*-fieldnow read through the matchingmaybe-get-*instead of repeating the type check, so the int versions pick up the same rule. Their macros lose thetype-constandgetterarguments. I diffed the gendocs output against master, and only the three int docstrings change.Lua.get-intis untouched.Tests: 22 new assertions in
test/midlevel.carp. They read the values from the topic, plus theIntbounds as Lua integers and4294967338, through all three getters, and useLua.get-topto check the stack on theNothingpaths. On master, the 12Nothingassertions fail. Mutating each clause of the new check (either bound,>=/<=made strict, or dropping thefloortest) fails between 1 and 6 of them. I also ran a sweep of 102,362 values throughmaybe-get-int, usingmath.tointegerplus theIntrange as the oracle. It finds 0 mismatches on this branch and 81,288 on master.Suites run locally on Lua 5.4 / armhf:
lua76,midlevel86 (was 64),cfunction10,metatable22,coroutine25, all with 0 failures. angler and carp-fmt, rebuilt from their current HEAD, are clean over CI's file set, andgendocs.carpruns.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.