Skip to content

Commit 6f736a0

Browse files
authored
Improve error handling (#9)
* add FileNotFound error * remove LuaCompilerException and add LuaCompileFailure * format decode errors * improve unknown error * improve runtime exceptions handling * minor renaming * add UndefinedMethod exception * add format_error test * add format_error examples * implement format_stacktrace * fix crashes when assert and error are called with incorrect arguments * fix tests * improve code * add badarg exception * more improvements * fix badarg exception type
1 parent e544f81 commit 6f736a0

File tree

3 files changed

+410
-54
lines changed

3 files changed

+410
-54
lines changed

src/glua.gleam

Lines changed: 196 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import gleam/dynamic
66
import gleam/dynamic/decode
7+
import gleam/int
78
import gleam/list
9+
import gleam/option
810
import gleam/result
911
import gleam/string
1012

@@ -13,34 +15,210 @@ pub type Lua
1315

1416
/// Represents the errors than can happend during the parsing and execution of Lua code
1517
pub type LuaError {
16-
/// There was an exception when compiling the Lua code.
17-
LuaCompilerException(messages: List(String))
18+
/// The compilation process of the Lua code failed because of the presence of one or more compile errors.
19+
LuaCompileFailure(errors: List(LuaCompileError))
1820
/// The Lua environment threw an exception during code execution.
1921
LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua)
2022
/// A certain key was not found in the Lua environment.
21-
KeyNotFound
23+
KeyNotFound(key: List(String))
24+
/// A Lua source file was not found
25+
FileNotFound(path: String)
2226
/// The value returned by the Lua environment could not be decoded using the provided decoder.
2327
UnexpectedResultType(List(decode.DecodeError))
2428
/// An error that could not be identified.
25-
UnknownError
29+
UnknownError(error: dynamic.Dynamic)
30+
}
31+
32+
/// Represents a Lua compilation error
33+
pub type LuaCompileError {
34+
LuaCompileError(line: Int, kind: LuaCompileErrorKind, message: String)
35+
}
36+
37+
/// Represents the kind of a Lua compilation error
38+
pub type LuaCompileErrorKind {
39+
Parse
40+
Tokenize
2641
}
2742

2843
/// Represents the kind of exceptions that can happen at runtime during Lua code execution.
2944
pub type LuaRuntimeExceptionKind {
3045
/// The exception that happens when trying to access an index that does not exists on a table (also happens when indexing non-table values).
31-
IllegalIndex(value: String, index: String)
46+
IllegalIndex(index: String, value: String)
3247
/// The exception that happens when the `error` function is called.
33-
ErrorCall(messages: List(String))
48+
ErrorCall(message: String, level: option.Option(Int))
3449
/// The exception that happens when trying to call a function that is not defined.
3550
UndefinedFunction(value: String)
51+
/// The exception that happens when trying to call a method that is not defined for an object.
52+
UndefinedMethod(object: String, method: String)
3653
/// The exception that happens when an invalid arithmetic operation is performed.
3754
BadArith(operator: String, args: List(String))
55+
/// The exception that happens when a function is called with incorrect arguments.
56+
Badarg(function: String, args: List(dynamic.Dynamic))
3857
/// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument.
3958
AssertError(message: String)
4059
/// An exception that could not be identified
4160
UnknownException
4261
}
4362

