Luax

provides a safe, higher-level interface over Lua. Where the Lua module returns raw values and status codes, Luax returns Maybe and Result types, manages stack cleanup internally, and provides macros that reduce boilerplate for common patterns.

Safe stack access. The maybe-get-* family type-checks before reading and returns Nothing on mismatch, so you never get garbage from reading the wrong type:

(Lua.push-int lua 42)
(match (Luax.maybe-get-int lua -1)
  (Maybe.Just n) (IO.println &(fmt "got %d" n))
  (Maybe.Nothing) (IO.errorln "not a number"))
(match (Luax.maybe-get-string lua -1)
  (Maybe.Just _) ()  ; won't match — it's an int
  (Maybe.Nothing) (IO.println "correctly rejected"))
(Lua.pop lua 1)

Globals. set-*-global and get-*-global handle the push/pop cycle for you. The getters return Maybe and clean up the stack regardless of success:

(Luax.set-int-global lua "score" 100)
(Luax.set-bool-global lua "debug" true)
(match (Luax.get-int-global lua "score")
  (Maybe.Just n) (IO.println &(fmt "score: %d" n))
  (Maybe.Nothing) (IO.errorln "missing"))

Table fields. get-*-field reads a field from a table already on the stack and pops the field value afterward, so you can read multiple fields in sequence without manual stack management. set-*-field handles the index shift from pushing the value:

(Lua.get-global lua (cstr "player"))
(match (Luax.get-string-field lua -1 "name")
  (Maybe.Just name) (IO.println &name)
  (Maybe.Nothing) (IO.errorln "no name"))
(match (Luax.get-int-field lua -1 "hp")
  (Maybe.Just hp) (IO.println &(fmt "hp: %d" hp))
  (Maybe.Nothing) (IO.errorln "no hp"))
(Luax.set-int-field lua -1 "hp" 0)
(Lua.pop lua 1)

Calling functions. call-fn looks up a global function, pushes arguments, calls it, and reads the result — returning (Error msg) if the function is undefined or raises a Lua error:

(match (Luax.call-fn lua add Lua.get-int
         (Lua.push-int 3) (Lua.push-int 4))
  (Result.Success v) (IO.println &(fmt "3 + 4 = %d" v))
  (Result.Error e) (IO.errorln &e))

Building tables. make-table creates a table, sets fields, and assigns it to a global in one expression:

(Luax.make-table lua point
  (x (Lua.push-int 10))
  (y (Lua.push-int 20)))
; point is now a Lua global with fields x=10, y=20

Code execution. do-in wraps Lua.do-string with Result error handling:

(match (Luax.do-in lua "x = 1 + nil")
  (Result.Success _) ()
  (Result.Error e) (IO.println &(fmt "caught: %s" &e)))

call-fn

macro

Macro

                        (call-fn lua name getter :rest push-exprs)
                    

Call the Lua function name and read the result with getter. Arguments are passed as push expressions—the lua state argument is inserted automatically. Returns (Error msg) if the function is undefined or if the call raises a Lua error.

(Luax.call-fn lua add Lua.get-int
  (Lua.push-int 3)
  (Lua.push-int 4))

do-in

defn

(Fn [(Ref Lua a), (Ref String b)] (Result String String))

                        (do-in lua code)
                    

Compile and execute the Lua string code. Returns (Success "") on success or (Error msg) with the Lua error message on failure. Wraps Lua.do-string with Result handling.

get-carp-str

defn

(Fn [(Ref Lua a), Int] String)

                        (get-carp-str lua index)
                    

Read the value at index as a Carp String, using Lua's tostring coercion. Returns an empty string if the conversion fails.

get-string-global

defn

(Fn [(Ref Lua a), (Ref String b)] (Maybe String))

                        (get-string-global lua name)
                    

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.

make-table

macro

Macro

                        (make-table lua name :rest field-specs)
                    

Create a Lua table with the given fields and assign it to the global name. Each field spec is (field-name (push-expr)), where the lua state argument is inserted into the push expression automatically.

(Luax.make-table lua player
  (name (Lua.push-carp-str "Ada"))
  (hp (Lua.push-int 100)))

maybe-get-userdata

defn

(Fn [(Ref Lua a), Int] (Maybe (Ptr ())))

                        (maybe-get-userdata lua index)
                    

Read the value at index as a userdata pointer, returning Nothing if it is not a full or light userdata. Leaves the stack unchanged.

set-string-global

defn

(Fn [(Ref Lua a), (Ref String b), (Ref String c)] ())

                        (set-string-global lua name value)
                    

Push value and assign it to the global name.