Skip to content

return Nothing from the Luax int getters for inexact numbers - #13

Merged
hellerve merged 1 commit into
masterfrom
claude/luax-exact-int
Oct 7, 2026
Merged

hellerve merged 1 commit into
masterfrom
claude/luax-exact-int

Conversation

@carpentry-agent

Copy link
Copy Markdown

Luax.maybe-get-int, get-int-global and get-int-field check that the value is a number, then read it with lua_tointeger. That function gives 0 for a float with a fractional part, and its 64-bit result gets narrowed into Int. So the safe layer handed back confident wrong integers, even though the Luax doc promises "you never get garbage". I measured this on master against Lua 5.4, and all three getters agree:

Lua value master this PR
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-int now reads the value as a double, through maybe-get-double, and returns Just only for a whole number in Int range. That check is exact for Lua integers as well: every Int is representable as a double, and the integer-to-double conversion rounds monotonically, so nothing outside Int range can round into it. It is pure Carp, with no new C binding.

get-*-global and get-*-field now read through the matching maybe-get-* instead of repeating the type check, so the int versions pick up the same rule. Their macros lose the type-const and getter arguments. I diffed the gendocs output against master, and only the three int docstrings change. Lua.get-int is untouched.

Tests: 22 new assertions in test/midlevel.carp. They read the values from the topic, plus the Int bounds as Lua integers and 4294967338, through all three getters, and use Lua.get-top to check the stack on the Nothing paths. On master, the 12 Nothing assertions fail. Mutating each clause of the new check (either bound, >=/<= made strict, or dropping the floor test) fails between 1 and 6 of them. I also ran a sweep of 102,362 values through maybe-get-int, using math.tointeger plus the Int range as the oracle. It finds 0 mismatches on this branch and 81,288 on master.

Suites run locally on Lua 5.4 / armhf: lua 76, midlevel 86 (was 64), cfunction 10, metatable 22, coroutine 25, all with 0 failures. angler and carp-fmt, rebuilt from their current HEAD, are clean over CI's file set, and gendocs.carp runs.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

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.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.carp against master's lua.carp gives 74/12. The 12 failures are exactly the 12 reject assertions.
  • Docs: gendocs.carp runs on both sides. The generated docs/ differ only in the three int docstrings, as the body says. Each one reads correctly ("Returns Nothing if the global is nil or not a whole number that fits in an Int").

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) for math.maxinteger and (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. Also 9223372036854775808 (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.type plus %a/tostring), not from the getters. It expects Just n exactly 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 Just rows right and all 7,794 Nothing rows wrong.
  • The other getters. maybe-get-double, -float, -bool and -string run 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-*-global and get-*-field for 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.
  • Mutants. One env-gated build ran the full midlevel suite, which is 86/0 unmutated:

    mutant tests failed
    drop the Int.MIN bound 3
    drop the Int.MAX bound 6
    >= made strict 2
    <= made strict 1
    drop the floor test 2
    get-int-global alone reverted to the old Lua.get-int read 12
    get-int-field alone reverted to the old read 12
    maybe-get-int reverted to the old read 12
    get-int-global skips its pop suite aborts (rc 134)
    get-int-field skips its pop 4
    floor replaced by ceil (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 (port 8080, hp 100, 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.maxinteger reads 9223372036854775807), Carp's Int is 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.

@hellerve
hellerve marked this pull request as ready for review October 7, 2026 10:29
@hellerve
hellerve merged commit 6dc765f into master Oct 7, 2026
2 checks passed
@hellerve
hellerve deleted the claude/luax-exact-int branch October 7, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant