From 62b7f6b619016ebce431257990f6d1c46e7885ad Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Wed, 7 Oct 2026 03:33:33 +0200 Subject: [PATCH] return Nothing from the Luax int getters for inexact numbers 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. --- lua.carp | 70 ++++++++++++----------------- test/midlevel.carp | 110 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 42 deletions(-) diff --git a/lua.carp b/lua.carp index 7ebb715..06d25df 100644 --- a/lua.carp +++ b/lua.carp @@ -63,8 +63,9 @@ (defn %fn-name [lua name value] (do (%pusher lua value) (Lua.set-global lua (cstr name)))))))) -(defmacro luax--def-get-global [suffix type-const getter article type-article] +(defmacro luax--def-get-global [suffix article type-article] (let [fn-name (Symbol.concat ['get- suffix '-global]) + maybe-get (Symbol.concat ['maybe-get- suffix]) doc-str (Dynamic.String.concat ["Fetch the global `name` as " article @@ -77,11 +78,7 @@ (defn %fn-name [lua name] (do (Lua.get-global lua (cstr name)) - (let-do [result (if (= (Lua.type-of lua -1) %type-const) - (Maybe.Just (%getter lua -1)) - (Maybe.Nothing))] - (Lua.pop lua 1) - result))))))) + (let-do [result (%maybe-get lua -1)] (Lua.pop lua 1) result))))))) (defmacro luax--def-set-field [suffix pusher] (let [fn-name (Symbol.concat ['set- suffix '-field])] @@ -94,8 +91,9 @@ (%pusher lua value) (Lua.set-field lua (if (< index 0) (- index 1) index) (cstr name)))))))) -(defmacro luax--def-get-field [suffix type-const getter article type-article] +(defmacro luax--def-get-field [suffix article type-article] (let [fn-name (Symbol.concat ['get- suffix '-field]) + maybe-get (Symbol.concat ['maybe-get- suffix]) doc-str (Dynamic.String.concat ["Read field `name` from the table at `index` as " article @@ -108,11 +106,7 @@ (defn %fn-name [lua index name] (do (ignore (Lua.get-field lua index (cstr name))) - (let-do [result (if (= (Lua.type-of lua -1) %type-const) - (Maybe.Just (%getter lua -1)) - (Maybe.Nothing))] - (Lua.pop lua 1) - result))))))) + (let-do [result (%maybe-get lua -1)] (Lua.pop lua 1) result))))))) (defmodule Lua (doc OK "Status code returned on success.") @@ -792,7 +786,6 @@ and assigns it to a global in one expression: (defn get-carp-str [lua index] (String.from-cstr-or (Lua.to-string lua index) @"")) - (luax--def-maybe-get int Lua.TYPE_NUMBER Lua.get-int "an integer" "a number") (luax--def-maybe-get double Lua.TYPE_NUMBER Lua.get-double @@ -810,6 +803,19 @@ and assigns it to a global in one expression: "a Carp `String`" "a string") + (defn- exact-int [d] + (if (and (>= d (Double.from-int Int.MIN)) + (<= d (Double.from-int Int.MAX)) + (= d (Double.floor d))) + (Maybe.Just (Double.to-int d)) + (Maybe.Nothing))) + + (doc maybe-get-int "Read the value at `index` as an integer, returning +`Nothing` if it is not a whole number that fits in an `Int`. Leaves the stack +unchanged.") + (defn maybe-get-int [lua index] + (Maybe.and-then (maybe-get-double lua index) &exact-int)) + (doc maybe-get-userdata "Read the value at `index` as a userdata pointer, returning `Nothing` if it is not a full or light userdata. Leaves the stack unchanged.") @@ -830,18 +836,10 @@ unchanged.") (defn set-string-global [lua name value] (do (Lua.push-carp-str lua value) (Lua.set-global lua (cstr name)))) - (luax--def-get-global int Lua.TYPE_NUMBER Lua.get-int "an integer" "a number") - (luax--def-get-global double - Lua.TYPE_NUMBER - Lua.get-double - "a double" - "a number") - (luax--def-get-global float Lua.TYPE_NUMBER Lua.get-float "a float" "a number") - (luax--def-get-global bool - Lua.TYPE_BOOLEAN - Lua.get-bool - "a boolean" - "a boolean") + (luax--def-get-global int "an integer" "a whole number that fits in an `Int`") + (luax--def-get-global double "a double" "a number") + (luax--def-get-global float "a float" "a number") + (luax--def-get-global bool "a boolean" "a boolean") ; `string` defined explicitly for the same reason as set-string-global above. (doc get-string-global "Fetch the global `name` as a Carp `String`. Returns `Nothing` if the global is nil or not a string. Pops the global from the stack internally.") @@ -860,23 +858,11 @@ unchanged.") (luax--def-set-field bool Lua.push-bool) (luax--def-set-field string Lua.push-carp-str) - (luax--def-get-field int Lua.TYPE_NUMBER Lua.get-int "an integer" "a number") - (luax--def-get-field double - Lua.TYPE_NUMBER - Lua.get-double - "a double" - "a number") - (luax--def-get-field float Lua.TYPE_NUMBER Lua.get-float "a float" "a number") - (luax--def-get-field bool - Lua.TYPE_BOOLEAN - Lua.get-bool - "a boolean" - "a boolean") - (luax--def-get-field string - Lua.TYPE_STRING - get-carp-str - "a Carp `String`" - "a string") + (luax--def-get-field int "an integer" "a whole number that fits in an `Int`") + (luax--def-get-field double "a double" "a number") + (luax--def-get-field float "a float" "a number") + (luax--def-get-field bool "a boolean" "a boolean") + (luax--def-get-field string "a Carp `String`" "a string") (doc do-in "Compile and execute the Lua string `code`. Returns `(Success \"\")` diff --git a/test/midlevel.carp b/test/midlevel.carp index 9850430..9a9fb5e 100644 --- a/test/midlevel.carp +++ b/test/midlevel.carp @@ -6,6 +6,27 @@ (add-cflag "-L/opt/homebrew/lib") (Lua.setup "lua") +(defn int-reads [value] + (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua &(str* "x = " value "; t = {x = x}"))) + (Lua.get-global lua (cstr "t")) + (Lua.get-global lua (cstr "x")) + (let-do [direct (Luax.maybe-get-int lua -1)] + (Lua.pop lua 1) + [direct + (Luax.get-int-global lua "x") + (Luax.get-int-field lua -1 "x")]))) + +(defn int-reads-top [value] + (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua &(str* "x = " value "; t = {x = x}"))) + (Lua.get-global lua (cstr "t")) + (Lua.get-global lua (cstr "x")) + (ignore (Luax.maybe-get-int lua -1)) + (ignore (Luax.get-int-global lua "x")) + (ignore (Luax.get-int-field lua -2 "x")) + (Lua.get-top lua))) + (deftest test (assert-true test (= &(Maybe.Just 42) @@ -22,6 +43,95 @@ &(Lua.with-lua-do (Lua.push-nil lua) (Luax.maybe-get-int lua -1))) "maybe-get-int returns Nothing for nil") + (assert-equal test + &(Array.replicate 3 &(Maybe.Just 42)) + &(int-reads "42") + "int getters read a Lua integer") + (assert-equal test + &(Array.replicate 3 &(Maybe.Just 2)) + &(int-reads "2.0") + "int getters read a whole float") + (assert-equal test + &(Array.replicate 3 &(Maybe.Just 0)) + &(int-reads "-0.0") + "int getters read negative zero as 0") + (assert-equal test + &(Array.replicate 3 &(Maybe.Just Int.MAX)) + &(int-reads "2^31 - 1") + "int getters read Int.MAX") + (assert-equal test + &(Array.replicate 3 &(Maybe.Just Int.MIN)) + &(int-reads "-2^31") + "int getters read Int.MIN") + (assert-equal test + &(Array.replicate 3 &(Maybe.Just Int.MIN)) + &(int-reads "-2147483648") + "int getters read Int.MIN as a Lua integer") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "2.5") + "int getters reject a fractional number") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "-2.5") + "int getters reject a negative fractional number") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "2^31") + "int getters reject a float above Int.MAX") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "2147483648") + "int getters reject a Lua integer above Int.MAX") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "-2^31 - 1") + "int getters reject a float below Int.MIN") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "-2147483649") + "int getters reject a Lua integer below Int.MIN") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "4294967338") + "int getters reject a Lua integer that wraps to a small Int") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "2^53") + "int getters reject 2^53") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "math.maxinteger") + "int getters reject math.maxinteger") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "0/0") + "int getters reject NaN") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "1/0") + "int getters reject inf") + (assert-equal test + &(Array.replicate 3 &(Maybe.Nothing)) + &(int-reads "-1/0") + "int getters reject -inf") + (assert-equal test + 2 + (int-reads-top "42") + "int getters leave the stack as documented") + (assert-equal test + 2 + (int-reads-top "2.5") + "int getters leave the stack as documented for a fractional number") + (assert-equal test + 2 + (int-reads-top "2^31") + "int getters leave the stack as documented out of Int range") + (assert-equal test + 2 + (int-reads-top "0/0") + "int getters leave the stack as documented for NaN") + (assert-true test (= &(Maybe.Just 2.5f) &(Lua.with-lua-do (Lua.push-float lua 2.5f) (Luax.maybe-get-float lua -1)))