diff --git a/src/glua.gleam b/src/glua.gleam index cc5ae51..c475e1a 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -4,7 +4,9 @@ import gleam/dynamic import gleam/dynamic/decode +import gleam/int import gleam/list +import gleam/option import gleam/result import gleam/string @@ -13,34 +15,210 @@ 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. - KeyNotFound + KeyNotFound(key: List(String)) + /// A Lua source file was not found + 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. - UnknownError + UnknownError(error: dynamic.Dynamic) +} + +/// 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). - 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 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 function is called with incorrect arguments. + 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 UnknownException } +/// Turns a `glua.LuaError` value into a human-readable string +/// +/// ## 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) -> + "Lua compile error: " + <> "\n\n" + <> string.join(list.map(errors, format_compile_error), with: "\n") + LuaRuntimeException(exception, state) -> { + let base = "Lua runtime exception: " <> format_exception(exception) + let stacktrace = get_stacktrace(state) + + case stacktrace { + "" -> base + stacktrace -> base <> "\n\n" <> stacktrace + } + } + KeyNotFound(path) -> + "Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found" + FileNotFound(path) -> + "Lua source file " <> "\"" <> path <> "\"" <> " not found" + UnexpectedResultType(decode_errors) -> + list.map(decode_errors, format_decode_error) |> string.join(with: "\n") + UnknownError(error) -> "Unknown error: " <> format_unknown_error(error) + } +} + +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(exception: LuaRuntimeExceptionKind) -> String { + case exception { + IllegalIndex(index, value) -> + "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 + UndefinedMethod(obj, method) -> + "Undefined method " + <> "\"" + <> method + <> "\"" + <> " for object: " + <> "\"" + <> obj + <> "\"" + BadArith(operator, args) -> + "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" + } +} + +@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 + + case error.path { + [] -> base + path -> base <> " at " <> string.join(path, with: ".") + } +} + +@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 + /// The exception that happens when a functi /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk @@ -235,7 +413,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, @@ -332,7 +510,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) }) @@ -428,7 +606,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) @@ -604,6 +782,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 1edf9e7..497a1b9 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([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]). @@ -28,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 @@ -58,20 +57,29 @@ 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({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}) -> - {lua_runtime_exception, Error, State}; +map_error({error, Errors, _}) -> + {lua_compile_failure, lists:map(fun map_compile_error/1, Errors)}; +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}) -> + 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, 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, {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 = @@ -81,12 +89,100 @@ 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, {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(_) -> - unknown_error. +map_error(Error) -> + {unknown_error, 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)}. + + +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, [_ | 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, + io_lib:format("~p~s", [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 luerl_lib:format_value/1, Args)), ")"]. coerce(X) -> X. @@ -110,7 +206,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 -> @@ -120,7 +216,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 -> @@ -139,8 +235,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( @@ -183,5 +282,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..a7cce4e 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" } @@ -211,14 +210,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 +340,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 +351,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() { @@ -371,6 +370,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() { @@ -392,10 +395,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( @@ -411,11 +424,23 @@ 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.eval(state:, code: "error({1})", using: decode.int) let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), @@ -423,6 +448,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: "local i = function(x) return x end; i:call(1)", + using: decode.string, + ) + + assert method == "call" + let assert Error(glua.LuaRuntimeException( exception: glua.BadArith(operator:, args:), state: _, @@ -442,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() { @@ -551,3 +592,32 @@ 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: "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.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" +}