From 3607a66a2820f10f0cb3da3d417f6956cd1c71a5 Mon Sep 17 00:00:00 2001 From: selenil Date: Wed, 17 Dec 2025 13:47:29 -0500 Subject: [PATCH 01/19] add FileNotFound error --- src/glua.gleam | 2 ++ src/glua_ffi.erl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/glua.gleam b/src/glua.gleam index cc5ae51..759a5ff 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -19,6 +19,8 @@ pub type LuaError { LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua) /// A certain key was not found in the Lua environment. KeyNotFound + /// A Lua source file was not found + FileNotFound /// The value returned by the Lua environment could not be decoded using the provided decoder. UnexpectedResultType(List(decode.DecodeError)) /// An error that could not be identified. diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1edf9e7..132df34 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -62,6 +62,8 @@ is_encoded(_) -> map_error({error, [{_, luerl_parse, Errors} | _], _}) -> FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors), {lua_compiler_exception, FormattedErrors}; +map_error({error, [{_,_,enoent}], _}) -> + file_not_found; map_error({lua_error, {illegal_index, Tbl, Value}, State}) -> FormattedTbl = list_to_binary(io_lib:format("~p", [Tbl])), FormattedValue = unicode:characters_to_binary(Value), From 88f9c3a1655d0704e56413bab248de9c89f895a5 Mon Sep 17 00:00:00 2001 From: selenil Date: Mon, 22 Dec 2025 19:45:08 -0500 Subject: [PATCH 02/19] wip --- src/glua.gleam | 73 +++++++++++++++++++++++++++++++++++++++++--- src/glua_ffi.erl | 15 +++++---- test/glua_test.gleam | 8 ++--- 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 759a5ff..44196a3 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -18,9 +18,9 @@ pub type LuaError { /// The Lua environment threw an exception during code execution. LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua) /// A certain key was not found in the Lua environment. - KeyNotFound + KeyNotFound(key: List(String)) /// A Lua source file was not found - FileNotFound + FileNotFound(path: String) /// The value returned by the Lua environment could not be decoded using the provided decoder. UnexpectedResultType(List(decode.DecodeError)) /// An error that could not be identified. @@ -43,6 +43,60 @@ pub type LuaRuntimeExceptionKind { UnknownException } +/// Turns a `glua.LuaError` value into a human-readable string +/// +/// ## Examples +/// +/// ```gleam +/// ``` +/// +/// ```gleam +/// ``` +/// +/// ```gleam +/// ``` +/// +/// ```gleam +/// ``` +/// +/// ```gleam +/// ``` +/// +pub fn format_error(err: LuaError) -> String { + case err { + LuaCompilerException(errors) -> + "Lua compile error: " <> string.join(errors, with: "\n") + LuaRuntimeException(exn, state) -> + "Lua runtime exception: " + <> format_exception(exn) + <> "\n\n" + <> format_stacktrace(state) + + KeyNotFound(path) -> + "Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found" + FileNotFound(path) -> "File " <> "\"" <> path <> "\"" <> " not found" + UnexpectedResultType(decode_errors) -> "" + UnknownError -> "Unknow error" + } +} + +fn format_exception(exn: LuaRuntimeExceptionKind) -> String { + case exn { + IllegalIndex(index, value) -> + "invalid index " <> index <> "at object " <> value + ErrorCall(msg) -> "error call: " <> string.join(msg, with: ", ") + UndefinedFunction(fun) -> "" + BadArith(operator, args) -> + "bad arithmetic expression: " <> string.join(args, " " <> operator <> " ") + + AssertError(msg) -> "" + UnknownException -> "Unknown exception" + } +} + +@external(erlang, "glua_ffi", "format_stacktrace") +fn format_stacktrace(state: Lua) -> String + /// The exception that happens when a functi /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk @@ -237,7 +291,7 @@ fn sandbox_fun(msg: String) -> Value /// /// ```gleam /// glua.get(state: glua.new(), keys: ["non_existent"], using: decode.string) -/// // -> Error(glua.KeyNotFound) +/// // -> Error(glua.KeyNotFound(["non_existent"])) /// ``` pub fn get( state lua: Lua, @@ -334,7 +388,7 @@ pub fn set( case do_ref_get(lua, keys) { Ok(_) -> Ok(#(keys, lua)) - Error(KeyNotFound) -> { + Error(KeyNotFound(_)) -> { let #(tbl, lua) = alloc_table([], lua) do_set(lua, keys, tbl) |> result.map(fn(lua) { #(keys, lua) }) @@ -430,7 +484,7 @@ fn do_set_private(key: String, value: a, lua: Lua) -> Lua /// /// assert glua.delete_private(lua, "my_value") /// |> glua.get("my_value", decode.string) -/// == Error(glua.KeyNotFound) +/// == Error(glua.KeyNotFound(["my_value"])) /// ``` pub fn delete_private(state lua: Lua, key key: String) -> Lua { do_delete_private(key, lua) @@ -606,6 +660,15 @@ fn do_ref_eval_chunk( /// /// assert results == ["hello, world!"] /// ``` +/// +/// ```gleam +/// glua.eval_file( +/// state: glua.new(), +/// path: "path/to/non/existent/file", +/// using: decode.string +/// ) +/// //-> Error(glua.FileNotFound(["path/to/non/existent/file"])) +/// ``` pub fn eval_file( state lua: Lua, path path: String, diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 132df34..65b1442 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -1,8 +1,7 @@ -module(glua_ffi). - -import(luerl_lib, [lua_error/2]). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, +-export([format_stacktrace/1, coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3]). @@ -67,7 +66,7 @@ map_error({error, [{_,_,enoent}], _}) -> map_error({lua_error, {illegal_index, Tbl, Value}, State}) -> FormattedTbl = list_to_binary(io_lib:format("~p", [Tbl])), FormattedValue = unicode:characters_to_binary(Value), - {lua_runtime_exception, {illegal_index, FormattedTbl, FormattedValue}, State}; + {lua_runtime_exception, {illegal_index, FormattedTbl, FormattedValue}, State}; map_error({lua_error, {error_call, _} = Error, State}) -> {lua_runtime_exception, Error, State}; map_error({lua_error, {undefined_function, Value}, State}) -> @@ -90,6 +89,10 @@ map_error({lua_error, _, State}) -> map_error(_) -> unknown_error. +format_stacktrace(State) -> + Stacktrace = luerl:get_stacktrace(State), + <<"stacktrace"/utf8>>. + coerce(X) -> X. @@ -112,7 +115,7 @@ sandbox_fun(Msg) -> get_table_keys(Lua, Keys) -> case luerl:get_table_keys(Keys, Lua) of {ok, nil, _} -> - {error, key_not_found}; + {error, {key_not_found, Keys}}; {ok, Value, _} -> {ok, Value}; Other -> @@ -122,7 +125,7 @@ get_table_keys(Lua, Keys) -> get_table_keys_dec(Lua, Keys) -> case luerl:get_table_keys_dec(Keys, Lua) of {ok, nil, _} -> - {error, key_not_found}; + {error, {key_not_found, Keys}}; {ok, Value, _} -> {ok, Value}; Other -> @@ -185,5 +188,5 @@ get_private(Lua, Key) -> {ok, luerl:get_private(Key, Lua)} catch error:{badkey, _} -> - {error, key_not_found} + {error, {key_not_found, [Key]}} end. diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 184baa2..bce901a 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -211,14 +211,14 @@ pub fn get_returns_proper_errors_test() { let state = glua.new() assert glua.get(state:, keys: ["non_existent_global"], using: decode.string) - == Error(glua.KeyNotFound) + == 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"], using: decode.int) - == Error(glua.KeyNotFound) + == Error(glua.KeyNotFound(["my_table", "my_val"])) } pub fn set_test() { @@ -341,7 +341,7 @@ pub fn get_private_test() { assert glua.new() |> glua.get_private("non_existent", using: decode.string) - == Error(glua.KeyNotFound) + == Error(glua.KeyNotFound(["non_existent"])) } pub fn delete_private_test() { @@ -352,7 +352,7 @@ pub fn delete_private_test() { assert glua.delete_private(lua, "the_value") |> glua.get_private(key: "the_value", using: decode.string) - == Error(glua.KeyNotFound) + == Error(glua.KeyNotFound(["the_value"])) } pub fn load_test() { From 35bcda211e511823cd696a65add11c6cd475ea43 Mon Sep 17 00:00:00 2001 From: selenil Date: Sat, 3 Jan 2026 16:59:51 -0500 Subject: [PATCH 03/19] improve FileNotFound --- src/glua.gleam | 3 ++- src/glua_ffi.erl | 9 +++++---- test/glua_test.gleam | 4 ++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 44196a3..984db6d 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -74,7 +74,8 @@ pub fn format_error(err: LuaError) -> String { KeyNotFound(path) -> "Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found" - FileNotFound(path) -> "File " <> "\"" <> path <> "\"" <> " not found" + FileNotFound(path) -> + "Lua source file " <> "\"" <> path <> "\"" <> " not found" UnexpectedResultType(decode_errors) -> "" UnknownError -> "Unknow error" } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 65b1442..5075a82 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -61,8 +61,6 @@ is_encoded(_) -> map_error({error, [{_, luerl_parse, Errors} | _], _}) -> FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors), {lua_compiler_exception, FormattedErrors}; -map_error({error, [{_,_,enoent}], _}) -> - file_not_found; map_error({lua_error, {illegal_index, Tbl, Value}, State}) -> FormattedTbl = list_to_binary(io_lib:format("~p", [Tbl])), FormattedValue = unicode:characters_to_binary(Value), @@ -144,8 +142,11 @@ load(Lua, Code) -> unicode:characters_to_list(Code), Lua)). load_file(Lua, Path) -> - to_gleam(luerl:loadfile( - unicode:characters_to_list(Path), Lua)). + case luerl:loadfile(unicode:characters_to_list(Path), Lua) of + {error, [{none, file, enoent} | _], _} -> + {error, {file_not_found, Path}}; + Other -> to_gleam(Other) + end. eval(Lua, Code) -> to_gleam(luerl:do( diff --git a/test/glua_test.gleam b/test/glua_test.gleam index bce901a..f6d256a 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -371,6 +371,10 @@ pub fn eval_load_file_test() { glua.eval_chunk(state: lua, chunk:, using: decode.string) assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + + let assert Error(e) = + glua.load_file(state: glua.new(), path: "non_existent_file") + assert e == glua.FileNotFound("non_existent_file") } pub fn eval_test() { From 7ff6dfb24877cd4d9acbbaec9cc2f696ae2c5128 Mon Sep 17 00:00:00 2001 From: selenil Date: Sat, 3 Jan 2026 20:15:25 -0500 Subject: [PATCH 04/19] remove LuaCompilerException and add LuaCompileFailure --- src/glua.gleam | 36 ++++++++++++++++++++++++++++++++---- src/glua_ffi.erl | 17 +++++++++++++---- test/glua_test.gleam | 18 ++++++++++++++---- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 984db6d..00d130d 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -4,6 +4,7 @@ import gleam/dynamic import gleam/dynamic/decode +import gleam/int import gleam/list import gleam/result import gleam/string @@ -13,8 +14,8 @@ pub type Lua /// Represents the errors than can happend during the parsing and execution of Lua code pub type LuaError { - /// There was an exception when compiling the Lua code. - LuaCompilerException(messages: List(String)) + /// 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. LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua) /// A certain key was not found in the Lua environment. @@ -27,6 +28,17 @@ pub type LuaError { UnknownError } +/// Represents a Lua compilation error +pub type LuaCompileError { + LuaCompileError(line: Int, kind: LuaCompileErrorKind, message: String) +} + +/// Represents the kind of a Lua compilation error +pub type LuaCompileErrorKind { + Parse + Tokenize +} + /// Represents the kind of exceptions that can happen at runtime during Lua code execution. pub type LuaRuntimeExceptionKind { /// The exception that happens when trying to access an index that does not exists on a table (also happens when indexing non-table values). @@ -64,8 +76,10 @@ pub type LuaRuntimeExceptionKind { /// pub fn format_error(err: LuaError) -> String { case err { - LuaCompilerException(errors) -> - "Lua compile error: " <> string.join(errors, with: "\n") + LuaCompileFailure(errors) -> + "Lua compile error: " + <> "\n\n" + <> string.join(list.map(errors, format_compile_error), with: "\n") LuaRuntimeException(exn, state) -> "Lua runtime exception: " <> format_exception(exn) @@ -81,6 +95,20 @@ pub fn format_error(err: LuaError) -> String { } } +fn format_compile_error(error: LuaCompileError) -> String { + let kind = case error.kind { + Parse -> "parse" + Tokenize -> "tokenize" + } + + "Failed to " + <> kind + <> ": error on line " + <> int.to_string(error.line) + <> ": " + <> error.message +} + fn format_exception(exn: LuaRuntimeExceptionKind) -> String { case exn { IllegalIndex(index, value) -> diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 5075a82..eae8313 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -57,10 +57,8 @@ is_encoded({erl_mfa,_,_,_}) -> is_encoded(_) -> false. -%% TODO: Improve compiler errors handling and try to detect more errors -map_error({error, [{_, luerl_parse, Errors} | _], _}) -> - FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors), - {lua_compiler_exception, FormattedErrors}; +map_error({error, Errors, _}) -> + {lua_compile_failure, lists:map(fun map_compile_error/1, Errors)}; map_error({lua_error, {illegal_index, Tbl, Value}, State}) -> FormattedTbl = list_to_binary(io_lib:format("~p", [Tbl])), FormattedValue = unicode:characters_to_binary(Value), @@ -87,6 +85,17 @@ map_error({lua_error, _, State}) -> map_error(_) -> unknown_error. +map_compile_error({Line, Type, {user, Messages}}) -> + map_compile_error({Line, Type, Messages}); +map_compile_error({Line, Type, {illegal, Token}}) -> + map_compile_error({Line, Type, io_lib:format("~p ~p",["Illegal token",Token])}); +map_compile_error({Line, Type, Messages}) -> + Kind = case Type of + luerl_parse -> parse; + luerl_scan -> tokenize + end, + {lua_compile_error, Line, Kind, unicode:characters_to_binary(Messages)}. + format_stacktrace(State) -> Stacktrace = luerl:get_stacktrace(State), <<"stacktrace"/utf8>>. diff --git a/test/glua_test.gleam b/test/glua_test.gleam index f6d256a..4e52415 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -396,10 +396,20 @@ pub fn eval_test() { pub fn eval_returns_proper_errors_test() { let state = glua.new() - assert glua.eval(state:, code: "if true then 1 + ", using: decode.int) - == Error( - glua.LuaCompilerException(messages: ["syntax error before: ", "1"]), - ) + let assert Error(e) = + glua.eval(state:, code: "if true then 1 + ", using: decode.int) + assert e + == glua.LuaCompileFailure([ + glua.LuaCompileError(1, glua.Parse, "syntax error before: 1"), + ]) + + let assert Error(e) = + glua.eval(state:, code: "print(\"hi)", using: decode.int) + + assert e + == glua.LuaCompileFailure([ + glua.LuaCompileError(1, glua.Tokenize, "syntax error near '\"'"), + ]) assert glua.eval(state:, code: "return 'Hello from Lua!'", using: decode.int) == Error( From 23f233f491b6983601bfd83763eb2a52e2e88c64 Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 4 Jan 2026 00:12:40 -0500 Subject: [PATCH 05/19] format decode errors --- src/glua.gleam | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/glua.gleam b/src/glua.gleam index 00d130d..3918b7a 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -90,7 +90,8 @@ pub fn format_error(err: LuaError) -> String { "Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found" FileNotFound(path) -> "Lua source file " <> "\"" <> path <> "\"" <> " not found" - UnexpectedResultType(decode_errors) -> "" + UnexpectedResultType(decode_errors) -> + list.map(decode_errors, format_decode_error) |> string.join(with: "\n") UnknownError -> "Unknow error" } } @@ -126,6 +127,15 @@ fn format_exception(exn: LuaRuntimeExceptionKind) -> String { @external(erlang, "glua_ffi", "format_stacktrace") fn format_stacktrace(state: Lua) -> String +fn format_decode_error(error: decode.DecodeError) -> String { + let base = "Expected " <> error.expected <> ", but found " <> error.found + + case error.path { + [] -> base + path -> base <> " at " <> string.join(path, with: ".") + } +} + /// The exception that happens when a functi /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk From db4076faa771b13d96ceb1840a46bc9cffe83554 Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 4 Jan 2026 00:17:40 -0500 Subject: [PATCH 06/19] improve unknown error --- src/glua.gleam | 7 +++++-- src/glua_ffi.erl | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 3918b7a..6e2b496 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -25,7 +25,7 @@ pub type LuaError { /// The value returned by the Lua environment could not be decoded using the provided decoder. UnexpectedResultType(List(decode.DecodeError)) /// An error that could not be identified. - UnknownError + UnknownError(error: dynamic.Dynamic) } /// Represents a Lua compilation error @@ -92,7 +92,7 @@ pub fn format_error(err: LuaError) -> String { "Lua source file " <> "\"" <> path <> "\"" <> " not found" UnexpectedResultType(decode_errors) -> list.map(decode_errors, format_decode_error) |> string.join(with: "\n") - UnknownError -> "Unknow error" + UnknownError(error) -> "Unknown error: " <> format_unknown_error(error) } } @@ -136,6 +136,9 @@ fn format_decode_error(error: decode.DecodeError) -> String { } } +@external(erlang, "luerl_lib", "format_error") +fn format_unknown_error(error: dynamic.Dynamic) -> String + /// The exception that happens when a functi /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index eae8313..af18005 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -27,7 +27,7 @@ to_gleam(Value) -> {error, _, _} = Error -> {error, map_error(Error)}; error -> - {error, unknown_error} + {error, {unknown_error, nil}} end. %% helper to determine if a value is encoded or not @@ -82,8 +82,8 @@ map_error({lua_error, {assert_error, _} = Error, State}) -> {lua_runtime_exception, Error, State}; map_error({lua_error, _, State}) -> {lua_runtime_exception, unknown_exception, State}; -map_error(_) -> - unknown_error. +map_error(Error) -> + {unknown_error, Error}. map_compile_error({Line, Type, {user, Messages}}) -> map_compile_error({Line, Type, Messages}); From 64cc7724439a989f83b1fdb01ffb8414e9fed8b2 Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 4 Jan 2026 00:50:28 -0500 Subject: [PATCH 07/19] improve runtime exceptions handling --- src/glua.gleam | 39 ++++++++++++++++++++++++++++----------- src/glua_ffi.erl | 17 ++++++++++------- test/glua_test.gleam | 35 ++++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 6e2b496..8c8d5d7 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -6,6 +6,7 @@ import gleam/dynamic import gleam/dynamic/decode import gleam/int import gleam/list +import gleam/option import gleam/result import gleam/string @@ -42,9 +43,9 @@ pub type LuaCompileErrorKind { /// Represents the kind of exceptions that can happen at runtime during Lua code execution. pub type LuaRuntimeExceptionKind { /// The exception that happens when trying to access an index that does not exists on a table (also happens when indexing non-table values). - IllegalIndex(value: String, index: String) + IllegalIndex(index: String, value: String) /// The exception that happens when the `error` function is called. - ErrorCall(messages: List(String)) + ErrorCall(message: String, level: option.Option(Int)) /// The exception that happens when trying to call a function that is not defined. UndefinedFunction(value: String) /// The exception that happens when an invalid arithmetic operation is performed. @@ -80,9 +81,9 @@ pub fn format_error(err: LuaError) -> String { "Lua compile error: " <> "\n\n" <> string.join(list.map(errors, format_compile_error), with: "\n") - LuaRuntimeException(exn, state) -> + LuaRuntimeException(exception, state) -> "Lua runtime exception: " - <> format_exception(exn) + <> format_exception(exception) <> "\n\n" <> format_stacktrace(state) @@ -110,16 +111,32 @@ fn format_compile_error(error: LuaCompileError) -> String { <> error.message } -fn format_exception(exn: LuaRuntimeExceptionKind) -> String { - case exn { +fn format_exception(exception: LuaRuntimeExceptionKind) -> String { + case exception { IllegalIndex(index, value) -> - "invalid index " <> index <> "at object " <> value - ErrorCall(msg) -> "error call: " <> string.join(msg, with: ", ") - UndefinedFunction(fun) -> "" + "Invalid index " + <> "\"" + <> index + <> "\"" + <> " at object " + <> "\"" + <> value + <> "\"" + ErrorCall(msg, level) -> { + let base = "error call: " <> msg + + case level { + option.Some(level) -> base <> " at level " <> int.to_string(level) + option.None -> base + } + } + + UndefinedFunction(fun) -> "Undefined function: " <> fun BadArith(operator, args) -> - "bad arithmetic expression: " <> string.join(args, " " <> operator <> " ") + "Bad arithmetic expression: " + <> string.join(args, with: " " <> operator <> " ") - AssertError(msg) -> "" + AssertError(msg) -> "Assertion failed with message: " <> msg UnknownException -> "Unknown exception" } } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index af18005..303f275 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -59,16 +59,19 @@ is_encoded(_) -> map_error({error, Errors, _}) -> {lua_compile_failure, lists:map(fun map_compile_error/1, Errors)}; -map_error({lua_error, {illegal_index, Tbl, Value}, State}) -> - FormattedTbl = list_to_binary(io_lib:format("~p", [Tbl])), - FormattedValue = unicode:characters_to_binary(Value), - {lua_runtime_exception, {illegal_index, FormattedTbl, FormattedValue}, State}; -map_error({lua_error, {error_call, _} = Error, State}) -> +map_error({lua_error, {illegal_index, Value, Index}, State}) -> + FormattedIndex = unicode:characters_to_binary(Index), + FormattedValue = unicode:characters_to_binary(io_lib:format("~p",[luerl:decode(Value, State)])), + {lua_runtime_exception, {illegal_index, FormattedIndex, FormattedValue}, State}; +map_error({lua_error, {error_call, Args}, State}) -> + Error = case Args of + [Msg, Level] when is_binary(Msg) andalso is_integer(Level) -> {error_call, Msg, {some, Level}}; + [Msg] when is_binary(Msg) -> {error_call, Msg, none} + end, {lua_runtime_exception, Error, State}; map_error({lua_error, {undefined_function, Value}, State}) -> {lua_runtime_exception, - {undefined_function, list_to_binary(io_lib:format("~p", [Value]))}, - State}; + {undefined_function, unicode:characters_to_binary(io_lib:format("~p",[Value]))}, State}; map_error({lua_error, {badarith, Operator, Args}, State}) -> FormattedOperator = unicode:characters_to_binary(atom_to_list(Operator)), FormattedArgs = diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 4e52415..aa7ed84 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -53,18 +53,18 @@ pub fn sandbox_test() { using: decode.int, ) - assert exception == glua.ErrorCall(["math.max is sandboxed"]) + 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(_, name), _)) = + let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = glua.eval( state: lua, code: "return string.upper('my_string')", using: decode.string, ) - assert name == "upper" + assert index == "upper" let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) @@ -74,7 +74,7 @@ pub fn sandbox_test() { code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", ) - assert exception == glua.ErrorCall(["os.execute is sandboxed"]) + 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") @@ -86,7 +86,7 @@ pub fn sandbox_test() { using: decode.string, ) - assert exception == glua.ErrorCall(["print is sandboxed"]) + assert exception == glua.ErrorCall("print is sandboxed", option.None) } pub fn new_sandboxed_test() { @@ -95,18 +95,18 @@ pub fn new_sandboxed_test() { let assert Error(glua.LuaRuntimeException(exception, _)) = glua.ref_eval(state: lua, code: "return load(\"return 1\")") - assert exception == glua.ErrorCall(["load is sandboxed"]) + assert exception == glua.ErrorCall("load is sandboxed", option.None) let arg = glua.int(1) let assert Error(glua.LuaRuntimeException(exception, _)) = glua.ref_call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) - assert exception == glua.ErrorCall(["os.exit is sandboxed"]) + assert exception == glua.ErrorCall("os.exit is sandboxed", option.None) - let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = + let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = glua.ref_eval(state: lua, code: "io.write('some_message')") - assert name == "write" + 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"]) @@ -173,10 +173,9 @@ pub fn userdata_test() { let userdata = Userdata("other_userdata", 2) let assert Ok(lua) = glua.set(lua, ["my_other_userdata"], glua.userdata(userdata)) - let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(value, index), _)) = + let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = glua.eval(lua, "return my_other_userdata.foo", decode.string) - assert value == "{usdref,1}" assert index == "foo" } @@ -425,11 +424,21 @@ pub fn eval_returns_proper_errors_test() { assert index == "b" let assert Error(glua.LuaRuntimeException( - exception: glua.ErrorCall(messages:), + exception: glua.ErrorCall(message, level), state: _, )) = glua.eval(state:, code: "error('error message')", using: decode.int) - assert messages == ["error message"] + assert message == "error message" + assert level == option.None + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(message, level), + state: _, + )) = + glua.eval(state:, code: "error('error with level', 1)", using: decode.int) + + assert message == "error with level" + assert level == option.Some(1) let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), From 2cbaf531f31c5e256ff28d4c878696e61f38290e Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 4 Jan 2026 00:53:37 -0500 Subject: [PATCH 08/19] minor renaming --- src/glua.gleam | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 8c8d5d7..ea10e1b 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -75,8 +75,8 @@ pub type LuaRuntimeExceptionKind { /// ```gleam /// ``` /// -pub fn format_error(err: LuaError) -> String { - case err { +pub fn format_error(error: LuaError) -> String { + case error { LuaCompileFailure(errors) -> "Lua compile error: " <> "\n\n" From 0e5d59ce46c82ad1391e3d41fabdf1fd514b064a Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 4 Jan 2026 13:39:21 -0500 Subject: [PATCH 09/19] add UndefinedMethod exception --- src/glua.gleam | 11 +++++++++++ src/glua_ffi.erl | 3 +++ test/glua_test.gleam | 13 +++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/glua.gleam b/src/glua.gleam index ea10e1b..42909dd 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -48,6 +48,8 @@ pub type LuaRuntimeExceptionKind { ErrorCall(message: String, level: option.Option(Int)) /// The exception that happens when trying to call a function that is not defined. UndefinedFunction(value: String) + /// The exception that happens when trying to call a method that is not defined for an object. + UndefinedMethod(object: String, method: String) /// The exception that happens when an invalid arithmetic operation is performed. BadArith(operator: String, args: List(String)) /// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument. @@ -132,6 +134,15 @@ fn format_exception(exception: LuaRuntimeExceptionKind) -> String { } UndefinedFunction(fun) -> "Undefined function: " <> fun + UndefinedMethod(obj, method) -> + "Undefined method " + <> "\"" + <> method + <> "\"" + <> " for object: " + <> "\"" + <> obj + <> "\"" BadArith(operator, args) -> "Bad arithmetic expression: " <> string.join(args, with: " " <> operator <> " ") diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 303f275..b72fbae 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -72,6 +72,9 @@ map_error({lua_error, {error_call, Args}, State}) -> map_error({lua_error, {undefined_function, Value}, State}) -> {lua_runtime_exception, {undefined_function, unicode:characters_to_binary(io_lib:format("~p",[Value]))}, State}; +map_error({lua_error, {undefined_method, Obj, Value}, State}) -> + {lua_runtime_exception, + {undefined_method, unicode:characters_to_binary(io_lib:format("~p", [Obj])), Value}, State}; map_error({lua_error, {badarith, Operator, Args}, State}) -> FormattedOperator = unicode:characters_to_binary(atom_to_list(Operator)), FormattedArgs = diff --git a/test/glua_test.gleam b/test/glua_test.gleam index aa7ed84..78c3763 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -446,6 +446,19 @@ pub fn eval_returns_proper_errors_test() { )) = glua.eval(state:, code: "local a = 5; a()", using: decode.int) assert value == "5" + + let assert Error(glua.LuaRuntimeException( + exception: glua.UndefinedMethod(_, method:), + state: _, + )) = + glua.eval( + state:, + code: "return io:write('something')", + using: decode.string, + ) + + assert method == "write" + let assert Error(glua.LuaRuntimeException( exception: glua.BadArith(operator:, args:), state: _, From f2c6d3e88e10d42566fabf30020a27bcd55e4189 Mon Sep 17 00:00:00 2001 From: selenil Date: Sun, 4 Jan 2026 13:59:57 -0500 Subject: [PATCH 10/19] add format_error test --- src/glua.gleam | 13 ++++++++----- src/glua_ffi.erl | 4 ++-- test/glua_test.gleam | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index 42909dd..9c08bb8 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -83,12 +83,15 @@ pub fn format_error(error: LuaError) -> String { "Lua compile error: " <> "\n\n" <> string.join(list.map(errors, format_compile_error), with: "\n") - LuaRuntimeException(exception, state) -> - "Lua runtime exception: " - <> format_exception(exception) - <> "\n\n" - <> format_stacktrace(state) + LuaRuntimeException(exception, state) -> { + let base = "Lua runtime exception: " <> format_exception(exception) + let stacktrace = format_stacktrace(state) + case stacktrace { + "" -> base + stacktrace -> base <> "\n\n" <> stacktrace + } + } KeyNotFound(path) -> "Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found" FileNotFound(path) -> diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index b72fbae..8e56a30 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -103,8 +103,8 @@ map_compile_error({Line, Type, Messages}) -> {lua_compile_error, Line, Kind, unicode:characters_to_binary(Messages)}. format_stacktrace(State) -> - Stacktrace = luerl:get_stacktrace(State), - <<"stacktrace"/utf8>>. + %% Stacktrace = luerl:get_stacktrace(State), + <<""/utf8>>. coerce(X) -> X. diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 78c3763..50fca1c 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -587,3 +587,36 @@ pub fn nested_function_references_test() { glua.call_function(state: lua, ref:, args: [arg], using: decode.float) assert result == 20.0 } + +pub fn format_error_test() { + let state = glua.new() + + let assert Error(e) = glua.ref_eval(state:, code: "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.ref_eval(state:, code: "error('an error')") + assert glua.format_error(e) == "Lua runtime exception: error call: an error" + + let assert Error(e) = + glua.ref_eval(state:, code: "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.ref_eval(state:, code: "assert(false, 'assertion message')") + assert glua.format_error(e) + == "Lua runtime exception: Assertion failed with message: assertion message" + + let assert Error(e) = + glua.get(state:, keys: ["non_existent"], using: decode.string) + assert glua.format_error(e) == "Key \"non_existent\" not found" + + let assert Error(e) = glua.load_file(state:, path: "non_existent_file") + assert glua.format_error(e) + == "Lua source file \"non_existent_file\" not found" + + let assert Error(e) = + glua.eval(state:, code: "return 1 + 1", using: decode.string) + assert glua.format_error(e) == "Expected String, but found Int" +} From f51ae7fc844c1f16aaa2180ab9fbb75967443957 Mon Sep 17 00:00:00 2001 From: selenil Date: Mon, 5 Jan 2026 17:17:03 -0500 Subject: [PATCH 11/19] add format_error examples --- src/glua.gleam | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/glua.gleam b/src/glua.gleam index 9c08bb8..da3efa4 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -63,20 +63,59 @@ pub type LuaRuntimeExceptionKind { /// ## Examples /// /// ```gleam +/// let assert Error(e) = glua.eval( +/// state: glua.new(), +/// code: "if true end", +/// using: decode.string +/// ) +/// +/// 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(), +/// code: "local a = 1; local b = true; return a + b", +/// using: decode.string +/// ) +/// +/// glua.format_error(e) +/// // -> "Lua runtime exception: Bad arithmetic expression: 1 + true" /// ``` /// /// ```gleam +/// let assert Error(e) = glua.get( +/// state: glua.new(), +/// keys: ["a_value"], +/// using: decode.string +/// ) +/// +/// glua.format_error(e) +/// // -> "Key \"a_value\" not found" /// ``` /// /// ```gleam +/// let assert Error(e) = glua.eval_file( +/// state: glua.new(), +/// path: "my_lua_file.lua", +/// using: decode.string +/// ) +/// +/// glua.format_error(e) +/// // -> "Lua source file \"my_lua_file.lua\" not found" /// ``` /// /// ```gleam -/// ``` +/// let assert Error(e) = glua.eval( +/// state: glua.new(), +/// code: "return 1 + 1", +/// using: decode.string +/// ) /// +/// glua.format_error(e) +/// // -> "Expected String, but found Int" +/// ``` pub fn format_error(error: LuaError) -> String { case error { LuaCompileFailure(errors) -> From 7d66915c0df65550161a8292609f252fa7be7abb Mon Sep 17 00:00:00 2001 From: selenil Date: Mon, 5 Jan 2026 21:41:45 -0500 Subject: [PATCH 12/19] fix tests --- test/glua_test.gleam | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 50fca1c..826c913 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -453,11 +453,11 @@ pub fn eval_returns_proper_errors_test() { )) = glua.eval( state:, - code: "return io:write('something')", + code: "local i = function(x) return x end; i:call(1)", using: decode.string, ) - assert method == "write" + assert method == "call" let assert Error(glua.LuaRuntimeException( exception: glua.BadArith(operator:, args:), From b9b4a444378e7d8c7ea46185f7ef77591ae24ddb Mon Sep 17 00:00:00 2001 From: selenil Date: Tue, 6 Jan 2026 15:51:03 -0500 Subject: [PATCH 13/19] implement format_stacktrace --- src/glua_ffi.erl | 63 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 8e56a30..2c793ad 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -102,9 +102,68 @@ map_compile_error({Line, Type, Messages}) -> end, {lua_compile_error, Line, Kind, unicode:characters_to_binary(Messages)}. +%% retrieves the stacktraces for a given state +%% and returns it in a pretty-string +%% borrowed from: https://github.com/tv-labs/lua format_stacktrace(State) -> - %% Stacktrace = luerl:get_stacktrace(State), - <<""/utf8>>. + Stacktrace = luerl:get_stacktrace(State), + [_ | Rest] = Stacktrace, + Zipped = gleam@list:zip(Stacktrace, Rest), + Lines = lists:map( + fun + ({{Func, [{tref, _} = Tref | Args], _}, {_, _, Context}}) -> + Keys = lists:map( + fun({K, _}) -> io_lib:format("~p", [K]) end, + luerl:decode(Tref, State) + ), + FormattedArgs = format_args(Args), + io_lib:format( + "~p with arguments ~s\n" + "^--- self is incorrect for object with keys ~s\n\n\n" + "Line ~p", + [ + Func, + FormattedArgs, + lists:join(", ", Keys), + proplists:get_value(line, Context) + ] + ); + ({{Func, Args, _}, {_, _, Context}}) -> + FormattedArgs = format_args(Args), + Name = + case Func of + nil -> + " " ++ FormattedArgs; + "-no-name-" -> + ""; + {luerl_lib_basic, basic_error} -> + "error" ++ FormattedArgs; + {luerl_lib_basic, basic_error, undefined} -> + "error" ++ FormattedArgs; + {luerl_lib_basic, error_call, undefined} -> + "error" ++ FormattedArgs; + {luerl_lib_basic, assert, undefined} -> + "assert" ++ FormattedArgs; + _ -> + N = + case Func of + {tref, _} -> ""; + _ -> Func + end, + " " ++ N ++ FormattedArgs + end, + io_lib:format("Line ~p: ~s", [ + proplists:get_value(line, Context), + Name + ]) + end, + Zipped + ), + unicode:characters_to_binary(lists:join("\n", Lines)). + +%% borrowed from: https://github.com/tv-labs/lua +format_args(Args) -> + ["(", lists:join(", ", lists:map(fun(A) -> io_lib:format("~p",[A]) end, Args)), ")"]. coerce(X) -> X. From 636600bb5c21703d914795ff8c0390e28df4b412 Mon Sep 17 00:00:00 2001 From: selenil Date: Tue, 6 Jan 2026 16:35:36 -0500 Subject: [PATCH 14/19] fix crashes when assert and error are called with incorrect arguments --- src/glua_ffi.erl | 26 +++++++++++++++++++------- test/glua_test.gleam | 5 +++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 2c793ad..6d4937c 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -64,11 +64,16 @@ map_error({lua_error, {illegal_index, Value, Index}, State}) -> FormattedValue = unicode:characters_to_binary(io_lib:format("~p",[luerl:decode(Value, State)])), {lua_runtime_exception, {illegal_index, FormattedIndex, FormattedValue}, State}; map_error({lua_error, {error_call, Args}, State}) -> - Error = case Args of - [Msg, Level] when is_binary(Msg) andalso is_integer(Level) -> {error_call, Msg, {some, Level}}; - [Msg] when is_binary(Msg) -> {error_call, Msg, none} - end, - {lua_runtime_exception, Error, State}; + case Args of + [Msg, Level] when is_binary(Msg) andalso is_integer(Level) -> + {lua_runtime_exception, {error_call, Msg, {some, Level}}, State}; + [Msg] when is_binary(Msg) -> + {lua_runtime_exception, {error_call, Msg, none}, State}; + + % error() was called with incorrect arguments + _ -> + {unknown_error, {error_call, Args}} + end; map_error({lua_error, {undefined_function, Value}, State}) -> {lua_runtime_exception, {undefined_function, unicode:characters_to_binary(io_lib:format("~p",[Value]))}, State}; @@ -84,8 +89,15 @@ map_error({lua_error, {badarith, Operator, Args}, State}) -> end, Args), {lua_runtime_exception, {bad_arith, FormattedOperator, FormattedArgs}, State}; -map_error({lua_error, {assert_error, _} = Error, State}) -> - {lua_runtime_exception, Error, State}; +map_error({lua_error, {assert_error, Msg} = Error, State}) -> + case Msg of + M when is_binary(M) -> + {lua_runtime_exception, Error, State}; + + % assert() was called with incorrect arguments + _ -> + {unknown_error, Error} + end; map_error({lua_error, _, State}) -> {lua_runtime_exception, unknown_exception, State}; map_error(Error) -> diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 826c913..8db0481 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -440,6 +440,8 @@ pub fn eval_returns_proper_errors_test() { assert message == "error with level" assert level == option.Some(1) + let assert Error(_) = glua.eval(state:, code: "error({1})", using: decode.int) + let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), state: _, @@ -478,6 +480,9 @@ pub fn eval_returns_proper_errors_test() { ) assert message == "assertion failed" + + let assert Error(_) = + glua.eval(state:, code: "assert(false, {1})", using: decode.int) } pub fn eval_file_test() { From 024aff617314f214d03532241e29545557d84b8d Mon Sep 17 00:00:00 2001 From: selenil Date: Tue, 6 Jan 2026 19:21:51 -0500 Subject: [PATCH 15/19] fix tests --- src/glua.gleam | 2 +- test/glua_test.gleam | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index da3efa4..c142fd9 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -167,7 +167,7 @@ fn format_exception(exception: LuaRuntimeExceptionKind) -> String { <> value <> "\"" ErrorCall(msg, level) -> { - let base = "error call: " <> msg + let base = "Error call: " <> msg case level { option.Some(level) -> base <> " at level " <> int.to_string(level) diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 8db0481..a7cce4e 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -600,19 +600,15 @@ pub fn format_error_test() { 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.ref_eval(state:, code: "error('an error')") - assert glua.format_error(e) == "Lua runtime exception: error call: an error" + let assert Error(e) = glua.ref_eval(state:, code: "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.ref_eval(state:, code: "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.ref_eval(state:, code: "assert(false, 'assertion message')") - assert glua.format_error(e) - == "Lua runtime exception: Assertion failed with message: assertion message" - let assert Error(e) = glua.get(state:, keys: ["non_existent"], using: decode.string) assert glua.format_error(e) == "Key \"non_existent\" not found" From 19111b8e7530c635e5970cf7977dd65583fb3061 Mon Sep 17 00:00:00 2001 From: selenil Date: Tue, 6 Jan 2026 19:33:38 -0500 Subject: [PATCH 16/19] improve code --- src/glua.gleam | 6 +++--- src/glua_ffi.erl | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index c142fd9..9e0e2f5 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -124,7 +124,7 @@ pub fn format_error(error: LuaError) -> String { <> string.join(list.map(errors, format_compile_error), with: "\n") LuaRuntimeException(exception, state) -> { let base = "Lua runtime exception: " <> format_exception(exception) - let stacktrace = format_stacktrace(state) + let stacktrace = get_stacktrace(state) case stacktrace { "" -> base @@ -194,8 +194,8 @@ fn format_exception(exception: LuaRuntimeExceptionKind) -> String { } } -@external(erlang, "glua_ffi", "format_stacktrace") -fn format_stacktrace(state: Lua) -> String +@external(erlang, "glua_ffi", "get_stacktrace") +fn get_stacktrace(state: Lua) -> String fn format_decode_error(error: decode.DecodeError) -> String { let base = "Expected " <> error.expected <> ", but found " <> error.found diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 6d4937c..0491be3 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -1,7 +1,7 @@ -module(glua_ffi). -import(luerl_lib, [lua_error/2]). --export([format_stacktrace/1, coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, +-export([get_stacktrace/1, coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3]). @@ -114,12 +114,17 @@ map_compile_error({Line, Type, Messages}) -> end, {lua_compile_error, Line, Kind, unicode:characters_to_binary(Messages)}. -%% retrieves the stacktraces for a given state -%% and returns it in a pretty-string + +get_stacktrace(State) -> + case luerl:get_stacktrace(State) of + [] -> + <<"">>; + Stacktrace -> format_stacktrace(State, Stacktrace) + end. + +%% turns a Lua stacktrace into a string suitable for pretty-printing %% borrowed from: https://github.com/tv-labs/lua -format_stacktrace(State) -> - Stacktrace = luerl:get_stacktrace(State), - [_ | Rest] = Stacktrace, +format_stacktrace(State, [_ | Rest] = Stacktrace) -> Zipped = gleam@list:zip(Stacktrace, Rest), Lines = lists:map( fun From 357873349a156f48de972d617390f8172de484dd Mon Sep 17 00:00:00 2001 From: selenil Date: Wed, 7 Jan 2026 13:19:51 -0500 Subject: [PATCH 17/19] add badarg exception --- src/glua.gleam | 10 ++++++++++ src/glua_ffi.erl | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/glua.gleam b/src/glua.gleam index 9e0e2f5..b403762 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -52,6 +52,8 @@ pub type LuaRuntimeExceptionKind { UndefinedMethod(object: String, method: String) /// The exception that happens when an invalid arithmetic operation is performed. BadArith(operator: String, args: List(String)) + /// The exception that happens when a function is called with incorrect arguments. + Badarg(function: String, args: List(String)) /// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument. AssertError(message: String) /// An exception that could not be identified @@ -189,6 +191,11 @@ fn format_exception(exception: LuaRuntimeExceptionKind) -> String { "Bad arithmetic expression: " <> string.join(args, with: " " <> operator <> " ") + Badarg(function, args) -> + "Bad argument " + <> string.join(list.map(args, format_lua_value), with: ", ") + <> " for function " + <> function AssertError(msg) -> "Assertion failed with message: " <> msg UnknownException -> "Unknown exception" } @@ -206,6 +213,9 @@ fn format_decode_error(error: decode.DecodeError) -> String { } } +@external(erlang, "luerl_lib", "format_value") +fn format_lua_value(v: anything) -> String + @external(erlang, "luerl_lib", "format_error") fn format_unknown_error(error: dynamic.Dynamic) -> String diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 0491be3..7612e42 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -98,6 +98,8 @@ map_error({lua_error, {assert_error, Msg} = Error, State}) -> _ -> {unknown_error, Error} end; +map_error({lua_error, {badarg, F, Args}, State}) -> + {lua_runtime_exception, {badarg, atom_to_binary(F), Args}, State}; map_error({lua_error, _, State}) -> {lua_runtime_exception, unknown_exception, State}; map_error(Error) -> From 71939b116d268dd6a43a046905d08f812a658ec7 Mon Sep 17 00:00:00 2001 From: selenil Date: Wed, 7 Jan 2026 13:38:08 -0500 Subject: [PATCH 18/19] more improvements --- src/glua_ffi.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 7612e42..497a1b9 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -152,7 +152,7 @@ format_stacktrace(State, [_ | Rest] = Stacktrace) -> Name = case Func of nil -> - " " ++ FormattedArgs; + "" ++ FormattedArgs; "-no-name-" -> ""; {luerl_lib_basic, basic_error} -> @@ -169,7 +169,7 @@ format_stacktrace(State, [_ | Rest] = Stacktrace) -> {tref, _} -> ""; _ -> Func end, - " " ++ N ++ FormattedArgs + io_lib:format("~p~s", [N, FormattedArgs]) end, io_lib:format("Line ~p: ~s", [ proplists:get_value(line, Context), @@ -182,7 +182,7 @@ format_stacktrace(State, [_ | Rest] = Stacktrace) -> %% borrowed from: https://github.com/tv-labs/lua format_args(Args) -> - ["(", lists:join(", ", lists:map(fun(A) -> io_lib:format("~p",[A]) end, Args)), ")"]. + ["(", lists:join(", ", lists:map(fun luerl_lib:format_value/1, Args)), ")"]. coerce(X) -> X. From ebdb6373c59b848ef6655ae4f9a8d32b13618d70 Mon Sep 17 00:00:00 2001 From: selenil Date: Wed, 7 Jan 2026 15:28:34 -0500 Subject: [PATCH 19/19] fix badarg exception type --- src/glua.gleam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/glua.gleam b/src/glua.gleam index b403762..c475e1a 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -53,7 +53,7 @@ pub type LuaRuntimeExceptionKind { /// The exception that happens when an invalid arithmetic operation is performed. BadArith(operator: String, args: List(String)) /// The exception that happens when a function is called with incorrect arguments. - Badarg(function: String, args: List(String)) + Badarg(function: String, args: List(dynamic.Dynamic)) /// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument. AssertError(message: String) /// An exception that could not be identified