diff --git a/README.md b/README.md index 996467b..ff67ecc 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ state and closes it when the block exits: ``` The `Lua` module wraps the Lua C API directly: stack operations -(`push-int`, `push-float`, `push-bool`, `push-string`, `get-int`, etc.), +(`push-int`, `push-double`, `push-bool`, `push-string`, `get-int`, etc.), globals (`get-global`, `set-global`), tables (`create-table`, `set-field`, `get-field`, `next`), code execution (`do-string`, `do-file`, `call`), and type checking (`type-of`, `TYPE_NIL`, `TYPE_NUMBER`, etc.). It also provides a @@ -44,6 +44,14 @@ few conveniences: `Lua.fun` for defining Lua functions inline, `Lua.val` for evaluating Lua expressions into globals, and `Lua.eval-file` for loading Lua files with error handling. +A Lua number is a C double, so `push-double` and `get-double` (and the matching +`Luax.set-double-global`, `Luax.get-double-field`, …) carry Lua floats at full +precision. The `float` versions still work — a float widens into a double +exactly — but `get-float` narrows on the way back, so reading through it drops +everything past single precision. Lua integers are a separate 64-bit subtype; +`get-double` converts them to doubles, so integers above 2^53 are no longer all +exactly representable and may come back rounded. + The `Luax` module provides safe wrappers that return `Maybe` and `Result` types instead of requiring manual type checks: @@ -58,8 +66,8 @@ types instead of requiring manual type checks: (Luax.make-table lua player (name (Lua.push-carp-str "Ada")) (hp (Lua.push-int 100)) - (x (Lua.push-float 3.5f)) - (y (Lua.push-float 7.0f))) + (x (Lua.push-double 3.5)) + (y (Lua.push-double 7.0))) ; read table fields — returns Maybe, keeps the stack clean (Lua.get-global lua (cstr "player")) diff --git a/lua.carp b/lua.carp index a772c1e..7ebb715 100644 --- a/lua.carp +++ b/lua.carp @@ -163,8 +163,10 @@ automatically.") (register push-light-user-data (Fn [&Lua (Ptr ())] ()) "lua_pushlightuserdata") (doc push-nil "Push nil onto the stack.") (register push-nil (Fn [&Lua] ()) "lua_pushnil") - (doc push-float "Push a float (Lua number) onto the stack.") - (register push-float (Fn [&Lua Float] ()) "lua_pushnumber") + (doc push-double "Push a double (Lua number) onto the stack.") + (register push-double (Fn [&Lua Double] ()) "lua_pushnumber") + (doc push-float "Push a float onto the stack as a Lua number.") + (defn push-float [lua f] (push-double lua (Double.from-float f))) (doc push-string "Push a C string onto the stack. For Carp strings, use [`push-carp-str`](#push-carp-str) instead.") (deftemplate push-string @@ -189,9 +191,13 @@ elements on the stack.") (doc get-int "Read the value at `index` as an integer. Does not check the type—use [`Luax.maybe-get-int`](#maybe-get-int) for a safe version.") (register get-int (Fn [&Lua Int] Int) "lua_tointeger") - (doc get-float "Read the value at `index` as a float. Does not check the + (doc get-double "Read the value at `index` as a double. Does not check the +type—use [`Luax.maybe-get-double`](#maybe-get-double) for a safe version.") + (register get-double (Fn [&Lua Int] Double) "lua_tonumber") + (doc get-float "Read the value at `index` as a float. Lua numbers are doubles, +so this narrows; use [`get-double`](#get-double) instead. Does not check the type—use [`Luax.maybe-get-float`](#maybe-get-float) for a safe version.") - (register get-float (Fn [&Lua Int] Float) "lua_tonumber") + (defn get-float [lua index] (Double.to-float (get-double lua index))) (doc get-string "Read the value at `index` as a C string pointer. Returns a raw pointer; prefer [`Luax.get-carp-str`](#get-carp-str) or [`Luax.maybe-get-string`](#maybe-get-string) for safe access.") @@ -595,20 +601,25 @@ the Lua standard library available. ``` Values are passed between Carp and Lua through the stack. Push values with -[`push-int`](#push-int), [`push-float`](#push-float), +[`push-int`](#push-int), [`push-double`](#push-double), [`push-bool`](#push-bool), [`push-carp-str`](#push-carp-str), etc. Read them -back with [`get-int`](#get-int), [`get-float`](#get-float), and so on, using +back with [`get-int`](#get-int), [`get-double`](#get-double), and so on, using negative indices to address from the top of the stack (-1 is the top element). ``` ; push two values, read the top one (Lua.push-int lua 42) -(Lua.push-float lua 3.14f) -(let [f (Lua.get-float lua -1) ; 3.14 - i (Lua.get-int lua -2)] ; 42 +(Lua.push-double lua 3.14) +(let [d (Lua.get-double lua -1) ; 3.14 + i (Lua.get-int lua -2)] ; 42 (Lua.pop lua 2)) ``` +A Lua number is a C double. [`push-float`](#push-float) widens exactly, but +[`get-float`](#get-float) narrows on the way back, so a number read through it +loses everything past single precision and anything above the float range comes +back as an infinity. + To call a Lua function at the low level, push the function, then its arguments, then use [`call`](#call) with the argument and result counts. The result replaces the function and arguments on the stack. @@ -616,9 +627,9 @@ replaces the function and arguments on the stack. ``` (Lua.get-global lua (cstr \"math\")) (ignore (Lua.get-field lua -1 (cstr \"sqrt\"))) -(Lua.push-float lua 9.0f) +(Lua.push-double lua 9.0) (ignore (Lua.call lua 1 1 0)) -(IO.println &(str (Lua.get-float lua -1))) ; 3.0 +(IO.println &(str (Lua.get-double lua -1))) ; 3.0 (Lua.pop lua 2) ; pop result and math table ``` @@ -782,6 +793,11 @@ and assigns it to a global in one expression: (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 + "a double" + "a number") (luax--def-maybe-get float Lua.TYPE_NUMBER Lua.get-float "a float" "a number") (luax--def-maybe-get bool Lua.TYPE_BOOLEAN @@ -804,6 +820,7 @@ unchanged.") (Maybe.Nothing)))) (luax--def-set-global int Lua.push-int) + (luax--def-set-global double Lua.push-double) (luax--def-set-global float Lua.push-float) (luax--def-set-global bool Lua.push-bool) ; `string` is defined explicitly rather than via luax--def-set-global: the @@ -814,6 +831,11 @@ unchanged.") (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 @@ -833,11 +855,17 @@ unchanged.") result))) (luax--def-set-field int Lua.push-int) + (luax--def-set-field double Lua.push-double) (luax--def-set-field float Lua.push-float) (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 diff --git a/test/lua.carp b/test/lua.carp index d246ebd..d57baa4 100644 --- a/test/lua.carp +++ b/test/lua.carp @@ -48,6 +48,59 @@ 2.5f (Lua.with-lua-do (Lua.push-float lua 2.5f) (Lua.get-float lua -1)) "push-float / get-float round-trip") + (assert-equal test + 3.1415926535897931 + (Lua.with-lua-do (Lua.push-double lua 3.1415926535897931) + (Lua.get-double lua -1)) + "push-double / get-double round-trip keeps all 17 significant digits") + (assert-equal test + 16777217.0 + (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua "n = 16777217")) + (Lua.get-global lua (cstr "n")) + (Lua.get-double lua -1)) + "get-double reads a Lua integer above 2^24 intact") + (assert-true test + (let [n (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua "n = 1e300")) + (Lua.get-global lua (cstr "n")) + (Lua.get-double lua -1))] + (and (Double.> n 0.0) (Double.< n Double.MAX))) + "get-double reads 1e300 as a finite value") + (assert-true test + (let [n (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua "n = -1e300")) + (Lua.get-global lua (cstr "n")) + (Lua.get-double lua -1))] + (and (Double.< n 0.0) (Double.> n (Double.neg Double.MAX)))) + "get-double reads -1e300 as a finite value") + (assert-equal test + 3.1415926535897931 + (Lua.with-lua-do (Lua.libs lua) + (Lua.push-double lua 3.1415926535897931) + (Lua.set-global lua (cstr "n")) + (ignore (Luax.do-in lua "n = n * 2 / 2")) + (Lua.get-global lua (cstr "n")) + (Lua.get-double lua -1)) + "push-double hands Lua the full value") + + (assert-equal test + (Double.to-float 3.1415926535897931) + (Lua.with-lua-do (Lua.push-double lua 3.1415926535897931) + (Lua.get-float lua -1)) + "get-float still narrows to single precision") + (assert-true test + (Float.> + (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua "n = 1e300")) + (Lua.get-global lua (cstr "n")) + (Lua.get-float lua -1)) + Float.MAX) + "get-float still overflows past the float range") + (assert-equal test + (Double.from-float 2.5f) + (Lua.with-lua-do (Lua.push-float lua 2.5f) (Lua.get-double lua -1)) + "push-float still accepts a Float") (assert-true test (Lua.with-lua-do (Lua.push-bool lua true) (Lua.get-bool lua -1)) "push-bool true round-trip") diff --git a/test/midlevel.carp b/test/midlevel.carp index 261b8e4..9850430 100644 --- a/test/midlevel.carp +++ b/test/midlevel.carp @@ -31,6 +31,22 @@ &(Lua.with-lua-do (Lua.push-bool lua true) (Luax.maybe-get-float lua -1))) "maybe-get-float returns Nothing for boolean value") + (assert-true test + (= &(Maybe.Just 3.1415926535897931) + &(Lua.with-lua-do (Lua.push-double lua 3.1415926535897931) + (Luax.maybe-get-double lua -1))) + "maybe-get-double returns Just for number value") + (assert-true test + (Maybe.nothing? + &(Lua.with-lua-do (Lua.push-bool lua true) (Luax.maybe-get-double lua -1))) + "maybe-get-double returns Nothing for boolean value") + (assert-true test + (Maybe.nothing? + &(Lua.with-lua-do + (ignore (Lua.push-string lua (cstr "hello"))) + (Luax.maybe-get-double lua -1))) + "maybe-get-double returns Nothing for string value") + (assert-true test (= &(Maybe.Just true) &(Lua.with-lua-do (Lua.push-bool lua true) (Luax.maybe-get-bool lua -1))) @@ -191,6 +207,22 @@ (Lua.get-float lua -1)) "set-float-global sets a float global") + (assert-equal test + 3.1415926535897931 + (Lua.with-lua-do (Lua.libs lua) + (Luax.set-double-global lua "d" 3.1415926535897931) + (Lua.get-global lua (cstr "d")) + (Lua.get-double lua -1)) + "set-double-global sets a double global without losing precision") + (assert-true test + (let [n (Lua.with-lua-do (Lua.libs lua) + (ignore (Luax.do-in lua "d = 1e300")) + (ignore (Luax.do-in lua "d = d + 0")) + (Lua.get-global lua (cstr "d")) + (Lua.get-double lua -1))] + (and (Double.> n 0.0) (Double.< n Double.MAX))) + "double globals survive a round trip through Lua arithmetic") + (assert-true test (Lua.with-lua-do (Lua.libs lua) (Luax.set-bool-global lua "flag" true) @@ -238,6 +270,23 @@ (Luax.get-float-global lua "f"))) "get-float-global returns Just for existing float") + (assert-true test + (= &(Maybe.Just 3.1415926535897931) + &(Lua.with-lua-do (Lua.libs lua) + (Luax.set-double-global lua "d" 3.1415926535897931) + (Luax.get-double-global lua "d"))) + "get-double-global returns Just for existing number") + (assert-true test + (Maybe.nothing? + &(Lua.with-lua-do (Lua.libs lua) (Luax.get-double-global lua "nope"))) + "get-double-global returns Nothing for missing global") + (assert-true test + (Maybe.nothing? + &(Lua.with-lua-do (Lua.libs lua) + (Luax.set-string-global lua "s" "hello") + (Luax.get-double-global lua "s"))) + "get-double-global returns Nothing for wrong type") + (assert-true test (= &(Maybe.Just true) &(Lua.with-lua-do (Lua.libs lua) @@ -302,6 +351,24 @@ (Luax.get-float-field lua -1 "val"))) "get-float-field returns Just for existing float field") + (assert-true test + (= &(Maybe.Just 3.1415926535897931) + &(Lua.with-lua-do (Lua.libs lua) + (ignore + (Lua.do-string lua + (cstr "t = {val = 3.141592653589793}"))) + (Lua.get-global lua (cstr "t")) + (Luax.get-double-field lua -1 "val"))) + "get-double-field returns Just for existing number field") + (assert-true test + (Maybe.nothing? + &(Lua.with-lua-do (Lua.libs lua) + (ignore + (Lua.do-string lua (cstr "t = {name = 'Alice'}"))) + (Lua.get-global lua (cstr "t")) + (Luax.get-double-field lua -1 "name"))) + "get-double-field returns Nothing on type mismatch") + (assert-true test (= &(Maybe.Just true) &(Lua.with-lua-do (Lua.libs lua) @@ -344,6 +411,17 @@ (Lua.get-float lua -1)) "set-float-field sets a float field on a table") + (assert-equal test + 3.1415926535897931 + (Lua.with-lua-do (Lua.libs lua) + (Lua.create-table lua 0 1) + (Luax.set-double-field lua -1 "val" 3.1415926535897931) + (Lua.set-global lua (cstr "t")) + (Lua.get-global lua (cstr "t")) + (ignore (Lua.get-field lua -1 (cstr "val"))) + (Lua.get-double lua -1)) + "set-double-field sets a double field without losing precision") + (assert-true test (Lua.with-lua-do (Lua.libs lua) (Lua.create-table lua 0 1)