Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 147 additions & 9 deletions src/glua.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -13,26 +15,41 @@ 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 call to assert is made passing a value that evalues to `false` as the first argument.
Expand All @@ -41,6 +58,118 @@ pub type LuaRuntimeExceptionKind {
UnknownException
}

/// Turns a `glua.LuaError` value into a human-readable string
///
/// ## Examples
///
/// ```gleam
/// ```
///
/// ```gleam
/// ```
///
/// ```gleam
/// ```
///
/// ```gleam
/// ```
///
/// ```gleam
/// ```
///
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 = format_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 <> " ")

AssertError(msg) -> "Assertion failed with message: " <> msg
UnknownException -> "Unknown exception"
}
}

@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: ".")
}
}

@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
Expand Down Expand Up @@ -235,7 +364,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,
Expand Down Expand Up @@ -332,7 +461,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) })
Expand Down Expand Up @@ -428,7 +557,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)
Expand Down Expand Up @@ -604,6 +733,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,
Expand Down
63 changes: 42 additions & 21 deletions src/glua_ffi.erl
Original file line number Diff line number Diff line change
@@ -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]).

Expand All @@ -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
Expand Down Expand Up @@ -58,20 +57,24 @@ 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}) ->
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}) ->
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, {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 =
Expand All @@ -85,8 +88,23 @@ 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});
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),
<<""/utf8>>.

coerce(X) ->
X.
Expand All @@ -110,7 +128,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 ->
Expand All @@ -120,7 +138,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 ->
Expand All @@ -139,8 +157,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(
Expand Down Expand Up @@ -183,5 +204,5 @@ get_private(Lua, Key) ->
{ok, luerl:get_private(Key, Lua)}
catch
error:{badkey, _} ->
{error, key_not_found}
{error, {key_not_found, [Key]}}
end.
Loading
Loading