From 3a5473c86cd69131175a4ada63cf0a431491674b Mon Sep 17 00:00:00 2001 From: selenil Date: Fri, 13 Feb 2026 16:25:34 -0500 Subject: [PATCH 01/19] add action type and rewrite api --- src/glua.gleam | 195 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 129 insertions(+), 66 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 8cd78d0..6c5db05 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -216,6 +216,43 @@ fn format_lua_value(v: anything) -> String @external(erlang, "luerl_lib", "format_error") fn format_unknown_error(error: dynamic.Dynamic) -> String +pub opaque type Action(return) { + Action(function: fn(Lua) -> Result(#(Lua, return), LuaError)) +} + +pub fn run(state lua: Lua, action action: Action(a)) -> Result(a, LuaError) { + // drop the updated state by desing + action.function(lua) |> result.map(pair.second) +} + +pub fn then(action: Action(a), next: fn(a) -> Action(b)) -> Action(b) { + use state <- Action + use #(new, ret) <- result.try(action.function(state)) + + next(ret).function(new) +} + +pub fn success(value: a) -> Action(a) { + use state <- Action + Ok(#(state, value)) +} + +pub fn map(over action: Action(a), with fun: fn(a) -> b) -> Action(b) { + use state <- Action + action.function(state) + |> result.map(pair.map_second(_, fun)) +} + +pub fn fold(over list: List(a), with fun: fn(a) -> Action(b)) -> Action(List(b)) { + use state <- Action + list.try_fold(list, #(state, []), fn(acc, e) { + let #(state, results) = acc + fun(e).function(state) + |> result.map(pair.map_second(_, fn(ret) { [ret, ..results] })) + }) + |> result.map(pair.map_second(_, list.reverse)) +} + /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk @@ -347,18 +384,30 @@ fn do_function( /// assert glua.dereference(state:, ref: ref2, using: decode.bool) == Ok(True) /// ``` pub fn dereference( - state lua: Lua, ref ref: Value, using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { - do_dereference(lua, ref) - |> decode.run(decoder) - |> result.map_error(UnexpectedResultType) +) -> Action(a) { + use state <- Action + use ret <- result.map( + do_dereference(state, ref) + |> decode.run(decoder) + |> result.map_error(UnexpectedResultType), + ) + + #(state, ret) } @external(erlang, "glua_ffi", "dereference") fn do_dereference(lua: Lua, ref: Value) -> dynamic.Dynamic +pub fn returning( + over: Action(List(Value)), + using decoder: decode.Decoder(a), +) -> Action(List(a)) { + use refs <- then(over) + fold(refs, dereference(_, decoder)) +} + /// Creates a new Lua VM instance @external(erlang, "luerl", "init") pub fn new() -> Lua @@ -413,7 +462,9 @@ fn list_substraction(a: List(a), b: List(a)) -> List(a) /// ``` pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) { let msg = string.join(keys, with: ".") <> " is sandboxed" - set(lua, ["_G", ..keys], sandbox_fun(msg)) + + set(["_G", ..keys], sandbox_fun(msg)).function(lua) + |> result.map(pair.first) } @external(erlang, "glua_ffi", "sandbox_fun") @@ -446,8 +497,14 @@ fn sandbox_fun(msg: String) -> Value /// glua.get(state: glua.new(), keys: ["non_existent"]) /// // -> Error(glua.KeyNotFound(["non_existent"])) /// ``` +pub fn get(keys keys: List(String)) -> Action(Value) { + use state <- Action + use ret <- result.map(do_get(state, keys)) + #(state, ret) +} + @external(erlang, "glua_ffi", "get_table_keys") -pub fn get(state lua: Lua, keys keys: List(String)) -> Result(Value, LuaError) +fn do_get(lua: Lua, keys: List(String)) -> Result(Value, LuaError) /// Gets a private value that is not exposed to the Lua runtime. /// @@ -511,30 +568,26 @@ fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) /// /// assert results == emails /// ``` -pub fn set( - state lua: Lua, - keys keys: List(String), - value val: Value, -) -> Result(Lua, LuaError) { - let state = { - use acc, key <- list.try_fold(keys, #([], lua)) - let #(keys, lua) = acc - let keys = list.append(keys, [key]) - case get(lua, keys) { - Ok(_) -> Ok(#(keys, lua)) - - Error(KeyNotFound(_)) -> { - let #(tbl, lua) = do_table([], lua) - do_set(lua, keys, tbl) - |> result.map(fn(lua) { #(keys, lua) }) +pub fn set(keys keys: List(String), value val: Value) -> Action(Nil) { + use state <- Action + use #(new, keys) <- result.try( + list.try_fold(keys, #(state, []), fn(acc, k) { + let #(state, _) = acc + case do_get(state, list.append(keys, [k])) { + Ok(_) -> Ok(#(state, keys)) + + Error(KeyNotFound(_)) -> { + let #(tbl, new) = do_table([], state) + do_set(new, keys, tbl) + |> result.map(fn(pair) { #(pair.0, keys) }) + } + + Error(e) -> Error(e) } + }), + ) - Error(e) -> Error(e) - } - } - - use #(keys, lua) <- result.try(state) - do_set(lua, keys, val) + do_set(new, keys, val) } /// Sets a value that is not exposed to the Lua runtime and can only be accessed from Gleam. @@ -552,12 +605,14 @@ pub fn set_private(state lua: Lua, key key: String, value value: a) -> Lua { /// Sets a group of values under a particular table in the Lua environment. pub fn set_api( - lua: Lua, keys: List(String), values: List(#(String, Value)), -) -> Result(Lua, LuaError) { - use state, #(key, val) <- list.try_fold(values, lua) - set(state, list.append(keys, [key]), val) +) -> Action(Nil) { + use _ <- then( + fold(values, fn(pair) { set(list.append(keys, [pair.0]), pair.1) }), + ) + + success(Nil) } /// Sets the paths where the Lua runtime will look when requiring other Lua files. @@ -582,16 +637,13 @@ pub fn set_api( /// glua.dereference(state:, ref:, using: decode.int) /// // -> Ok(9) /// ``` -pub fn set_lua_paths( - state lua: Lua, - paths paths: List(String), -) -> Result(Lua, LuaError) { +pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil) { let paths = string.join(paths, with: ";") |> string - set(lua, ["package", "path"], paths) + set(["package", "path"], paths) } @external(erlang, "glua_ffi", "set_table_keys") -fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) +fn do_set(lua: Lua, keys: List(String), val: a) -> Result(#(Lua, Nil), LuaError) @external(erlang, "luerl", "put_private") fn do_set_private(key: String, value: a, lua: Lua) -> Lua @@ -618,20 +670,22 @@ fn do_delete_private(key: String, lua: Lua) -> Lua /// Parses a string of Lua code and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. +pub fn load(code code: String) -> Action(Chunk) { + Action(do_load(_, code)) +} + @external(erlang, "glua_ffi", "load") -pub fn load( - state lua: Lua, - code code: String, -) -> Result(#(Lua, Chunk), LuaError) +fn do_load(lua: Lua, code: String) -> Result(#(Lua, Chunk), LuaError) /// Parses a Lua source file and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. +pub fn load_file(path: String) -> Action(Chunk) { + Action(do_load_file(_, path)) +} + @external(erlang, "glua_ffi", "load_file") -pub fn load_file( - state lua: Lua, - path path: String, -) -> Result(#(Lua, Chunk), LuaError) +fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError) /// Evaluates a string of Lua code. /// @@ -666,11 +720,12 @@ pub fn load_file( /// > instead of calling `glua.eval` repeatly it is recommended to first convert /// > the code to a chunk by passing it to `glua.load`, and then /// > evaluate that chunk using `glua.eval_chunk`. +pub fn eval(code: String) -> Action(List(Value)) { + Action(do_eval(_, code)) +} + @external(erlang, "glua_ffi", "eval") -pub fn eval( - state lua: Lua, - code code: String, -) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError) /// Evaluates a compiled chunk of Lua code. /// @@ -688,10 +743,14 @@ pub fn eval( /// glua.dereference(state:, ref:, using: decode.string) /// // -> Ok("hello, world!") /// ``` +pub fn eval_chunk(chunk: Chunk) -> Action(List(Value)) { + Action(do_eval_chunk(_, chunk)) +} + @external(erlang, "glua_ffi", "eval_chunk") -pub fn eval_chunk( - state lua: Lua, - chunk chunk: Chunk, +fn do_eval_chunk( + lua: Lua, + chunk: Chunk, ) -> Result(#(Lua, List(Value)), LuaError) /// Evaluates a Lua source file. @@ -713,11 +772,12 @@ pub fn eval_chunk( /// ) /// //-> Error(glua.FileNotFound(["path/to/non/existent/file"])) /// ``` +pub fn eval_file(path: String) -> Action(List(Value)) { + Action(do_eval_file(_, path)) +} + @external(erlang, "glua_ffi", "eval_file") -pub fn eval_file( - state lua: Lua, - path path: String, -) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval_file(lua: Lua, path: String) -> Result(#(Lua, List(Value)), LuaError) /// Calls a Lua function by reference. /// @@ -753,11 +813,15 @@ pub fn eval_file( /// glua.dereference(state:, ref:, using: decode.int) /// // -> Ok(55) /// ``` +pub fn call_function(fun: Value, args: List(Value)) -> Action(List(Value)) { + Action(do_call_function(_, fun, args)) +} + @external(erlang, "glua_ffi", "call_function") -pub fn call_function( - state lua: Lua, - ref fun: Value, - args args: List(Value), +fn do_call_function( + lua: Lua, + fun: Value, + args: List(Value), ) -> Result(#(Lua, List(Value)), LuaError) /// Gets a reference to the function at `keys`, then inmediatly calls it with the provided `args`. @@ -776,10 +840,9 @@ pub fn call_function( /// // -> Ok(HELLO FROM GLEAM!") /// ``` pub fn call_function_by_name( - state lua: Lua, keys keys: List(String), args args: List(Value), -) -> Result(#(Lua, List(Value)), LuaError) { - use fun <- result.try(get(lua, keys)) - call_function(lua, fun, args) +) -> Action(List(Value)) { + use fun <- then(get(keys)) + call_function(fun, args) } From 10fc73c332bd2ad872b822cb4a53d492f9928a8e Mon Sep 17 00:00:00 2001 From: selenil Date: Mon, 9 Feb 2026 16:10:35 -0500 Subject: [PATCH 02/19] fix set function --- src/glua.gleam | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 6c5db05..be238cd 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -571,15 +571,16 @@ fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) pub fn set(keys keys: List(String), value val: Value) -> Action(Nil) { use state <- Action use #(new, keys) <- result.try( - list.try_fold(keys, #(state, []), fn(acc, k) { - let #(state, _) = acc - case do_get(state, list.append(keys, [k])) { + list.try_fold(keys, #(state, []), fn(acc, key) { + let #(state, keys) = acc + let keys = list.append(keys, [key]) + case do_get(state, keys) { Ok(_) -> Ok(#(state, keys)) Error(KeyNotFound(_)) -> { let #(tbl, new) = do_table([], state) - do_set(new, keys, tbl) - |> result.map(fn(pair) { #(pair.0, keys) }) + use new <- result.map(do_set(new, keys, tbl)) + #(new, keys) } Error(e) -> Error(e) @@ -588,6 +589,7 @@ pub fn set(keys keys: List(String), value val: Value) -> Action(Nil) { ) do_set(new, keys, val) + |> result.map(fn(state) { #(state, Nil) }) } /// Sets a value that is not exposed to the Lua runtime and can only be accessed from Gleam. @@ -643,7 +645,7 @@ pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil) { } @external(erlang, "glua_ffi", "set_table_keys") -fn do_set(lua: Lua, keys: List(String), val: a) -> Result(#(Lua, Nil), LuaError) +fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) @external(erlang, "luerl", "put_private") fn do_set_private(key: String, value: a, lua: Lua) -> Lua From a23edd1740493035f3e12e6a69de79b1c5aeeb36 Mon Sep 17 00:00:00 2001 From: selenil Date: Fri, 13 Feb 2026 16:26:37 -0500 Subject: [PATCH 03/19] add CustomError constructor and failure function --- src/glua.gleam | 90 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index be238cd..169956e 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -15,7 +15,7 @@ import gleam/string pub type Lua /// Represents the errors than can happend during the parsing and execution of Lua code -pub type LuaError { +pub type LuaError(error) { /// The compilation process of the Lua code failed because of the presence of one or more compile errors. LuaCompileFailure(errors: List(LuaCompileError)) /// The Lua environment threw an exception during code execution. @@ -26,6 +26,8 @@ pub type LuaError { FileNotFound(path: String) /// The value returned by the Lua environment could not be decoded using the provided decoder. UnexpectedResultType(List(decode.DecodeError)) + /// An app-defined error + CustomError(error: error) /// An error that could not be identified. UnknownError(error: dynamic.Dynamic) } @@ -115,7 +117,7 @@ pub type LuaRuntimeExceptionKind { /// glua.format_error(e) /// // -> "Expected String, but found Int" /// ``` -pub fn format_error(error: LuaError) -> String { +pub fn format_error(error: LuaError(e)) -> String { case error { LuaCompileFailure(errors) -> "Lua compile error: " @@ -136,6 +138,7 @@ pub fn format_error(error: LuaError) -> String { "Lua source file " <> "\"" <> path <> "\"" <> " not found" UnexpectedResultType(decode_errors) -> list.map(decode_errors, format_decode_error) |> string.join(with: "\n") + CustomError(error) -> string.inspect(error) UnknownError(error) -> "Unknown error: " <> format_unknown_error(error) } } @@ -216,34 +219,45 @@ fn format_lua_value(v: anything) -> String @external(erlang, "luerl_lib", "format_error") fn format_unknown_error(error: dynamic.Dynamic) -> String -pub opaque type Action(return) { - Action(function: fn(Lua) -> Result(#(Lua, return), LuaError)) +pub opaque type Action(return, error) { + Action(function: fn(Lua) -> Result(#(Lua, return), LuaError(error))) } -pub fn run(state lua: Lua, action action: Action(a)) -> Result(a, LuaError) { +pub fn run( + state lua: Lua, + action action: Action(return, error), +) -> Result(return, LuaError(error)) { // drop the updated state by desing action.function(lua) |> result.map(pair.second) } -pub fn then(action: Action(a), next: fn(a) -> Action(b)) -> Action(b) { +pub fn then(action: Action(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { use state <- Action use #(new, ret) <- result.try(action.function(state)) next(ret).function(new) } -pub fn success(value: a) -> Action(a) { +pub fn success(value: a) -> Action(a, e) { use state <- Action Ok(#(state, value)) } -pub fn map(over action: Action(a), with fun: fn(a) -> b) -> Action(b) { +pub fn failure(error: e) -> Action(a, e) { + use _ <- Action + Error(CustomError(error)) +} + +pub fn map(over action: Action(a, e), with fun: fn(a) -> b) -> Action(b, e) { use state <- Action action.function(state) |> result.map(pair.map_second(_, fun)) } -pub fn fold(over list: List(a), with fun: fn(a) -> Action(b)) -> Action(List(b)) { +pub fn fold( + over list: List(a), + with fun: fn(a) -> Action(b, e), +) -> Action(List(b), e) { use state <- Action list.try_fold(list, #(state, []), fn(acc, e) { let #(state, results) = acc @@ -386,7 +400,7 @@ fn do_function( pub fn dereference( ref ref: Value, using decoder: decode.Decoder(a), -) -> Action(a) { +) -> Action(a, e) { use state <- Action use ret <- result.map( do_dereference(state, ref) @@ -401,9 +415,9 @@ pub fn dereference( fn do_dereference(lua: Lua, ref: Value) -> dynamic.Dynamic pub fn returning( - over: Action(List(Value)), + over: Action(List(Value), e), using decoder: decode.Decoder(a), -) -> Action(List(a)) { +) -> Action(List(a), e) { use refs <- then(over) fold(refs, dereference(_, decoder)) } @@ -439,7 +453,7 @@ pub const default_sandbox = [ /// In case you want to sandbox more Lua values, pass to `glua.sandbox` the returned Lua state. pub fn new_sandboxed( allow excluded: List(List(String)), -) -> Result(Lua, LuaError) { +) -> Result(Lua, LuaError(e)) { list_substraction(default_sandbox, excluded) |> list.try_fold(from: new(), with: sandbox) } @@ -460,7 +474,10 @@ fn list_substraction(a: List(a), b: List(a)) -> List(a) /// // 'important_file' was not deleted /// assert exception == glua.ErrorCall(["os.execute is sandboxed"]) /// ``` -pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) { +pub fn sandbox( + state lua: Lua, + keys keys: List(String), +) -> Result(Lua, LuaError(e)) { let msg = string.join(keys, with: ".") <> " is sandboxed" set(["_G", ..keys], sandbox_fun(msg)).function(lua) @@ -497,14 +514,14 @@ fn sandbox_fun(msg: String) -> Value /// glua.get(state: glua.new(), keys: ["non_existent"]) /// // -> Error(glua.KeyNotFound(["non_existent"])) /// ``` -pub fn get(keys keys: List(String)) -> Action(Value) { +pub fn get(keys keys: List(String)) -> Action(Value, e) { use state <- Action use ret <- result.map(do_get(state, keys)) #(state, ret) } @external(erlang, "glua_ffi", "get_table_keys") -fn do_get(lua: Lua, keys: List(String)) -> Result(Value, LuaError) +fn do_get(lua: Lua, keys: List(String)) -> Result(Value, LuaError(e)) /// Gets a private value that is not exposed to the Lua runtime. /// @@ -520,13 +537,13 @@ pub fn get_private( state lua: Lua, key key: String, using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { +) -> Result(a, LuaError(e)) { use value <- result.try(do_get_private(lua, key)) decode.run(value, decoder) |> result.map_error(UnexpectedResultType) } @external(erlang, "glua_ffi", "get_private") -fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) +fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError(e)) /// Sets a value in the Lua environment. /// @@ -568,7 +585,7 @@ fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) /// /// assert results == emails /// ``` -pub fn set(keys keys: List(String), value val: Value) -> Action(Nil) { +pub fn set(keys keys: List(String), value val: Value) -> Action(Nil, e) { use state <- Action use #(new, keys) <- result.try( list.try_fold(keys, #(state, []), fn(acc, key) { @@ -609,7 +626,7 @@ pub fn set_private(state lua: Lua, key key: String, value value: a) -> Lua { pub fn set_api( keys: List(String), values: List(#(String, Value)), -) -> Action(Nil) { +) -> Action(Nil, e) { use _ <- then( fold(values, fn(pair) { set(list.append(keys, [pair.0]), pair.1) }), ) @@ -639,13 +656,13 @@ pub fn set_api( /// glua.dereference(state:, ref:, using: decode.int) /// // -> Ok(9) /// ``` -pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil) { +pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil, e) { let paths = string.join(paths, with: ";") |> string set(["package", "path"], paths) } @external(erlang, "glua_ffi", "set_table_keys") -fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) +fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError(e)) @external(erlang, "luerl", "put_private") fn do_set_private(key: String, value: a, lua: Lua) -> Lua @@ -672,22 +689,22 @@ fn do_delete_private(key: String, lua: Lua) -> Lua /// Parses a string of Lua code and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. -pub fn load(code code: String) -> Action(Chunk) { +pub fn load(code code: String) -> Action(Chunk, e) { Action(do_load(_, code)) } @external(erlang, "glua_ffi", "load") -fn do_load(lua: Lua, code: String) -> Result(#(Lua, Chunk), LuaError) +fn do_load(lua: Lua, code: String) -> Result(#(Lua, Chunk), LuaError(e)) /// Parses a Lua source file and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. -pub fn load_file(path: String) -> Action(Chunk) { +pub fn load_file(path: String) -> Action(Chunk, e) { Action(do_load_file(_, path)) } @external(erlang, "glua_ffi", "load_file") -fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError) +fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError(e)) /// Evaluates a string of Lua code. /// @@ -722,12 +739,12 @@ fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError) /// > instead of calling `glua.eval` repeatly it is recommended to first convert /// > the code to a chunk by passing it to `glua.load`, and then /// > evaluate that chunk using `glua.eval_chunk`. -pub fn eval(code: String) -> Action(List(Value)) { +pub fn eval(code: String) -> Action(List(Value), e) { Action(do_eval(_, code)) } @external(erlang, "glua_ffi", "eval") -fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError(e)) /// Evaluates a compiled chunk of Lua code. /// @@ -745,7 +762,7 @@ fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError) /// glua.dereference(state:, ref:, using: decode.string) /// // -> Ok("hello, world!") /// ``` -pub fn eval_chunk(chunk: Chunk) -> Action(List(Value)) { +pub fn eval_chunk(chunk: Chunk) -> Action(List(Value), e) { Action(do_eval_chunk(_, chunk)) } @@ -753,7 +770,7 @@ pub fn eval_chunk(chunk: Chunk) -> Action(List(Value)) { fn do_eval_chunk( lua: Lua, chunk: Chunk, -) -> Result(#(Lua, List(Value)), LuaError) +) -> Result(#(Lua, List(Value)), LuaError(e)) /// Evaluates a Lua source file. /// @@ -774,12 +791,15 @@ fn do_eval_chunk( /// ) /// //-> Error(glua.FileNotFound(["path/to/non/existent/file"])) /// ``` -pub fn eval_file(path: String) -> Action(List(Value)) { +pub fn eval_file(path: String) -> Action(List(Value), e) { Action(do_eval_file(_, path)) } @external(erlang, "glua_ffi", "eval_file") -fn do_eval_file(lua: Lua, path: String) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval_file( + lua: Lua, + path: String, +) -> Result(#(Lua, List(Value)), LuaError(e)) /// Calls a Lua function by reference. /// @@ -815,7 +835,7 @@ fn do_eval_file(lua: Lua, path: String) -> Result(#(Lua, List(Value)), LuaError) /// glua.dereference(state:, ref:, using: decode.int) /// // -> Ok(55) /// ``` -pub fn call_function(fun: Value, args: List(Value)) -> Action(List(Value)) { +pub fn call_function(fun: Value, args: List(Value)) -> Action(List(Value), e) { Action(do_call_function(_, fun, args)) } @@ -824,7 +844,7 @@ fn do_call_function( lua: Lua, fun: Value, args: List(Value), -) -> Result(#(Lua, List(Value)), LuaError) +) -> Result(#(Lua, List(Value)), LuaError(e)) /// Gets a reference to the function at `keys`, then inmediatly calls it with the provided `args`. /// @@ -844,7 +864,7 @@ fn do_call_function( pub fn call_function_by_name( keys keys: List(String), args args: List(Value), -) -> Action(List(Value)) { +) -> Action(List(Value), e) { use fun <- then(get(keys)) call_function(fun, args) } From be985bb07bbba165483d88210207c9393bc829d1 Mon Sep 17 00:00:00 2001 From: selenil Date: Wed, 11 Feb 2026 15:07:56 -0500 Subject: [PATCH 04/19] change the way functions are encoded --- src/glua.gleam | 8 ++------ src/glua_ffi.erl | 10 ++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 169956e..7bc4e93 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -308,9 +308,7 @@ pub fn table_decoder( decode.list(of: inner) } -pub fn function( - f: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value { +pub fn function(f: fn(List(Value)) -> Action(List(Value), e)) -> Value { do_function(f) } @@ -372,9 +370,7 @@ pub fn userdata(lua: Lua, v: anything) -> #(Lua, Value) { fn do_userdata(v: anything, lua: Lua) -> #(Value, Lua) @external(erlang, "glua_ffi", "wrap_fun") -fn do_function( - fun: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value +fn do_function(fun: fn(List(Value)) -> Action(List(Value), e)) -> Value /// Converts a reference to a Lua value into type-safe Gleam data using the provided decoder. /// diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index eb31c1a..15495c9 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -125,6 +125,8 @@ map_error({lua_error, {assert_error, Msg} = Error, State}) -> end; map_error({lua_error, {badarg, F, Args}, State}) -> {lua_runtime_exception, {badarg, atom_to_binary(F), Args}, State}; +map_error({lua_error, {glua_action_error, Err}, _}) -> + Err; map_error({lua_error, _, State}) -> {lua_runtime_exception, unknown_exception, State}; map_error(Error) -> @@ -217,8 +219,12 @@ coerce_nil() -> wrap_fun(Fun) -> {erl_func, fun(Args, State) -> - {NewState, Ret} = Fun(State, dereference_list(State, Args)), - {Ret, NewState} + {action, F} = Fun(Args), + case F(State) of + {ok, {NewState, Ret}} -> {Ret, NewState}; + {error, Err} -> + {error, map_error(lua_error({glua_action_error, Err}, State))} + end end}. sandbox_fun(Msg) -> From 8dad9a5ab7b94152e896046b5bcbbe31c3b51ad3 Mon Sep 17 00:00:00 2001 From: selenil Date: Wed, 11 Feb 2026 15:34:03 -0500 Subject: [PATCH 05/19] make table and userdata encoders work with the Action type --- src/glua.gleam | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 7bc4e93..1d5a51b 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -288,8 +288,9 @@ pub fn int(v: Int) -> Value @external(erlang, "glua_ffi", "coerce") pub fn float(v: Float) -> Value -pub fn table(lua: Lua, values: List(#(Value, Value))) -> #(Lua, Value) { - do_table(values, lua) |> pair.swap +pub fn table(values: List(#(Value, Value))) -> Action(Value, e) { + use state <- Action + Ok(do_table(values, state) |> pair.swap) } @external(erlang, "luerl_heap", "alloc_table") @@ -362,8 +363,9 @@ pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { /// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_), _)) = /// glua.eval(state:, code: "return lucy.email") /// ``` -pub fn userdata(lua: Lua, v: anything) -> #(Lua, Value) { - do_userdata(v, lua) |> pair.swap +pub fn userdata(v: anything) -> Action(Value, e) { + use state <- Action + Ok(do_userdata(v, state) |> pair.swap) } @external(erlang, "luerl_heap", "alloc_userdata") From 6d46f3fa457884c8dfef6c90512f4e4ce905edab Mon Sep 17 00:00:00 2001 From: selenil Date: Thu, 12 Feb 2026 15:25:25 -0500 Subject: [PATCH 06/19] add returning_list function and modify returning function --- src/glua.gleam | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 1d5a51b..b040dd9 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -413,10 +413,18 @@ pub fn dereference( fn do_dereference(lua: Lua, ref: Value) -> dynamic.Dynamic pub fn returning( - over: Action(List(Value), e), + action act: Action(Value, e), + using decoder: decode.Decoder(a), +) -> Action(a, e) { + use ref <- then(act) + deference(ref, decoder) +} + +pub fn returning_list( + action act: Action(List(Value), e), using decoder: decode.Decoder(a), ) -> Action(List(a), e) { - use refs <- then(over) + use refs <- then(act) fold(refs, dereference(_, decoder)) } From a321c141ae03d3a8dba60498b051556e6aa34088 Mon Sep 17 00:00:00 2001 From: selenil Date: Thu, 12 Feb 2026 15:39:33 -0500 Subject: [PATCH 07/19] add error and error_with_code functions --- src/glua.gleam | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/glua.gleam b/src/glua.gleam index b040dd9..a129f4c 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -248,6 +248,14 @@ pub fn failure(error: e) -> Action(a, e) { Error(CustomError(error)) } +pub fn error(message: String) -> Action(List(Value), e) { + call_function_by_name(["error"], [string(message)]) +} + +pub fn error_with_code(message: String, code: Int) -> Action(List(Value), e) { + call_function_by_name(["error"], [string(message), int(code)]) +} + pub fn map(over action: Action(a, e), with fun: fn(a) -> b) -> Action(b, e) { use state <- Action action.function(state) @@ -309,10 +317,12 @@ pub fn table_decoder( decode.list(of: inner) } -pub fn function(f: fn(List(Value)) -> Action(List(Value), e)) -> Value { +pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value { do_function(f) } +pub opaque type Never + pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { list.map(values, encoder) } From a171a5e1329e5f9ebc8f0d46ec15b5d62b8c240e Mon Sep 17 00:00:00 2001 From: Cake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:50:56 -1000 Subject: [PATCH 08/19] Tests + Some additions (#28) * Return glua.Lua when running glua.run * Remove opaque from Never, improve docs * Fix old tests * Add glua.try --- src/glua.gleam | 45 ++- test/glua_test.gleam | 687 ++++++++++++++++++++++++------------------- 2 files changed, 430 insertions(+), 302 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index a129f4c..de166bc 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -226,9 +226,9 @@ pub opaque type Action(return, error) { pub fn run( state lua: Lua, action action: Action(return, error), -) -> Result(return, LuaError(error)) { +) -> Result(#(Lua, return), LuaError(error)) { // drop the updated state by desing - action.function(lua) |> result.map(pair.second) + action.function(lua) } pub fn then(action: Action(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { @@ -321,7 +321,17 @@ pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value { do_function(f) } -pub opaque type Never +// Taken from hexdocs.pm/funtil/1.1.0/funtil.html#Never +/// This type is used to represent a value that can never happen. What does that +/// mean exactly? +/// +/// - A `Bool` is a type that has two values: `True` and `False`. +/// - `Nil` is a type that has one value: `Nil`. +/// - `Never` is a type that has zero values: it's impossible to construct! +/// +/// This library uses this type to make `glua.failure` impossible to construct in `glua.function`s +/// to encourage using `glua.error` instead since `glua.failure` wouldn't make sense in that case. +pub type Never pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { list.map(values, encoder) @@ -884,3 +894,32 @@ pub fn call_function_by_name( use fun <- then(get(keys)) call_function(fun, args) } + +/// If the input is `Ok`, it passes its value to a function that yields an +/// `Action`, and returns the yielded `Action`. +/// +/// If the input is an `Error`, the function is not called and +/// a failing `Action` is returned with the original error. +/// +/// This is a shorthand for writing a case with `glua.then` +/// +/// ## Example +/// ```gleam +/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")])) +/// use value <- glua.try(list.first(return)) +/// ``` +/// +/// As opposed to this +/// +/// ```gleam +/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")])) +/// use value <- glua.then(case return { +/// [first] -> first +/// _ -> "Error getting first return value" +/// }) +pub fn try(result: Result(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { + case result { + Ok(ret) -> Action(next(ret).function) + Error(err) -> failure(err) + } +} diff --git a/test/glua_test.gleam b/test/glua_test.gleam index c701e9c..690d6f2 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,5 +1,4 @@ import gleam/dict -import gleam/dynamic import gleam/dynamic/decode import gleam/int import gleam/list @@ -13,35 +12,32 @@ pub fn main() -> Nil { } pub fn get_table_test() { - let lua = glua.new() let my_table = [ #("meaning of life", 42), #("pi", 3), #("euler's number", 3), ] let cool_numbers = - glua.function(fn(lua, _params) { - let #(lua, table) = - glua.table( - lua, - my_table - |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), - ) - - #(lua, [table]) + glua.function(fn(_params) { + use table <- glua.then(glua.table( + my_table + |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), + )) + glua.success([table]) }) - let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(lua, ["cool_numbers"], []) - let assert Ok(table) = - glua.dereference( - state: lua, - ref:, + let action = { + use Nil <- glua.then(glua.set(["cool_numbers"], cool_numbers)) + use ret <- glua.then(glua.call_function_by_name(["cool_numbers"], [])) + let assert [number] = ret + use table <- glua.then(glua.deference( + ref: number, using: decode.dict(decode.string, decode.int), - ) - - assert table == dict.from_list(my_table) + )) + assert table == dict.from_list(my_table) + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn sandbox_test() { @@ -49,31 +45,38 @@ pub fn sandbox_test() { let args = list.map([20, 10], glua.int) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) + glua.run(lua, glua.call_function_by_name(keys: ["math", "max"], args:)) assert exception == glua.ErrorCall("math.max is sandboxed", option.None) let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = - glua.eval(state: lua, code: "return string.upper('my_string')") + glua.run(lua, glua.eval("return string.upper('my_string')")) assert index == "upper" let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.eval( - state: lua, - code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", + glua.run( + lua, + glua.eval( + // TODO: test failure case + "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", + ), ) assert exception == glua.ErrorCall("os.execute is sandboxed", option.None) let assert Ok(lua) = glua.sandbox(glua.new(), ["print"]) - let arg = glua.string("sandbox test is failing") let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name(state: lua, keys: ["print"], args: [arg]) + glua.run( + lua, + glua.call_function_by_name(keys: ["print"], args: [ + glua.string("sandbox test is failing"), + ]), + ) assert exception == glua.ErrorCall("print is sandboxed", option.None) } @@ -82,59 +85,74 @@ pub fn new_sandboxed_test() { let assert Ok(lua) = glua.new_sandboxed([]) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.eval(state: lua, code: "return load(\"return 1\")") + glua.run(lua, glua.eval("return load(\"return 1\")")) assert exception == glua.ErrorCall("load is sandboxed", option.None) - let arg = glua.int(1) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) + glua.run( + lua, + glua.call_function_by_name(keys: ["os", "exit"], args: [ + glua.int(1), + ]), + ) assert exception == glua.ErrorCall("os.exit is sandboxed", option.None) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = - glua.eval(state: lua, code: "io.write('some_message')") + glua.run(lua, glua.eval("io.write('some_message')")) assert index == "write" let assert Ok(lua) = glua.new_sandboxed([["package"], ["require"]]) - let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) + let action = { + use _ <- glua.then(glua.set_lua_paths(paths: ["./test/lua/?.lua"])) - let code = "local s = require 'example'; return s" - let assert Ok(#(state, [ref])) = glua.eval(state: lua, code:) - let assert Ok(result) = glua.dereference(state:, ref:, using: decode.string) + let code = "local s = require 'example'; return s" + use ref <- glua.then(glua.eval(code)) + use ref <- glua.try(list.first(ref)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + glua.success(Nil) + } + let assert Ok(_) = glua.run(lua, action) } pub fn encoding_and_decoding_nested_tables_test() { - let lua = glua.new() - let #(lua, tb1) = - glua.table(lua, [#(glua.string("deeper_key"), glua.string("deeper_value"))]) - let #(lua, tb2) = glua.table(lua, [#(glua.int(1), tb1)]) - let #(lua, tb3) = glua.table(lua, [#(glua.string("key"), tb2)]) - - let keys = ["my_nested_table"] - - let nested_table_decoder = - decode.dict( - decode.string, - decode.dict(decode.int, decode.dict(decode.string, decode.string)), + let action = { + use tb1 <- glua.then( + glua.table([#(glua.string("deeper_key"), glua.string("deeper_value"))]), ) + use tb2 <- glua.then(glua.table([#(glua.int(1), tb1)])) + use tb3 <- glua.then(glua.table([#(glua.string("key"), tb2)])) + + let keys = ["my_nested_table"] + + let nested_table_decoder = + decode.dict( + decode.string, + decode.dict(decode.int, decode.dict(decode.string, decode.string)), + ) + + use Nil <- glua.then(glua.set(keys:, value: tb3)) + use ref <- glua.then(glua.get(keys:)) + + use result <- glua.then(glua.deference(ref:, using: nested_table_decoder)) + + assert result + == dict.from_list([ + #( + "key", + dict.from_list([ + #(1, dict.from_list([#("deeper_key", "deeper_value")])), + ]), + ), + ]) + glua.success(Nil) + } - let assert Ok(lua) = glua.set(state: lua, keys:, value: tb3) - - let assert Ok(ref) = glua.get(state: lua, keys:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: nested_table_decoder) - - assert result - == dict.from_list([ - #( - "key", - dict.from_list([#(1, dict.from_list([#("deeper_key", "deeper_value")]))]), - ), - ]) + let assert Ok(_) = glua.run(glua.new(), action) } pub type Userdata { @@ -142,184 +160,204 @@ pub type Userdata { } pub fn userdata_test() { - let lua = glua.new() - let #(lua, userdata) = glua.userdata(lua, Userdata("my-userdata", 1)) - let userdata_decoder = { - use foo <- decode.field(1, decode.string) - use bar <- decode.field(2, decode.int) - decode.success(Userdata(foo:, bar:)) + let action = { + use userdata <- glua.then(glua.userdata(Userdata("my-userdata", 1))) + let userdata_decoder = { + use foo <- decode.field(1, decode.string) + use bar <- decode.field(2, decode.int) + decode.success(Userdata(foo:, bar:)) + } + + use Nil <- glua.then(glua.set(["my_userdata"], userdata)) + use ref <- glua.then(glua.eval("return my_userdata")) + use ref <- glua.try(list.first(ref)) + use result <- glua.then(glua.deference(ref:, using: userdata_decoder)) + + assert result == Userdata("my-userdata", 1) + + use userdata <- glua.then(glua.userdata(Userdata("other-userdata", 2))) + use Nil <- glua.then(glua.set(["my_other_userdata"], userdata)) + glua.success(Nil) } + let assert Ok(#(lua, Nil)) = glua.run(glua.new(), action) - let assert Ok(lua) = glua.set(lua, ["my_userdata"], userdata) - let assert Ok(#(lua, [ref])) = glua.eval(lua, "return my_userdata") - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: userdata_decoder) - - assert result == Userdata("my-userdata", 1) - - let #(lua, userdata) = glua.userdata(lua, Userdata("other-userdata", 2)) - let assert Ok(lua) = glua.set(lua, ["my_other_userdata"], userdata) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = - glua.eval(lua, "return my_other_userdata.foo") + glua.run(lua, glua.eval("return my_other_userdata.foo")) assert index == "foo" } pub fn get_test() { - let state = glua.new() - - let assert Ok(ref) = glua.get(state: state, keys: ["math", "pi"]) - let assert Ok(pi) = glua.dereference(state:, ref:, using: decode.float) + let action = { + use ref <- glua.then(glua.get(keys: ["math", "pi"])) + use pi <- glua.then(glua.deference(ref:, using: decode.float)) - assert pi >. 3.14 && pi <. 3.15 + // TODO: Replace with glua/lib/math.pi + assert pi >. 3.14 && pi <. 3.15 - let keys = ["my_table", "my_value"] - let encoded = glua.bool(True) - let assert Ok(state) = glua.set(state:, keys:, value: encoded) - let assert Ok(ref) = glua.get(state:, keys:) - let assert Ok(ret) = glua.dereference(state:, ref:, using: decode.bool) + let keys = ["my_table", "my_value"] + use Nil <- glua.then(glua.set(keys:, value: glua.bool(True))) + use ref <- glua.then(glua.get(keys:)) + use ret <- glua.then(glua.deference(ref:, using: decode.bool)) - assert ret == True + assert ret == True + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) - let code = - " + let action = { + let code = + " my_value = 10 return 'ignored' " - let assert Ok(#(state, _)) = glua.new() |> glua.eval(code:) - let assert Ok(ref) = glua.get(state:, keys: ["my_value"]) - let assert Ok(ret) = glua.dereference(state:, ref:, using: decode.int) + use _ <- glua.then(glua.eval(code)) + use ref <- glua.then(glua.get(keys: ["my_value"])) + use ret <- glua.then(glua.deference(ref:, using: decode.int)) - assert ret == 10 + assert ret == 10 + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn get_returns_proper_errors_test() { - let state = glua.new() - - assert glua.get(state:, keys: ["non_existent_global"]) + assert glua.run(glua.new(), glua.get(keys: ["non_existent_global"])) == Error(glua.KeyNotFound(["non_existent_global"])) - let encoded = glua.int(10) - let assert Ok(state) = - glua.set(state:, keys: ["my_table", "some_value"], value: encoded) - - assert glua.get(state:, keys: ["my_table", "my_val"]) - == Error(glua.KeyNotFound(["my_table", "my_val"])) + let action = { + use Nil <- glua.then(glua.set( + keys: ["my_table", "some_value"], + value: glua.int(10), + )) + use _ret <- glua.then(glua.get(keys: ["my_table", "my_val"])) + panic as "unreachable" + } + let assert Error(glua.KeyNotFound(["my_table", "my_val"])) = + glua.run(glua.new(), action) } pub fn set_test() { - let encoded = glua.string("custom version") + let action = { + let encoded = glua.string("custom version") - let assert Ok(lua) = - glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) - let assert Ok(ref) = glua.get(state: lua, keys: ["_VERSION"]) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use Nil <- glua.then(glua.set(keys: ["_VERSION"], value: encoded)) + use ref <- glua.then(glua.get(keys: ["_VERSION"])) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "custom version" + assert result == "custom version" - let numbers = - [2, 4, 7, 12] - |> list.index_map(fn(n, i) { #(i + 1, n * n) }) + let numbers = + [2, 4, 7, 12] + |> list.index_map(fn(n, i) { #(i + 1, n * n) }) - let keys = ["math", "squares"] + let keys = ["math", "squares"] - let #(lua, encoded) = - glua.table( - lua, + use encoded <- glua.then(glua.table( numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), - ) - let assert Ok(lua) = glua.set(lua, keys, encoded) + )) + use Nil <- glua.then(glua.set(keys, encoded)) - let assert Ok(ref) = glua.get(lua, keys) - assert glua.dereference( - state: lua, + use ref <- glua.then(glua.get(keys)) + use check <- glua.then(glua.deference( ref:, using: decode.dict(decode.int, decode.int), - ) - == Ok(dict.from_list([#(1, 4), #(2, 16), #(3, 49), #(4, 144)])) - - let count_odd = fn(lua: glua.Lua, args: List(dynamic.Dynamic)) { - let assert [list] = args - let assert Ok(list) = decode.run(list, decode.dict(decode.int, decode.int)) - - let count = - list.map(dict.to_list(list), pair.second) - |> list.count(int.is_odd) - - #(lua, list.map([count], glua.int)) - } - - let encoded = glua.function(count_odd) - let assert Ok(lua) = glua.set(glua.new(), ["count_odd"], encoded) - - let #(lua, arg) = - glua.table( - lua, - list.index_map(list.range(1, 10), fn(i, n) { - #(glua.int(i + 1), glua.int(n)) - }), - ) - - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(state: lua, keys: ["count_odd"], args: [arg]) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) - - assert result == 5 - - let #(lua, tbl) = - glua.table(lua, [ - #( - glua.string("is_even"), - glua.function(fn(lua, args) { - let assert [arg] = args - let assert Ok(arg) = decode.run(arg, decode.int) - #(lua, list.map([int.is_even(arg)], glua.bool)) + )) + assert check == dict.from_list([#(1, 4), #(2, 16), #(3, 49), #(4, 144)]) + + let count_odd = fn(args: List(glua.Value)) { + let assert [list] = args + use list <- glua.then(glua.deference( + list, + decode.dict(decode.int, decode.int), + )) + + let count = + list.map(dict.to_list(list), pair.second) + |> list.count(int.is_odd) + + glua.success(list.map([count], glua.int)) + } + + let encoded = glua.function(count_odd) + use Nil <- glua.then(glua.set(["count_odd"], encoded)) + + use arg <- glua.then( + glua.table( + list.index_map(list.range(1, 10), fn(i, n) { + #(glua.int(i + 1), glua.int(n)) }), ), - #( - glua.string("is_odd"), - glua.function(fn(lua, args) { - let assert [arg] = args - let assert Ok(arg) = decode.run(arg, decode.int) - #(lua, list.map([int.is_odd(arg)], glua.bool)) - }), - ), - ]) + ) - let arg = glua.int(4) + use refs <- glua.then( + glua.call_function_by_name(keys: ["count_odd"], args: [arg]), + ) + use ref <- glua.try(list.first(refs)) + + use result <- glua.then(glua.deference(ref:, using: decode.int)) + + assert result == 5 + + use tbl <- glua.then( + glua.table([ + #( + glua.string("is_even"), + glua.function(fn(args) { + let assert [arg] = args + use arg <- glua.then(glua.deference(arg, decode.int)) + list.map([int.is_even(arg)], glua.bool) + |> glua.success() + }), + ), + #( + glua.string("is_odd"), + glua.function(fn(args) { + let assert [arg] = args + use arg <- glua.then(glua.deference(arg, decode.int)) + list.map([int.is_odd(arg)], glua.bool) + |> glua.success() + }), + ), + ]), + ) - let assert Ok(lua) = glua.set(state: lua, keys: ["my_functions"], value: tbl) + use Nil <- glua.then(glua.set(keys: ["my_functions"], value: tbl)) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name( - state: lua, - keys: ["my_functions", "is_even"], - args: [arg], + use refs <- glua.then( + glua.call_function_by_name(keys: ["my_functions", "is_even"], args: [ + glua.int(4), + ]), ) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.bool) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.bool)) - assert result == True + assert result == True - let assert Ok(#(lua, [ref])) = - glua.eval(state: lua, code: "return my_functions.is_odd(4)") + use refs <- glua.then(glua.eval("return my_functions.is_odd(4)")) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.bool)) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.bool) - - assert result == False + assert result == False + glua.success(Nil) + } + glua.run(glua.new(), action) } pub fn set_lua_paths_test() { - let assert Ok(state) = - glua.set_lua_paths(state: glua.new(), paths: ["./test/lua/?.lua"]) + let action = { + use Nil <- glua.then(glua.set_lua_paths(paths: ["./test/lua/?.lua"])) - let code = "local s = require 'example'; return s" + let code = "local s = require 'example'; return s" - let assert Ok(#(lua, [ref])) = glua.eval(state:, code:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use refs <- glua.then(glua.eval(code)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + glua.success(Nil) + } + glua.run(glua.new(), action) } pub fn get_private_test() { @@ -345,63 +383,74 @@ pub fn delete_private_test() { } pub fn load_test() { - let assert Ok(#(lua, chunk)) = - glua.load(state: glua.new(), code: "return 5 * 5") - let assert Ok(#(lua, [ref])) = glua.eval_chunk(state: lua, chunk:) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) - - assert result == 25 + let action = { + use chunk <- glua.then(glua.load(code: "return 5 * 5")) + use refs <- glua.then(glua.eval_chunk(chunk)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.int)) + + assert result == 25 + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn eval_load_file_test() { - let assert Ok(#(lua, chunk)) = - glua.load_file(state: glua.new(), path: "./test/lua/example.lua") - let assert Ok(#(lua, [ref])) = glua.eval_chunk(state: lua, chunk:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + let action = { + use chunk <- glua.then(glua.load_file("./test/lua/example.lua")) + use refs <- glua.then(glua.eval_chunk(chunk)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + use _ <- glua.then(glua.load_file("non_existent_file")) - let assert Error(e) = - glua.load_file(state: glua.new(), path: "non_existent_file") - assert e == glua.FileNotFound("non_existent_file") + panic as "unreachable" + } + let assert Error(glua.FileNotFound("non_existent_file")) = + glua.run(glua.new(), action) } pub fn eval_test() { - let assert Ok(#(lua, [ref])) = - glua.eval(state: glua.new(), code: "return 'hello, ' .. 'world!'") - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + let actions = { + use refs <- glua.then(glua.eval("return 'hello, ' .. 'world!'")) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "hello, world!" + assert result == "hello, world!" - let assert Ok(#(lua, refs)) = - glua.eval(state: lua, code: "return 2 + 2, 3 - 1") - let assert Ok(results) = - list.try_map(refs, glua.dereference(state: lua, ref: _, using: decode.int)) - - assert results == [4, 2] + use refs <- glua.then(glua.eval("return 2 + 2, 3 - 1")) + use return <- glua.then( + glua.fold(refs, glua.deference(ref: _, using: decode.int)), + ) + assert return == [4, 2] + glua.success(Nil) + } + glua.run(glua.new(), actions) } pub fn eval_returns_proper_errors_test() { - let state = glua.new() + let lua = glua.new() - let assert Error(e) = glua.eval(state:, code: "if true then 1 + ") + let assert Error(e) = glua.run(lua, glua.eval("if true then 1 + ")) assert e == glua.LuaCompileFailure([ glua.LuaCompileError(1, glua.Parse, "syntax error before: 1"), ]) - let assert Error(e) = glua.eval(state:, code: "print(\"hi)") - + let assert Error(e) = glua.run(lua, glua.eval("print(\"hi)")) assert e == glua.LuaCompileFailure([ glua.LuaCompileError(1, glua.Tokenize, "syntax error near '\"'"), ]) - let assert Ok(#(lua, [ref])) = - glua.eval(state:, code: "return 'Hello from Lua!'") - assert glua.dereference(state: lua, ref:, using: decode.int) + let action = { + use refs <- glua.then(glua.eval("return 'Hello from Lua!'")) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.deference(ref:, using: decode.int)) + panic as "unreachable" + } + assert glua.run(lua, action) == Error( glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), ) @@ -409,7 +458,7 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.IllegalIndex(value:, index:), state: _, - )) = glua.eval(state:, code: "return a.b") + )) = glua.run(lua, glua.eval("return a.b")) assert value == "nil" assert index == "b" @@ -417,7 +466,7 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.ErrorCall(message, level), state: _, - )) = glua.eval(state:, code: "error('error message')") + )) = glua.run(lua, glua.eval("error('error message')")) assert message == "error message" assert level == option.None @@ -425,31 +474,31 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.ErrorCall(message, level), state: _, - )) = glua.eval(state:, code: "error('error with level', 1)") + )) = glua.run(lua, glua.eval("error('error with level', 1)")) assert message == "error with level" assert level == option.Some(1) - let assert Error(_) = glua.eval(state:, code: "error({1})") + let assert Error(_) = glua.run(lua, glua.eval("error({1})")) let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), state: _, - )) = glua.eval(state:, code: "local a = 5; a()") + )) = glua.run(lua, glua.eval("local a = 5; a()")) assert value == "5" let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedMethod(_, method:), state: _, - )) = glua.eval(state:, code: "local i = function(x) return x end; i:call(1)") + )) = glua.run(lua, glua.eval("local i = function(x) return x end; i:call(1)")) assert method == "call" let assert Error(glua.LuaRuntimeException( exception: glua.BadArith(operator:, args:), state: _, - )) = glua.eval(state:, code: "return 10 / 0") + )) = glua.run(lua, glua.eval("return 10 / 0")) assert operator == "/" assert args == ["10", "0"] @@ -457,130 +506,170 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.AssertError(message:), state: _, - )) = glua.eval(state:, code: "assert(1 == 2, 'assertion failed')") + )) = glua.run(lua, glua.eval("assert(1 == 2, 'assertion failed')")) assert message == "assertion failed" - let assert Error(_) = glua.eval(state:, code: "assert(false, {1})") + let assert Error(_) = glua.run(lua, glua.eval("assert(false, {1})")) } pub fn eval_file_test() { - let assert Ok(#(state, [ref])) = - glua.eval_file(state: glua.new(), path: "./test/lua/example.lua") - let assert Ok(result) = glua.dereference(state:, ref:, using: decode.string) + let action = { + use refs <- glua.then(glua.eval_file("./test/lua/example.lua")) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + glua.success(Nil) + } + glua.run(glua.new(), action) } pub fn call_function_test() { - let assert Ok(#(lua, [fun])) = - glua.eval(state: glua.new(), code: "return string.reverse") + let action = { + use return <- glua.then(glua.eval("return string.reverse")) + use fun <- glua.try(list.first(return)) - let encoded = glua.string("auL") + let encoded = glua.string("auL") - let assert Ok(#(lua, [ref])) = - glua.call_function(state: lua, ref: fun, args: [encoded]) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use refs <- glua.then(glua.call_function(fun, [encoded])) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "Lua" + assert result == "Lua" - let assert Ok(#(lua, [fun])) = - glua.eval(state: lua, code: "return function(a, b) return a .. b end") + use return <- glua.then(glua.eval("return function(a, b) return a .. b end")) + use fun <- glua.try(list.first(return)) - let args = list.map(["Lua in ", "Gleam"], glua.string) + let args = list.map(["Lua in ", "Gleam"], glua.string) - let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, ref: fun, args:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use refs <- glua.then(glua.call_function(fun, args)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.string)) - assert result == "Lua in Gleam" + assert result == "Lua in Gleam" + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn call_function_returns_proper_errors_test() { - let state = glua.new() - - let assert Ok(#(state, [ref])) = - glua.eval(state:, code: "return string.upper") - - let arg = glua.string("Hello from Gleam!") + let action = { + use refs <- glua.then(glua.eval("return string.upper")) + use ref <- glua.try(list.first(refs)) + use refs <- glua.then( + glua.call_function(ref, [glua.string("Hello from Gleam!")]), + ) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.deference(ref:, using: decode.int)) + panic as "unreachable" + } - let assert Ok(#(lua, [ref])) = glua.call_function(state:, ref:, args: [arg]) - assert glua.dereference(state: lua, ref:, using: decode.int) + assert glua.run(glua.new(), action) == Error( glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), ) - let assert Ok(#(lua, [ref])) = glua.eval(state:, code: "return 1") + let action = { + use refs <- glua.then(glua.eval("return 1")) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.call_function(ref, [])) + panic as "unreachable" + } let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), state: _, - )) = glua.call_function(state: lua, ref:, args: []) + )) = glua.run(glua.new(), action) assert value == "1" } pub fn call_function_by_name_test() { - let args = list.map([20, 10], glua.int) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(state: glua.new(), keys: ["math", "max"], args:) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) - - assert result == 20 - - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(state: lua, keys: ["math", "min"], args:) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) - - assert result == 10 + let action = { + let args = list.map([20, 10], glua.int) + use refs <- glua.then(glua.call_function_by_name( + keys: ["math", "max"], + args:, + )) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.int)) + + assert result == 20 + + use refs <- glua.then(glua.call_function_by_name( + keys: ["math", "min"], + args:, + )) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.int)) + + assert result == 10 + + let arg = glua.float(10.2) + use refs <- glua.then( + glua.call_function_by_name(keys: ["math", "type"], args: [arg]), + ) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference( + ref:, + using: decode.optional(decode.string), + )) - let arg = glua.float(10.2) - let assert Ok(#(state, [ref])) = - glua.call_function_by_name(state: lua, keys: ["math", "type"], args: [arg]) - let assert Ok(result) = - glua.dereference(state:, ref:, using: decode.optional(decode.string)) + assert result == option.Some("float") - assert result == option.Some("float") + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn nested_function_references_test() { - let code = "return function() return math.sqrt end" - - let assert Ok(#(lua, [ref])) = glua.eval(state: glua.new(), code:) - let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, ref:, args: []) - - let arg = glua.int(400) - let assert Ok(#(_, [ref])) = glua.call_function(state: lua, ref:, args: [arg]) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.float) - assert result == 20.0 + let action = { + let code = "return function() return math.sqrt end" + + use refs <- glua.then(glua.eval(code)) + use ref <- glua.try(list.first(refs)) + use refs <- glua.then(glua.call_function(ref, [])) + use ref <- glua.try(list.first(refs)) + + use refs <- glua.then(glua.call_function(ref, [glua.int(400)])) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.deference(ref:, using: decode.float)) + assert result == 20.0 + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn format_error_test() { - let state = glua.new() + let lua = glua.new() - let assert Error(e) = glua.eval(state:, code: "1 +") + let assert Error(e) = glua.run(lua, glua.eval("1 +")) assert glua.format_error(e) == "Lua compile error: \n\nFailed to parse: error on line 1: syntax error before: 1" - let assert Error(e) = glua.eval(state:, code: "assert(false)") + let assert Error(e) = glua.run(lua, glua.eval("assert(false)")) assert glua.format_error(e) == "Lua runtime exception: Assertion failed with message: assertion failed\n\nLine 1: assert(false)" let assert Error(e) = - glua.eval(state:, code: "local a = true; local b = 1 * a") + glua.run(lua, glua.eval("local a = true; local b = 1 * a")) assert glua.format_error(e) == "Lua runtime exception: Bad arithmetic expression: 1 * true" - let assert Error(e) = glua.get(state:, keys: ["non_existent"]) + let assert Error(e) = glua.run(lua, glua.get(keys: ["non_existent"])) assert glua.format_error(e) == "Key \"non_existent\" not found" - let assert Error(e) = glua.load_file(state:, path: "non_existent_file") + let assert Error(e) = glua.run(lua, glua.load_file("non_existent_file")) assert glua.format_error(e) == "Lua source file \"non_existent_file\" not found" - let assert Ok(#(lua, [ref])) = glua.eval(state:, code: "return 1 + 1") - let assert Error(e) = glua.dereference(state: lua, ref:, using: decode.string) + let action = { + use refs <- glua.then(glua.eval("return 1 + 1")) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.deference(ref:, using: decode.string)) + panic as "unreachable" + } + let assert Error(e) = glua.run(glua.new(), action) assert glua.format_error(e) == "Expected String, but found Int" } From 481d8eca81f85d68471605a090247220c74420e4 Mon Sep 17 00:00:00 2001 From: selenil Date: Fri, 13 Feb 2026 16:03:39 -0500 Subject: [PATCH 09/19] remove unnecessary ffi function --- src/glua_ffi.erl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 15495c9..d585b28 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -23,9 +23,6 @@ to_gleam(Value) -> {error, {unknown_error, nil}} end. -dereference_list(St, LuerlTerms) -> - lists:map(fun (Lt) -> dereference(St, Lt) end, LuerlTerms). - %% transforms Lua values to their corresponding Erlang representation %% this is similar to `luerl:decode/2`, but returns values that are more decode-friendly in Gleam dereference(St, LT) -> From ad6b9c28b9066fc8083c95b2a9eebf8019a94f18 Mon Sep 17 00:00:00 2001 From: selenil Date: Fri, 13 Feb 2026 16:35:22 -0500 Subject: [PATCH 10/19] fix more typos --- src/glua.gleam | 2 +- test/glua_test.gleam | 60 ++++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index de166bc..515a88e 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -437,7 +437,7 @@ pub fn returning( using decoder: decode.Decoder(a), ) -> Action(a, e) { use ref <- then(act) - deference(ref, decoder) + dereference(ref, decoder) } pub fn returning_list( diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 690d6f2..6a35ac9 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -30,7 +30,7 @@ pub fn get_table_test() { use Nil <- glua.then(glua.set(["cool_numbers"], cool_numbers)) use ret <- glua.then(glua.call_function_by_name(["cool_numbers"], [])) let assert [number] = ret - use table <- glua.then(glua.deference( + use table <- glua.then(glua.dereference( ref: number, using: decode.dict(decode.string, decode.int), )) @@ -111,7 +111,7 @@ pub fn new_sandboxed_test() { let code = "local s = require 'example'; return s" use ref <- glua.then(glua.eval(code)) use ref <- glua.try(list.first(ref)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "LUA IS AN EMBEDDABLE LANGUAGE" glua.success(Nil) @@ -138,7 +138,7 @@ pub fn encoding_and_decoding_nested_tables_test() { use Nil <- glua.then(glua.set(keys:, value: tb3)) use ref <- glua.then(glua.get(keys:)) - use result <- glua.then(glua.deference(ref:, using: nested_table_decoder)) + use result <- glua.then(glua.dereference(ref:, using: nested_table_decoder)) assert result == dict.from_list([ @@ -171,7 +171,7 @@ pub fn userdata_test() { use Nil <- glua.then(glua.set(["my_userdata"], userdata)) use ref <- glua.then(glua.eval("return my_userdata")) use ref <- glua.try(list.first(ref)) - use result <- glua.then(glua.deference(ref:, using: userdata_decoder)) + use result <- glua.then(glua.dereference(ref:, using: userdata_decoder)) assert result == Userdata("my-userdata", 1) @@ -190,7 +190,7 @@ pub fn userdata_test() { pub fn get_test() { let action = { use ref <- glua.then(glua.get(keys: ["math", "pi"])) - use pi <- glua.then(glua.deference(ref:, using: decode.float)) + use pi <- glua.then(glua.dereference(ref:, using: decode.float)) // TODO: Replace with glua/lib/math.pi assert pi >. 3.14 && pi <. 3.15 @@ -198,7 +198,7 @@ pub fn get_test() { let keys = ["my_table", "my_value"] use Nil <- glua.then(glua.set(keys:, value: glua.bool(True))) use ref <- glua.then(glua.get(keys:)) - use ret <- glua.then(glua.deference(ref:, using: decode.bool)) + use ret <- glua.then(glua.dereference(ref:, using: decode.bool)) assert ret == True glua.success(Nil) @@ -213,7 +213,7 @@ pub fn get_test() { " use _ <- glua.then(glua.eval(code)) use ref <- glua.then(glua.get(keys: ["my_value"])) - use ret <- glua.then(glua.deference(ref:, using: decode.int)) + use ret <- glua.then(glua.dereference(ref:, using: decode.int)) assert ret == 10 glua.success(Nil) @@ -243,7 +243,7 @@ pub fn set_test() { use Nil <- glua.then(glua.set(keys: ["_VERSION"], value: encoded)) use ref <- glua.then(glua.get(keys: ["_VERSION"])) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "custom version" @@ -259,7 +259,7 @@ pub fn set_test() { use Nil <- glua.then(glua.set(keys, encoded)) use ref <- glua.then(glua.get(keys)) - use check <- glua.then(glua.deference( + use check <- glua.then(glua.dereference( ref:, using: decode.dict(decode.int, decode.int), )) @@ -267,7 +267,7 @@ pub fn set_test() { let count_odd = fn(args: List(glua.Value)) { let assert [list] = args - use list <- glua.then(glua.deference( + use list <- glua.then(glua.dereference( list, decode.dict(decode.int, decode.int), )) @@ -295,7 +295,7 @@ pub fn set_test() { ) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.int)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) assert result == 5 @@ -305,7 +305,7 @@ pub fn set_test() { glua.string("is_even"), glua.function(fn(args) { let assert [arg] = args - use arg <- glua.then(glua.deference(arg, decode.int)) + use arg <- glua.then(glua.dereference(arg, decode.int)) list.map([int.is_even(arg)], glua.bool) |> glua.success() }), @@ -314,7 +314,7 @@ pub fn set_test() { glua.string("is_odd"), glua.function(fn(args) { let assert [arg] = args - use arg <- glua.then(glua.deference(arg, decode.int)) + use arg <- glua.then(glua.dereference(arg, decode.int)) list.map([int.is_odd(arg)], glua.bool) |> glua.success() }), @@ -330,13 +330,13 @@ pub fn set_test() { ]), ) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.bool)) + use result <- glua.then(glua.dereference(ref:, using: decode.bool)) assert result == True use refs <- glua.then(glua.eval("return my_functions.is_odd(4)")) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.bool)) + use result <- glua.then(glua.dereference(ref:, using: decode.bool)) assert result == False glua.success(Nil) @@ -352,7 +352,7 @@ pub fn set_lua_paths_test() { use refs <- glua.then(glua.eval(code)) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "LUA IS AN EMBEDDABLE LANGUAGE" glua.success(Nil) @@ -387,7 +387,7 @@ pub fn load_test() { use chunk <- glua.then(glua.load(code: "return 5 * 5")) use refs <- glua.then(glua.eval_chunk(chunk)) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.int)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) assert result == 25 glua.success(Nil) @@ -400,7 +400,7 @@ pub fn eval_load_file_test() { use chunk <- glua.then(glua.load_file("./test/lua/example.lua")) use refs <- glua.then(glua.eval_chunk(chunk)) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "LUA IS AN EMBEDDABLE LANGUAGE" use _ <- glua.then(glua.load_file("non_existent_file")) @@ -415,13 +415,13 @@ pub fn eval_test() { let actions = { use refs <- glua.then(glua.eval("return 'hello, ' .. 'world!'")) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "hello, world!" use refs <- glua.then(glua.eval("return 2 + 2, 3 - 1")) use return <- glua.then( - glua.fold(refs, glua.deference(ref: _, using: decode.int)), + glua.fold(refs, glua.dereference(ref: _, using: decode.int)), ) assert return == [4, 2] glua.success(Nil) @@ -447,7 +447,7 @@ pub fn eval_returns_proper_errors_test() { let action = { use refs <- glua.then(glua.eval("return 'Hello from Lua!'")) use ref <- glua.try(list.first(refs)) - use _ <- glua.then(glua.deference(ref:, using: decode.int)) + use _ <- glua.then(glua.dereference(ref:, using: decode.int)) panic as "unreachable" } assert glua.run(lua, action) @@ -517,7 +517,7 @@ pub fn eval_file_test() { let action = { use refs <- glua.then(glua.eval_file("./test/lua/example.lua")) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "LUA IS AN EMBEDDABLE LANGUAGE" glua.success(Nil) @@ -534,7 +534,7 @@ pub fn call_function_test() { use refs <- glua.then(glua.call_function(fun, [encoded])) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "Lua" @@ -545,7 +545,7 @@ pub fn call_function_test() { use refs <- glua.then(glua.call_function(fun, args)) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.string)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) assert result == "Lua in Gleam" glua.success(Nil) @@ -561,7 +561,7 @@ pub fn call_function_returns_proper_errors_test() { glua.call_function(ref, [glua.string("Hello from Gleam!")]), ) use ref <- glua.try(list.first(refs)) - use _ <- glua.then(glua.deference(ref:, using: decode.int)) + use _ <- glua.then(glua.dereference(ref:, using: decode.int)) panic as "unreachable" } @@ -593,7 +593,7 @@ pub fn call_function_by_name_test() { args:, )) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.int)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) assert result == 20 @@ -602,7 +602,7 @@ pub fn call_function_by_name_test() { args:, )) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.int)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) assert result == 10 @@ -611,7 +611,7 @@ pub fn call_function_by_name_test() { glua.call_function_by_name(keys: ["math", "type"], args: [arg]), ) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference( + use result <- glua.then(glua.dereference( ref:, using: decode.optional(decode.string), )) @@ -634,7 +634,7 @@ pub fn nested_function_references_test() { use refs <- glua.then(glua.call_function(ref, [glua.int(400)])) use ref <- glua.try(list.first(refs)) - use result <- glua.then(glua.deference(ref:, using: decode.float)) + use result <- glua.then(glua.dereference(ref:, using: decode.float)) assert result == 20.0 glua.success(Nil) } @@ -667,7 +667,7 @@ pub fn format_error_test() { let action = { use refs <- glua.then(glua.eval("return 1 + 1")) use ref <- glua.try(list.first(refs)) - use _ <- glua.then(glua.deference(ref:, using: decode.string)) + use _ <- glua.then(glua.dereference(ref:, using: decode.string)) panic as "unreachable" } let assert Error(e) = glua.run(glua.new(), action) From f1f9cd07058e706e530fb2b1d7ed31c3f0216a0a Mon Sep 17 00:00:00 2001 From: selenil Date: Fri, 13 Feb 2026 16:36:35 -0500 Subject: [PATCH 11/19] fix lib tests --- test/lib_test.gleam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib_test.gleam b/test/lib_test.gleam index 4eea26e..861b03d 100644 --- a/test/lib_test.gleam +++ b/test/lib_test.gleam @@ -12,7 +12,7 @@ import glua/lib/utf8 /// Checks to see if a value matches a value at the given path. fn check(val: glua.Value, path: List(String)) { - let assert Ok(found) = glua.get(glua.new(), path) + let assert Ok(#(_, found)) = glua.run(glua.new(), glua.get(path)) assert val == found } From 9a26945a863c0d08a584a06b1ec4e4aac7cddcc8 Mon Sep 17 00:00:00 2001 From: selenil Date: Fri, 13 Feb 2026 17:35:04 -0500 Subject: [PATCH 12/19] rewrite docs and add labels to functions --- src/glua.gleam | 352 ++++++++++++++++++++++++++----------------------- 1 file changed, 190 insertions(+), 162 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 515a88e..46f0cb4 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -68,51 +68,50 @@ pub type LuaRuntimeExceptionKind { /// ## Examples /// /// ```gleam -/// let assert Error(e) = glua.eval( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.eval( /// code: "if true end", -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Lua compile error: \n\nFailed to parse: error on line 1: syntax error before: 'end'" /// ``` /// /// ```gleam -/// let assert Error(e) = glua.eval( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.eval( /// code: "local a = 1; local b = true; return a + b", -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Lua runtime exception: Bad arithmetic expression: 1 + true" /// ``` /// /// ```gleam -/// let assert Error(e) = glua.get( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.get( /// keys: ["a_value"], -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Key \"a_value\" not found" /// ``` /// /// ```gleam -/// let assert Error(e) = glua.eval_file( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.eval_file( /// path: "my_lua_file.lua", -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Lua source file \"my_lua_file.lua\" not found" /// ``` /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state: glua.new(), -/// code: "return 1 + 1", -/// ) -/// let assert Error(e) = glua.dereference(state:, ref:, using: decode.string) +/// let assert Error(e) = glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval( +/// code: "return 1 + 1", +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// }) /// /// glua.format_error(e) /// // -> "Expected String, but found Int" @@ -354,17 +353,19 @@ pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { /// decode.success(User(name:, is_admin:)) /// } /// -/// let state = glua.new() -/// let #(state, userdata) = glua.userdata(state, User(name: "Jhon Doe", is_admin: False)) -/// let assert Ok(state) = glua.set( -/// state:, -/// keys: ["a_user"], -/// value: userdata -/// ) -/// -/// let assert Ok(#(state, [ref])) = glua.eval(state:, code: "return a_user") -/// glua.dereference(state:, ref:, using: user_decoder) -/// // -> Ok(User("Jhon Doe", False)) +/// glua.run(glua.new(), { +/// use userdata <- glua.then(userdata(User("Jhon Doe", False))) +/// use _ <- glua.then(glua.set( +/// keys: ["a_user"], +/// value: userdata +/// )) +/// +/// use ret <- glua.then(glua.eval(code: "return a_user")) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: user_decoder) +/// }) +/// // -> Ok(#(_state, User("Jhon Doe", False))) /// ``` /// /// ```gleam @@ -372,16 +373,18 @@ pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { /// Person(name: String, email: String) /// } /// -/// let state = glua.new() -/// let #(state, userdata) = glua.userdata(state, Person(name: "Lucy", email: "lucy@example.com")) -/// let assert Ok(lua) = glua.set( -/// state:, -/// keys: ["lucy"], -/// value: userdata -/// ) -/// /// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_), _)) = -/// glua.eval(state:, code: "return lucy.email") +/// glua.run(glua.new(), { +/// use userdata <- glua.then(glua.userdata( +/// Person(name: "Lucy", email: "lucy@example.com") +/// )) +/// use _ <- glua.then(glua.set( +/// keys: ["lucy"], +/// value: userdata +/// )) +/// +/// glua.eval(code: "return lucy.email") +/// }) /// ``` pub fn userdata(v: anything) -> Action(Value, e) { use state <- Action @@ -399,21 +402,23 @@ fn do_function(fun: fn(List(Value)) -> Action(List(Value), e)) -> Value /// ## Examples /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state: glua.new(), -/// code: "return 'Hello from Lua!'" -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok("Hello from Lua!") +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "return 'Hello from Lua!'")) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// } +/// // -> Ok(#(_state, "Hello from Lua!")) /// ``` /// /// ```gleam -/// let assert Ok(#(state, [ref1, ref2])) = glua.eval( -/// state: glua.new(), -/// code: "return 1, true" +/// let assert Ok(#(state, [ref1, ref2])) = glua.run( +/// glua.new(), +/// glua.eval(code: "return 1, true") /// ) -/// assert glua.dereference(state:, ref: ref1, using: decode.int) == Ok(1) -/// assert glua.dereference(state:, ref: ref2, using: decode.bool) == Ok(True) +/// +/// assert glua.run(state, glua.dereference(ref: ref1, using: decode.int)) == Ok(1) +/// assert glua.run(state, glua.dereference(ref: ref2, using: decode.bool)) == Ok(True) /// ``` pub fn dereference( ref ref: Value, @@ -492,11 +497,12 @@ fn list_substraction(a: List(a), b: List(a)) -> List(a) /// ## Examples /// /// ```gleam -/// let assert Ok(lua) = glua.new() |> glua.sandbox(["os"], ["execute"]) -/// let assert Error(glua.LuaRuntimeException(exception, _)) = glua.eval( -/// state: lua, -/// code: "os.execute(\"rm -f important_file\"); return 0", -/// ) +/// let assert Ok(state) = glua.new() |> glua.sandbox(["os"], ["execute"]) +/// let assert Error(glua.LuaRuntimeException(exception, _)) = +/// glua.run(state, glua.eval( +/// code: "os.execute(\"rm -f important_file\"); return 0", +/// )) +/// /// // 'important_file' was not deleted /// assert exception == glua.ErrorCall(["os.execute is sandboxed"]) /// ``` @@ -518,26 +524,28 @@ fn sandbox_fun(msg: String) -> Value /// ## Examples /// /// ```gleam -/// let state = glua.new() -/// let assert Ok(ref) = glua.get(state:, keys: ["_VERSION"]) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok("Lua 5.3") +/// glua.run(glua.new(), { +/// use ref <- glua.then(glua.get(keys: ["_VERSION"])) +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "Lua 5.3")) /// ``` /// /// ```gleam -/// let assert Ok(state) = glua.set( -/// state: glua.new(), -/// keys: ["my_table", "my_value"], -/// value: glua.bool(True) -/// ) -/// -/// let assert Ok(ref) = glua.get(state:, keys: ["my_table", "my_value"]) -/// glua.dereference(state:, ref:, using: decode.bool) -/// // -> Ok(True) +/// glua.run(glua.new(), { +/// use _ <- glua.then(glua.set( +/// keys: ["my_table", "my_value"], +/// value: glua.bool(True) +/// )) +/// use ref <- glua.then(glua.get(keys: ["my_table", "my_value"])) +/// +/// glua.dereference(ref:, using: decode.bool) +/// }) +/// // -> Ok(#(_state, True)) /// ``` /// /// ```gleam -/// glua.get(state: glua.new(), keys: ["non_existent"]) +/// glua.run(glua.new(), glua.get(keys: ["non_existent"])) /// // -> Error(glua.KeyNotFound(["non_existent"])) /// ``` pub fn get(keys keys: List(String)) -> Action(Value, e) { @@ -581,33 +589,32 @@ fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError(e)) /// ## Examples /// /// ```gleam -/// let assert Ok(state) = glua.set( -/// state: glua.new(), -/// keys: ["my_number"], -/// value: glua.int(10) -/// ) -/// -/// let assert Ok(ref) = glua.get(state: lua, keys: ["my_number"]) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(10) +/// glua.run(glua.new(), { +/// use _ <- glua.then(glua.set( +/// keys: ["my_number"], +/// value: glua.int(10) +/// )) +/// use ref <- glua.get(keys: ["my_number"]) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 10)) /// ``` /// /// ```gleam /// let emails = ["jhondoe@example.com", "lucy@example.com"] -/// let #(state, encoded) = glua.table( -/// glua.new(), -/// list.index_map(emails, fn(email, i) { #(glua.int(i + 1), glua.string(email)) }) -/// ) -/// let assert Ok(state) = glua.set( -/// state:, -/// keys: ["info", "emails"], -/// value: encoded -/// ) +/// let assert Ok(#(_state, results)) = glua.run(glua.new(), { +/// use encoded <- glua.then(glua.table( +/// list.index_map(emails, fn(email, i) { #(glua.int(i + 1), glua.string(email)) }) +/// )) +/// use _ <- glua.then(glua.set(["info", "emails"], encoded)) +/// +/// use ret <- glua.then(glua.eval(code: "return info.emails")) +/// use ref <- glua.try(list.first(ret)) /// -/// let assert Ok(#(state, [ref])) = glua.eval(state:, code: "return info.emails") -/// let assert Ok(results) = -/// glua.dereference(state:, ref:, using: decode.dict(decode.int, decode.string)) -/// |> result.map(dict.values) +/// glua.dereference(ref:, using: decode.dict(decode.int, decode.string)) +/// |> glua.map(dict.values) +/// }) /// /// assert results == emails /// ``` @@ -670,17 +677,16 @@ pub fn set_api( /// /// ```gleam /// let my_scripts_paths = ["app/scripts/lua/?.lua"] -/// let assert Ok(state) = glua.set_lua_paths( -/// state: glua.new(), -/// paths: my_scripts_paths -/// ) -/// -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state:, -/// code: "local my_math = require 'my_script'; return my_math.square(3)" -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(9) +/// glua.run(glua.new(), { +/// use _ <- glua.then(glua.set_lua_paths(paths: my_scripts_paths)) +/// use ret <- glua.then(glua.eval( +/// code: "local my_math = require 'my_script'; return my_math.square(3)" +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 9)) /// ``` pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil, e) { let paths = string.join(paths, with: ";") |> string @@ -725,7 +731,7 @@ fn do_load(lua: Lua, code: String) -> Result(#(Lua, Chunk), LuaError(e)) /// Parses a Lua source file and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. -pub fn load_file(path: String) -> Action(Chunk, e) { +pub fn load_file(path path: String) -> Action(Chunk, e) { Action(do_load_file(_, path)) } @@ -737,27 +743,28 @@ fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError(e)) /// ## Examples /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state: glua.new(), -/// code: "return 1 + 2", -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(3) +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "return 1 + 2")) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 3)) /// ``` /// /// ```gleam -/// let assert Ok(#(state, [ref1, ref2])) = glua.eval( -/// state: glua.new(), +/// let assert Ok(#(state, [ref1, ref2])) = glua.run(glua.new(), glua.eval( /// code: "return 'hello, world!', 10", -/// ) -/// assert glua.dereference(state:, ref: ref1, using: decode.string) == "hello, world!" -/// assert glua.dereference(state:, ref: ref2, using: decode.int) == 10 +/// )) +/// +/// assert glua.run(state, glua.dereference(ref: ref1, using: decode.string)) == "hello, world!" +/// assert glua.run(state, glua.dereference(ref: ref2, using: decode.int)) == 10 /// ``` /// /// ```gleam -/// glua.eval(state: glua.new(), code: "return 1 * ") -/// // -> Error(glua.LuaCompilerException( -/// messages: ["syntax error before: ", "1"] +/// glua.run(glua.new(), glua.eval(code: "return 1 * ")) +/// // -> Error(glua.LuaCompileFailure( +/// [glua.LuaCompileError(1, Parse, "syntax error before: ")] /// )) /// ``` /// @@ -765,7 +772,7 @@ fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError(e)) /// > instead of calling `glua.eval` repeatly it is recommended to first convert /// > the code to a chunk by passing it to `glua.load`, and then /// > evaluate that chunk using `glua.eval_chunk`. -pub fn eval(code: String) -> Action(List(Value), e) { +pub fn eval(code code: String) -> Action(List(Value), e) { Action(do_eval(_, code)) } @@ -775,20 +782,20 @@ fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError(e)) /// Evaluates a compiled chunk of Lua code. /// /// ## Examples +/// /// ```gleam -/// let assert Ok(#(lua, chunk)) = glua.load( -/// state: glua.new(), -/// code: "return 'hello, world!'" -/// ) +/// glua.run(glua.new(), { +/// use chunk <- glua.then(glua.load( +/// code: "return 'hello, world!'" +/// )) +/// +/// use ret <- glua.then(glua.eval_chunk(chunk:)) +/// use ref <- glua.try(list.first(ret)) /// -/// let assert Ok(#(state, [ref])) = glua.eval_chunk( -/// state: lua, -/// chunk:, -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok("hello, world!") +/// glua.dereference(ref:, using: decode.string) +/// // -> Ok(#(_state, "hello, world!")) /// ``` -pub fn eval_chunk(chunk: Chunk) -> Action(List(Value), e) { +pub fn eval_chunk(chunk chunk: Chunk) -> Action(List(Value), e) { Action(do_eval_chunk(_, chunk)) } @@ -801,23 +808,26 @@ fn do_eval_chunk( /// Evaluates a Lua source file. /// /// ## Examples +/// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval_file( -/// state: glua.new(), -/// path: "path/to/hello.lua", -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// Ok("hello, world!") +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval_file( +/// path: "path/to/hello.lua", +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "hello, world!")) /// ``` /// /// ```gleam -/// glua.eval_file( -/// state: glua.new(), +/// glua.run(glua.new(), glua.eval_file( /// path: "path/to/non/existent/file", -/// ) -/// //-> Error(glua.FileNotFound(["path/to/non/existent/file"])) +/// )) +/// // -> Error(glua.FileNotFound(["path/to/non/existent/file"])) /// ``` -pub fn eval_file(path: String) -> Action(List(Value), e) { +pub fn eval_file(path path: String) -> Action(List(Value), e) { Action(do_eval_file(_, path)) } @@ -830,15 +840,21 @@ fn do_eval_file( /// Calls a Lua function by reference. /// /// ## Examples +/// /// ```gleam -/// let assert Ok(#(state, [fun])) = glua.eval(state: glua.new(), code: "return math.sqrt") -/// let assert Ok(#(state, [ref])) = glua.call_function( -/// state:, -/// ref: fun, -/// args: [glua.int(81)], -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(9) +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "return math.sqrt")) +/// use fun <- glua.try(list.first(ret)) +/// +/// use ret <- glua.then(glua.call_function( +/// ref: fun, +/// args: [glua.int(81)], +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.float) +/// }) +/// // -> Ok(#(_state, 9.0)) /// ``` /// /// ```gleam @@ -852,16 +868,25 @@ fn do_eval_file( /// /// return fib /// " -/// let assert Ok(#(state, [fun])) = glua.eval(state: glua.new(), code:) -/// let assert Ok(#(state), [ref])) = glua.call_function( -/// state: lua, -/// ref: fun, -/// args: [glua.int(10)], -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(55) +/// +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code:)) +/// use ref <- glua.try(list.first(ret)) +/// +/// use ret <- glua.then(glua.call_function( +/// ref: fun, +/// args: [glua.int(10)], +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 55)) /// ``` -pub fn call_function(fun: Value, args: List(Value)) -> Action(List(Value), e) { +pub fn call_function( + ref fun: Value, + args args: List(Value), +) -> Action(List(Value), e) { Action(do_call_function(_, fun, args)) } @@ -879,13 +904,16 @@ fn do_call_function( /// ## Examples /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.call_function_by_name( -/// state: glua.new(), -/// keys: ["string", "upper"], -/// args: [glua.string("hello from Gleam!")], -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok(HELLO FROM GLEAM!") +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.call_function_by_name( +/// keys: ["string", "upper"], +/// args: [glua.string("hello from Gleam!")] +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "HELLO FROM GLEAM!")) /// ``` pub fn call_function_by_name( keys keys: List(String), From e405f3a255b49af7e7ca88e2221b91f8b31654cd Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 15 Feb 2026 19:30:05 -0500 Subject: [PATCH 13/19] add more docs --- src/glua.gleam | 231 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 201 insertions(+), 30 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 46f0cb4..c9eb045 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -218,18 +218,120 @@ fn format_lua_value(v: anything) -> String @external(erlang, "luerl_lib", "format_error") fn format_unknown_error(error: dynamic.Dynamic) -> String +/// Represents an action that can be run within a Lua state and potentially mutates that state. +/// +/// `Action`s are how we interact with a Lua VM and thus many functions in this library +/// returns an `Action` when invoked. It is important to note that the execution of any `Action` is defered until +/// you pass it to `glua.run`: +/// +/// ```gleam +/// // no Lua code has been evaluated or even parsed, +/// // we're just creating an `Action` +/// let action = glua.eval("return 1") +/// +/// glua.run(glua.new(), action) // now our Lua code is evaluated +/// ``` +/// +/// `Action`s *can* fail and *can* mutate the Lua state. When calling multiple `Action`s in sequence, +/// you need to make sure each one is executed within the Lua state returned by the previous one since +/// executing an `Action` using outdated state could lead to unexpected behaviour. +/// +/// ```gleam +/// let result = { +/// use #(new_state, _) <- result.try( +/// glua.run(state, glua.set(keys: ["a_number"], value: glua.int(36))) +/// ) +/// use #(new_state, ret) <- result.try( +/// glua.run(state, glua.eval("return math.sqrt(a_number)")) +/// ) +/// +/// // we know that `math.sqrt` only returns one value +/// let assert [ref] = ret +/// +/// glua.run(new_state, glua.dereference(ref:, using: decode.float)) +/// |> result.map(pair.second) +/// } +/// result +/// // -> Ok(6.0) +/// ``` +/// +/// However, `glua` provides function to compose `Actions`s toghether without having to pass +/// the state explicitly. The most common of such functions is `glua.then`, which allows us to take +/// an existing `Action` and use its return value to construct another `Action`. +/// `glua.then` will automatically pass the state returned by the first action to the second one +/// and it will halt the chain as soon as any `Action` fails (like `result.try`). +/// This is equivalent to the above example: +/// +/// ```gleam +/// let state = glua.new() +/// let action = { +/// use _ <- glua.then(glua.set(keys: ["a_number"], value: glua.int(36))) +/// use ret <- glua.then(glua.eval(code: "return math.sqrt(a_number)")) +/// +/// // we know that `math.sqrt` only returns one value +/// let assert [ref] = ret +/// glua.dereference(ref:, using: decode.float) +/// } +/// +/// glua.run(state, action) |> result.map(pair.second) +/// // -> Ok(6.0) +/// ``` +/// +/// An `Action` takes two types parameters, `return` is the type of the value that the `Action` +/// would return in case it succeeds, and `error` is the type of custom errors that +/// the `Action` could return. pub opaque type Action(return, error) { Action(function: fn(Lua) -> Result(#(Lua, return), LuaError(error))) } +/// Runs an `Action` within a Lua environment. +/// +/// ## Examples +/// +/// ```gleam +/// let state = glua.new() +/// glua.run(state, { +/// use ret <- glua.then(glua.eval("return 'Hello from Lua!'")) +/// use ref <- glua.try(list.first(ret)) +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "Hello from Lua!")) +/// ``` pub fn run( state lua: Lua, action action: Action(return, error), ) -> Result(#(Lua, return), LuaError(error)) { - // drop the updated state by desing action.function(lua) } +/// Composes two `Action`s into a single one, by executing the first one and passing its return value +/// to a function that returns another `Action`. +/// +/// If the first `Action` returns an `Error` when executed, then the function is not called +/// and the error is returned. +/// +/// This function is the most common way to chain together multiple `Action`s. +/// +/// ## Examples +/// +/// ```gleam +/// let my_value = 1 +/// let assert Ok(#(_state, ret)) = glua.run(glua.new(), { +/// use _ <- glua.then(glua.set(keys: ["my_value"], value: glua.int(my_value))) +/// use ref <- glua.then(glua.get(keys: ["my_value"])) +/// glua.dereference(ref:, using: decode.int) +/// }) +/// +/// assert ret == my_value +/// ``` +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval_file(path: "./my_file.lua")) +/// glua.call_function_by_name(path: ["table", "pack"], args: ret) +/// }) +/// // -> Error(glua.FileNotFound("./my_file.lua")) +/// ``` pub fn then(action: Action(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { use state <- Action use #(new, ret) <- result.try(action.function(state)) @@ -237,30 +339,122 @@ pub fn then(action: Action(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { next(ret).function(new) } +/// Transforms the provided result into an `Action` by passing its value to a function +/// that yields an `Action`. +/// +/// If the input is an `Error`, then the function is not called and instead a failing `Action` +/// is returned with the original error. +/// +/// This is a shorthand for writing a case with `glua.then`: +/// +/// ```gleam +/// use fun <- glua.then(glua.get(["string", "reverse"])) +/// use return <- glua.then(glua.call_function(fun:, args: [glua.string("Hello")])) +/// use value <- glua.try(list.first(return)) +/// glua.dereference(ref: value, using: decode.string) +/// ``` +/// +/// as opposed to this: +/// +/// ```gleam +/// use fun <- glua.then(glua.get(["string", "reverse"])) +/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")])) +/// case return { +/// [first] -> glua.dereference(ref: first, using: decode.string) +/// _ -> glua.failure(Nil) +/// } +/// ``` +pub fn try(result: Result(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { + case result { + Ok(ret) -> Action(next(ret).function) + Error(err) -> failure(err) + } +} + +/// Creates an `Action` that always succeeds and returns `value`. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run(glua.new(), glua.success("my value")) +/// // -> Ok(#(_state, "my_value")) +/// ``` pub fn success(value: a) -> Action(a, e) { use state <- Action Ok(#(state, value)) } +/// Creates an `Action` that always fails with `glua.CustomError(error)`. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run( +/// glua.new(), +/// glua.failure("incorrect number of return values") +/// ) +/// // -> Error(glua.CustomError("incorrect number of return values")) +/// ``` pub fn failure(error: e) -> Action(a, e) { use _ <- Action Error(CustomError(error)) } +/// Invokes the Lua `error` function with the provided message. pub fn error(message: String) -> Action(List(Value), e) { call_function_by_name(["error"], [string(message)]) } +/// Invokes the Lua `error` function with the provided message and code. pub fn error_with_code(message: String, code: Int) -> Action(List(Value), e) { call_function_by_name(["error"], [string(message), int(code)]) } +/// Transforms the return value of an `Action` with the provided function. +/// +/// If the `Action` returns an `Error` when executed then the function is not called and the +/// error is returned. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use ref <- glua.then(glua.get(keys: ["_VERSION"])) +/// use version <- glua.map(glua.dereference(ref:, using: decode.string)) +/// "glua supports " <> version +/// }) +/// // -> Ok(#(_state, "glua supports Lua 5.3")) +/// ``` +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use n <- glua.map(glua.get(keys: ["my_number"])) +/// n * 2 +/// }) +/// // -> Error(glua.KeyNotFound(["my_number"])) +/// ``` pub fn map(over action: Action(a, e), with fun: fn(a) -> b) -> Action(b, e) { use state <- Action action.function(state) |> result.map(pair.map_second(_, fun)) } +/// Maps a list of elements into a list of `Action`s by calling a function in each element and then flattens +/// all the `Action`s into a single one. +/// +/// ## Examples +/// +/// ```gleam +/// let numbers = [9, 16, 25] +/// let keys = ["math", "sqrt"] +/// glua.run(glua.new(), glua.fold(numbers, fn(n) { +/// use ret <- glua.then(glua.call_function_by_name(keys:, args: [glua.int(n)])) +/// +/// let assert [ref] = ret +/// glua.dereference(ref:, using: decode.float) +/// })) +/// // -> Ok(#(_state, [3.0, 4.0, 5.0])) +/// ``` pub fn fold( over list: List(a), with fun: fn(a) -> Action(b, e), @@ -316,6 +510,12 @@ pub fn table_decoder( decode.list(of: inner) } +/// Encodes a Gleam function into a Lua function. +/// +/// > **Note**: The function to be encoded has to return an `Action` with a `Never` type +/// > as the `error` parameter, meaning that the function cannot invoke `glua.failure` in its body. +/// > If you want to return an error inside that function, you should use `glua.error` or `glua.error_with_code`, +/// > both of which will call the Lua `error` function. pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value { do_function(f) } @@ -922,32 +1122,3 @@ pub fn call_function_by_name( use fun <- then(get(keys)) call_function(fun, args) } - -/// If the input is `Ok`, it passes its value to a function that yields an -/// `Action`, and returns the yielded `Action`. -/// -/// If the input is an `Error`, the function is not called and -/// a failing `Action` is returned with the original error. -/// -/// This is a shorthand for writing a case with `glua.then` -/// -/// ## Example -/// ```gleam -/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")])) -/// use value <- glua.try(list.first(return)) -/// ``` -/// -/// As opposed to this -/// -/// ```gleam -/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")])) -/// use value <- glua.then(case return { -/// [first] -> first -/// _ -> "Error getting first return value" -/// }) -pub fn try(result: Result(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { - case result { - Ok(ret) -> Action(next(ret).function) - Error(err) -> failure(err) - } -} From 375dacdd392c9ac774bd60001e75b5b1add2d5ca Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 15 Feb 2026 19:32:55 -0500 Subject: [PATCH 14/19] fix code examples --- src/glua.gleam | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index c9eb045..23c4a3a 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -617,8 +617,10 @@ fn do_function(fun: fn(List(Value)) -> Action(List(Value), e)) -> Value /// glua.eval(code: "return 1, true") /// ) /// -/// assert glua.run(state, glua.dereference(ref: ref1, using: decode.int)) == Ok(1) -/// assert glua.run(state, glua.dereference(ref: ref2, using: decode.bool)) == Ok(True) +/// let assert Ok(#(_state, 1)) = +/// glua.run(state, glua.dereference(ref: ref1, using: decode.int)) +/// let assert Ok(#(_state, True)) = +/// glua.run(state, glua.dereference(ref: ref2, using: decode.bool)) /// ``` pub fn dereference( ref ref: Value, @@ -957,8 +959,10 @@ fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError(e)) /// code: "return 'hello, world!', 10", /// )) /// -/// assert glua.run(state, glua.dereference(ref: ref1, using: decode.string)) == "hello, world!" -/// assert glua.run(state, glua.dereference(ref: ref2, using: decode.int)) == 10 +/// let assert Ok(#(_state, "hello world")) = +/// glua.run(state, glua.dereference(ref: ref1, using: decode.string)) +/// let assert Ok(#(_state, 10)) = +/// glua.run(state, glua.dereference(ref: ref2, using: decode.int)) /// ``` /// /// ```gleam From b0e952f1e1d56d67889b02a492e7f23a74e2589c Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 15 Feb 2026 19:41:28 -0500 Subject: [PATCH 15/19] add guard function --- src/glua.gleam | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/glua.gleam b/src/glua.gleam index 23c4a3a..ea6b972 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -2,6 +2,7 @@ //// //// Gleam wrapper around [Luerl](https://github.com/rvirding/luerl). +import gleam/bool import gleam/dynamic import gleam/dynamic/decode import gleam/int @@ -371,6 +372,28 @@ pub fn try(result: Result(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { } } +/// Runs a callback function if the given bool is `False`, otherwise return a failing `Action` +/// using the provided value. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "local a = 1")) +/// use <- glua.guard(when: ret == [], return: "expected at least one value from Lua") +/// +/// fold(ret, glua.dereference(_, using: decode.int)) +/// }) +/// // -> Error(glua.CustomError("expected at least one value from Lua")) +/// ``` +pub fn guard( + when requirement: Bool, + return consequence: e, + otherwise alternative: fn() -> Action(a, e), +) -> Action(a, e) { + bool.guard(requirement, failure(consequence), alternative) +} + /// Creates an `Action` that always succeeds and returns `value`. /// /// ## Examples From a160ef7ebf869f5b7c5bf31b282f2f5d32ad2c20 Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 15 Feb 2026 20:45:45 -0500 Subject: [PATCH 16/19] fix code examples --- src/glua.gleam | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/glua.gleam b/src/glua.gleam index ea6b972..2bad89a 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -238,6 +238,7 @@ fn format_unknown_error(error: dynamic.Dynamic) -> String /// executing an `Action` using outdated state could lead to unexpected behaviour. /// /// ```gleam +/// let state = glua.new() /// let result = { /// use #(new_state, _) <- result.try( /// glua.run(state, glua.set(keys: ["a_number"], value: glua.int(36))) @@ -382,7 +383,7 @@ pub fn try(result: Result(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { /// use ret <- glua.then(glua.eval(code: "local a = 1")) /// use <- glua.guard(when: ret == [], return: "expected at least one value from Lua") /// -/// fold(ret, glua.dereference(_, using: decode.int)) +/// glua.fold(ret, glua.dereference(_, using: decode.int)) /// }) /// // -> Error(glua.CustomError("expected at least one value from Lua")) /// ``` From c7062bcc14ee8233355d60136b8fe67ebc4f4519 Mon Sep 17 00:00:00 2001 From: selenil Date: Tue, 17 Feb 2026 14:31:31 -0500 Subject: [PATCH 17/19] rename error_with_code to error_with_level --- src/glua.gleam | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 2bad89a..8872ed8 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -429,9 +429,9 @@ pub fn error(message: String) -> Action(List(Value), e) { call_function_by_name(["error"], [string(message)]) } -/// Invokes the Lua `error` function with the provided message and code. -pub fn error_with_code(message: String, code: Int) -> Action(List(Value), e) { - call_function_by_name(["error"], [string(message), int(code)]) +/// Invokes the Lua `error` function with the provided message and level. +pub fn error_with_level(message: String, level: Int) -> Action(List(Value), e) { + call_function_by_name(["error"], [string(message), int(level)]) } /// Transforms the return value of an `Action` with the provided function. From 051eb622a68a9cf52264de2fd4ee706ce0c6f2a3 Mon Sep 17 00:00:00 2001 From: selenil Date: Tue, 17 Feb 2026 14:34:18 -0500 Subject: [PATCH 18/19] improve tests --- test/glua_test.gleam | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 6a35ac9..40e1abd 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -583,6 +583,39 @@ pub fn call_function_returns_proper_errors_test() { )) = glua.run(glua.new(), action) assert value == "1" + + let fun = + fn(args) { + case args { + [a] -> glua.success([a]) + _ -> glua.error("called without arguments") + } + } + |> glua.function + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(message: msg, level: option.None), + state: _, + )) = glua.run(glua.new(), glua.call_function(fun, [])) + + assert msg == "called without arguments" + + let fun = + fn(args) { + case args { + [] -> glua.success([]) + _ -> glua.error_with_level("function takes 0 arguments", 1) + } + } + |> glua.function + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(message: msg, level: option.Some(level)), + state: _, + )) = glua.run(glua.new(), glua.call_function(fun, [glua.int(2)])) + + assert msg == "function takes 0 arguments" + assert level == 1 } pub fn call_function_by_name_test() { @@ -672,4 +705,7 @@ pub fn format_error_test() { } let assert Error(e) = glua.run(glua.new(), action) assert glua.format_error(e) == "Expected String, but found Int" + + let assert Error(e) = glua.run(glua.new(), glua.failure(1)) + assert glua.format_error(e) == "1" } From ea9421300456c42472025549226aeb00a17b2ac1 Mon Sep 17 00:00:00 2001 From: selenil Date: Thu, 19 Feb 2026 23:21:07 -0500 Subject: [PATCH 19/19] improve tests --- test/glua_test.gleam | 56 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 40e1abd..3fc8e42 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,9 +1,11 @@ import gleam/dict import gleam/dynamic/decode +import gleam/float import gleam/int import gleam/list import gleam/option import gleam/pair +import gleam/result import gleeunit import glua @@ -111,7 +113,7 @@ pub fn new_sandboxed_test() { let code = "local s = require 'example'; return s" use ref <- glua.then(glua.eval(code)) use ref <- glua.try(list.first(ref)) - use result <- glua.then(glua.dereference(ref:, using: decode.string)) + use result <- glua.map(glua.dereference(ref:, using: decode.string)) assert result == "LUA IS AN EMBEDDABLE LANGUAGE" glua.success(Nil) @@ -119,6 +121,58 @@ pub fn new_sandboxed_test() { let assert Ok(_) = glua.run(lua, action) } +pub fn guard_test() { + let action = { + use ret <- glua.then( + glua.call_function_by_name(keys: ["math", "sqrt"], args: [glua.float(9.0)]), + ) + use ref <- glua.try(list.first(ret)) + + use n <- glua.then(glua.dereference(ref:, using: decode.float)) + use <- glua.guard(when: n <. 0.0, return: Nil) + + glua.success("the root square of 9.0 is " <> float.to_string(n)) + } + + assert glua.run(glua.new(), action) |> result.map(pair.second) + == Ok("the root square of 9.0 is 3.0") + + let action = { + use ret <- glua.then(glua.eval(code: "local a = 1")) + use <- glua.guard( + when: ret == [], + return: "Expected at least one return value", + ) + + glua.success("unreachable") + } + + assert glua.run(glua.new(), action) + == Error(glua.CustomError("Expected at least one return value")) +} + +pub fn map_test() { + let action = + glua.get(keys: ["math", "pi"]) + |> glua.then(glua.dereference(_, using: decode.float)) + |> glua.map(float.truncate) + + assert glua.run(glua.new(), action) |> result.map(pair.second) == Ok(3) + + let action = { + use ret <- glua.then(glua.eval("return 3 * true")) + use ref <- glua.try(list.first(ret)) + + glua.map(glua.dereference(ref:, using: decode.int), fn(n) { + "the result is " <> int.to_string(n) + }) + } + let assert Error(glua.LuaRuntimeException(exception, _state)) = + glua.run(glua.new(), action) + + assert exception == glua.BadArith("*", ["3", "true"]) +} + pub fn encoding_and_decoding_nested_tables_test() { let action = { use tb1 <- glua.then(