63+
/// Turns a `glua.LuaError` value into a human-readable string
64+
///
65+
/// ## Examples
66+
///
67+
/// ```gleam
68+
/// let assert Error(e) = glua.eval(
69+
/// state: glua.new(),
70+
/// code: "if true end",
71+
/// using: decode.string
72+
/// )
73+
///
74+
/// glua.format_error(e)
75+
/// // -> "Lua compile error: \n\nFailed to parse: error on line 1: syntax error before: 'end'"
76+
/// ```
77+
///
78+
/// ```gleam
79+
/// let assert Error(e) = glua.eval(
80+
/// state: glua.new(),
81+
/// code: "local a = 1; local b = true; return a + b",
82+
/// using: decode.string
83+
/// )
84+
///
85+
/// glua.format_error(e)
86+
/// // -> "Lua runtime exception: Bad arithmetic expression: 1 + true"
87+
/// ```
88+
///
89+
/// ```gleam
90+
/// let assert Error(e) = glua.get(
91+
/// state: glua.new(),
92+
/// keys: ["a_value"],
93+
/// using: decode.string
94+
/// )
95+
///
96+
/// glua.format_error(e)
97+
/// // -> "Key \"a_value\" not found"
98+
/// ```
99+
///
100+
/// ```gleam
101+
/// let assert Error(e) = glua.eval_file(
102+
/// state: glua.new(),
103+
/// path: "my_lua_file.lua",
104+
/// using: decode.string
105+
/// )
106+
///
107+
/// glua.format_error(e)
108+
/// // -> "Lua source file \"my_lua_file.lua\" not found"
109+
/// ```
110+
///
111+
/// ```gleam
112+
/// let assert Error(e) = glua.eval(
113+
/// state: glua.new(),
114+
/// code: "return 1 + 1",
115+
/// using: decode.string
116+
/// )
117+
///
118+
/// glua.format_error(e)
119+
/// // -> "Expected String, but found Int"
120+
/// ```
121+
pub fn format_error(error: LuaError) -> String {
122+
case error {
123+
LuaCompileFailure(errors) ->
124+
"Lua compile error: "
125+
<> "\n\n"
126+
<> string.join(list.map(errors, format_compile_error), with: "\n")
127+
LuaRuntimeException(exception, state) -> {
128+
let base = "Lua runtime exception: " <> format_exception(exception)
129+
let stacktrace = get_stacktrace(state)
130+
131+
case stacktrace {
132+
"" -> base
133+
stacktrace -> base <> "\n\n" <> stacktrace
134+
}
135+
}
136+
KeyNotFound(path) ->
137+
"Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found"
138+
FileNotFound(path) ->
139+
"Lua source file " <> "\"" <> path <> "\"" <> " not found"
140+
UnexpectedResultType(decode_errors) ->
141+
list.map(decode_errors, format_decode_error) |> string.join(with: "\n")
142+
UnknownError(error) -> "Unknown error: " <> format_unknown_error(error)
143+
}
144+
}
145+
146+
fn format_compile_error(error: LuaCompileError) -> String {
147+
let kind = case error.kind {
148+
Parse -> "parse"
149+
Tokenize -> "tokenize"
150+
}
151+
152+
"Failed to "
153+
<> kind
154+
<> ": error on line "
155+
<> int.to_string(error.line)
156+
<> ": "
157+
<> error.message
158+
}
159+
160+
fn format_exception(exception: LuaRuntimeExceptionKind) -> String {
161+
case exception {
162+
IllegalIndex(index, value) ->
163+
"Invalid index "
164+
<> "\""
165+
<> index
166+
<> "\""
167+
<> " at object "
168+
<> "\""
169+
<> value
170+
<> "\""
171+
ErrorCall(msg, level) -> {
172+
let base = "Error call: " <> msg
173+
174+
case level {
175+
option.Some(level) -> base <> " at level " <> int.to_string(level)
176+
option.None -> base
177+
}
178+
}
179+
180+
UndefinedFunction(fun) -> "Undefined function: " <> fun
181+
UndefinedMethod(obj, method) ->
182+
"Undefined method "
183+
<> "\""
184+
<> method
185+
<> "\""
186+
<> " for object: "
187+
<> "\""
188+
<> obj
189+
<> "\""
190+
BadArith(operator, args) ->
191+
"Bad arithmetic expression: "
192+
<> string.join(args, with: " " <> operator <> " ")
193+
194+
Badarg(function, args) ->
195+
"Bad argument "
196+
<> string.join(list.map(args, format_lua_value), with: ", ")
197+
<> " for function "
198+
<> function
199+
AssertError(msg) -> "Assertion failed with message: " <> msg
200+
UnknownException -> "Unknown exception"
201+
}
202+
}
203+
204+
@external(erlang, "glua_ffi", "get_stacktrace")
205+
fn get_stacktrace(state: Lua) -> String
206+
207+
fn format_decode_error(error: decode.DecodeError) -> String {
208+
let base = "Expected " <> error.expected <> ", but found " <> error.found
209+
210+
case error.path {
211+
[] -> base
212+
path -> base <> " at " <> string.join(path, with: ".")
213+
}
214+
}
215+
216+
@external(erlang, "luerl_lib", "format_value")
217+
fn format_lua_value(v: anything) -> String
218+
219+
@external(erlang, "luerl_lib", "format_error")
220+
fn format_unknown_error(error: dynamic.Dynamic) -> String
221+
44222
/// The exception that happens when a functi
45223
/// Represents a chunk of Lua code that is already loaded into the Lua VM
46224
pub type Chunk
@@ -235,7 +413,7 @@ fn sandbox_fun(msg: String) -> Value
235413
///
236414
/// ```gleam
237415
/// glua.get(state: glua.new(), keys: ["non_existent"], using: decode.string)
238-
/// // -> Error(glua.KeyNotFound)
416+
/// // -> Error(glua.KeyNotFound(["non_existent"]))
239417
/// ```
240418
pub fn get(
241419
state lua: Lua,
@@ -332,7 +510,7 @@ pub fn set(
332510
case do_ref_get(lua, keys) {
333511
Ok(_) -> Ok(#(keys, lua))
334512

335-
Error(KeyNotFound) -> {
513+
Error(KeyNotFound(_)) -> {
336514
let #(tbl, lua) = alloc_table([], lua)
337515
do_set(lua, keys, tbl)
338516
|> result.map(fn(lua) { #(keys, lua) })
@@ -428,7 +606,7 @@ fn do_set_private(key: String, value: a, lua: Lua) -> Lua
428606
///
429607
/// assert glua.delete_private(lua, "my_value")
430608
/// |> glua.get("my_value", decode.string)
431-
/// == Error(glua.KeyNotFound)
609+
/// == Error(glua.KeyNotFound(["my_value"]))
432610
/// ```
433611
pub fn delete_private(state lua: Lua, key key: String) -> Lua {
434612
do_delete_private(key, lua)
@@ -604,6 +782,15 @@ fn do_ref_eval_chunk(
604782
///
605783
/// assert results == ["hello, world!"]
606784
/// ```
785+
///
786+
/// ```gleam
787+
/// glua.eval_file(
788+
/// state: glua.new(),
789+
/// path: "path/to/non/existent/file",
790+
/// using: decode.string
791+
/// )
792+
/// //-> Error(glua.FileNotFound(["path/to/non/existent/file"]))
793+
/// ```
607794
pub fn eval_file(
608795
state lua: Lua,
609796
path path: String,

0 commit comments

Comments
 (0)