diff --git a/src/glua.gleam b/src/glua.gleam index 8cd78d0..8872ed8 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -2,6 +2,7 @@ //// //// Gleam wrapper around [Luerl](https://github.com/rvirding/luerl). +import gleam/bool import gleam/dynamic import gleam/dynamic/decode import gleam/int @@ -15,7 +16,7 @@ import gleam/string pub type Lua /// Represents the errors than can happend during the parsing and execution of Lua code -pub type LuaError { +pub type LuaError(error) { /// The compilation process of the Lua code failed because of the presence of one or more compile errors. LuaCompileFailure(errors: List(LuaCompileError)) /// The Lua environment threw an exception during code execution. @@ -26,6 +27,8 @@ pub type LuaError { FileNotFound(path: String) /// The value returned by the Lua environment could not be decoded using the provided decoder. UnexpectedResultType(List(decode.DecodeError)) + /// An app-defined error + CustomError(error: error) /// An error that could not be identified. UnknownError(error: dynamic.Dynamic) } @@ -66,56 +69,55 @@ pub type LuaRuntimeExceptionKind { /// ## Examples /// /// ```gleam -/// let assert Error(e) = glua.eval( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.eval( /// code: "if true end", -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Lua compile error: \n\nFailed to parse: error on line 1: syntax error before: 'end'" /// ``` /// /// ```gleam -/// let assert Error(e) = glua.eval( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.eval( /// code: "local a = 1; local b = true; return a + b", -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Lua runtime exception: Bad arithmetic expression: 1 + true" /// ``` /// /// ```gleam -/// let assert Error(e) = glua.get( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.get( /// keys: ["a_value"], -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Key \"a_value\" not found" /// ``` /// /// ```gleam -/// let assert Error(e) = glua.eval_file( -/// state: glua.new(), +/// let assert Error(e) = glua.run(glua.new(), glua.eval_file( /// path: "my_lua_file.lua", -/// ) +/// )) /// /// glua.format_error(e) /// // -> "Lua source file \"my_lua_file.lua\" not found" /// ``` /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state: glua.new(), -/// code: "return 1 + 1", -/// ) -/// let assert Error(e) = glua.dereference(state:, ref:, using: decode.string) +/// let assert Error(e) = glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval( +/// code: "return 1 + 1", +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// }) /// /// glua.format_error(e) /// // -> "Expected String, but found Int" /// ``` -pub fn format_error(error: LuaError) -> String { +pub fn format_error(error: LuaError(e)) -> String { case error { LuaCompileFailure(errors) -> "Lua compile error: " @@ -136,6 +138,7 @@ pub fn format_error(error: LuaError) -> String { "Lua source file " <> "\"" <> path <> "\"" <> " not found" UnexpectedResultType(decode_errors) -> list.map(decode_errors, format_decode_error) |> string.join(with: "\n") + CustomError(error) -> string.inspect(error) UnknownError(error) -> "Unknown error: " <> format_unknown_error(error) } } @@ -216,6 +219,279 @@ fn format_lua_value(v: anything) -> String @external(erlang, "luerl_lib", "format_error") fn format_unknown_error(error: dynamic.Dynamic) -> String +/// Represents an action that can be run within a Lua state and potentially mutates that state. +/// +/// `Action`s are how we interact with a Lua VM and thus many functions in this library +/// returns an `Action` when invoked. It is important to note that the execution of any `Action` is defered until +/// you pass it to `glua.run`: +/// +/// ```gleam +/// // no Lua code has been evaluated or even parsed, +/// // we're just creating an `Action` +/// let action = glua.eval("return 1") +/// +/// glua.run(glua.new(), action) // now our Lua code is evaluated +/// ``` +/// +/// `Action`s *can* fail and *can* mutate the Lua state. When calling multiple `Action`s in sequence, +/// you need to make sure each one is executed within the Lua state returned by the previous one since +/// executing an `Action` using outdated state could lead to unexpected behaviour. +/// +/// ```gleam +/// let state = glua.new() +/// let result = { +/// use #(new_state, _) <- result.try( +/// glua.run(state, glua.set(keys: ["a_number"], value: glua.int(36))) +/// ) +/// use #(new_state, ret) <- result.try( +/// glua.run(state, glua.eval("return math.sqrt(a_number)")) +/// ) +/// +/// // we know that `math.sqrt` only returns one value +/// let assert [ref] = ret +/// +/// glua.run(new_state, glua.dereference(ref:, using: decode.float)) +/// |> result.map(pair.second) +/// } +/// result +/// // -> Ok(6.0) +/// ``` +/// +/// However, `glua` provides function to compose `Actions`s toghether without having to pass +/// the state explicitly. The most common of such functions is `glua.then`, which allows us to take +/// an existing `Action` and use its return value to construct another `Action`. +/// `glua.then` will automatically pass the state returned by the first action to the second one +/// and it will halt the chain as soon as any `Action` fails (like `result.try`). +/// This is equivalent to the above example: +/// +/// ```gleam +/// let state = glua.new() +/// let action = { +/// use _ <- glua.then(glua.set(keys: ["a_number"], value: glua.int(36))) +/// use ret <- glua.then(glua.eval(code: "return math.sqrt(a_number)")) +/// +/// // we know that `math.sqrt` only returns one value +/// let assert [ref] = ret +/// glua.dereference(ref:, using: decode.float) +/// } +/// +/// glua.run(state, action) |> result.map(pair.second) +/// // -> Ok(6.0) +/// ``` +/// +/// An `Action` takes two types parameters, `return` is the type of the value that the `Action` +/// would return in case it succeeds, and `error` is the type of custom errors that +/// the `Action` could return. +pub opaque type Action(return, error) { + Action(function: fn(Lua) -> Result(#(Lua, return), LuaError(error))) +} + +/// Runs an `Action` within a Lua environment. +/// +/// ## Examples +/// +/// ```gleam +/// let state = glua.new() +/// glua.run(state, { +/// use ret <- glua.then(glua.eval("return 'Hello from Lua!'")) +/// use ref <- glua.try(list.first(ret)) +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "Hello from Lua!")) +/// ``` +pub fn run( + state lua: Lua, + action action: Action(return, error), +) -> Result(#(Lua, return), LuaError(error)) { + action.function(lua) +} + +/// Composes two `Action`s into a single one, by executing the first one and passing its return value +/// to a function that returns another `Action`. +/// +/// If the first `Action` returns an `Error` when executed, then the function is not called +/// and the error is returned. +/// +/// This function is the most common way to chain together multiple `Action`s. +/// +/// ## Examples +/// +/// ```gleam +/// let my_value = 1 +/// let assert Ok(#(_state, ret)) = glua.run(glua.new(), { +/// use _ <- glua.then(glua.set(keys: ["my_value"], value: glua.int(my_value))) +/// use ref <- glua.then(glua.get(keys: ["my_value"])) +/// glua.dereference(ref:, using: decode.int) +/// }) +/// +/// assert ret == my_value +/// ``` +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval_file(path: "./my_file.lua")) +/// glua.call_function_by_name(path: ["table", "pack"], args: ret) +/// }) +/// // -> Error(glua.FileNotFound("./my_file.lua")) +/// ``` +pub fn then(action: Action(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { + use state <- Action + use #(new, ret) <- result.try(action.function(state)) + + next(ret).function(new) +} + +/// Transforms the provided result into an `Action` by passing its value to a function +/// that yields an `Action`. +/// +/// If the input is an `Error`, then the function is not called and instead a failing `Action` +/// is returned with the original error. +/// +/// This is a shorthand for writing a case with `glua.then`: +/// +/// ```gleam +/// use fun <- glua.then(glua.get(["string", "reverse"])) +/// use return <- glua.then(glua.call_function(fun:, args: [glua.string("Hello")])) +/// use value <- glua.try(list.first(return)) +/// glua.dereference(ref: value, using: decode.string) +/// ``` +/// +/// as opposed to this: +/// +/// ```gleam +/// use fun <- glua.then(glua.get(["string", "reverse"])) +/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")])) +/// case return { +/// [first] -> glua.dereference(ref: first, using: decode.string) +/// _ -> glua.failure(Nil) +/// } +/// ``` +pub fn try(result: Result(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) { + case result { + Ok(ret) -> Action(next(ret).function) + Error(err) -> failure(err) + } +} + +/// Runs a callback function if the given bool is `False`, otherwise return a failing `Action` +/// using the provided value. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "local a = 1")) +/// use <- glua.guard(when: ret == [], return: "expected at least one value from Lua") +/// +/// glua.fold(ret, glua.dereference(_, using: decode.int)) +/// }) +/// // -> Error(glua.CustomError("expected at least one value from Lua")) +/// ``` +pub fn guard( + when requirement: Bool, + return consequence: e, + otherwise alternative: fn() -> Action(a, e), +) -> Action(a, e) { + bool.guard(requirement, failure(consequence), alternative) +} + +/// Creates an `Action` that always succeeds and returns `value`. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run(glua.new(), glua.success("my value")) +/// // -> Ok(#(_state, "my_value")) +/// ``` +pub fn success(value: a) -> Action(a, e) { + use state <- Action + Ok(#(state, value)) +} + +/// Creates an `Action` that always fails with `glua.CustomError(error)`. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run( +/// glua.new(), +/// glua.failure("incorrect number of return values") +/// ) +/// // -> Error(glua.CustomError("incorrect number of return values")) +/// ``` +pub fn failure(error: e) -> Action(a, e) { + use _ <- Action + Error(CustomError(error)) +} + +/// Invokes the Lua `error` function with the provided message. +pub fn error(message: String) -> Action(List(Value), e) { + call_function_by_name(["error"], [string(message)]) +} + +/// Invokes the Lua `error` function with the provided message and level. +pub fn error_with_level(message: String, level: Int) -> Action(List(Value), e) { + call_function_by_name(["error"], [string(message), int(level)]) +} + +/// Transforms the return value of an `Action` with the provided function. +/// +/// If the `Action` returns an `Error` when executed then the function is not called and the +/// error is returned. +/// +/// ## Examples +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use ref <- glua.then(glua.get(keys: ["_VERSION"])) +/// use version <- glua.map(glua.dereference(ref:, using: decode.string)) +/// "glua supports " <> version +/// }) +/// // -> Ok(#(_state, "glua supports Lua 5.3")) +/// ``` +/// +/// ```gleam +/// glua.run(glua.new(), { +/// use n <- glua.map(glua.get(keys: ["my_number"])) +/// n * 2 +/// }) +/// // -> Error(glua.KeyNotFound(["my_number"])) +/// ``` +pub fn map(over action: Action(a, e), with fun: fn(a) -> b) -> Action(b, e) { + use state <- Action + action.function(state) + |> result.map(pair.map_second(_, fun)) +} + +/// Maps a list of elements into a list of `Action`s by calling a function in each element and then flattens +/// all the `Action`s into a single one. +/// +/// ## Examples +/// +/// ```gleam +/// let numbers = [9, 16, 25] +/// let keys = ["math", "sqrt"] +/// glua.run(glua.new(), glua.fold(numbers, fn(n) { +/// use ret <- glua.then(glua.call_function_by_name(keys:, args: [glua.int(n)])) +/// +/// let assert [ref] = ret +/// glua.dereference(ref:, using: decode.float) +/// })) +/// // -> Ok(#(_state, [3.0, 4.0, 5.0])) +/// ``` +pub fn fold( + over list: List(a), + with fun: fn(a) -> Action(b, e), +) -> Action(List(b), e) { + use state <- Action + list.try_fold(list, #(state, []), fn(acc, e) { + let #(state, results) = acc + fun(e).function(state) + |> result.map(pair.map_second(_, fn(ret) { [ret, ..results] })) + }) + |> result.map(pair.map_second(_, list.reverse)) +} + /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk @@ -237,8 +513,9 @@ pub fn int(v: Int) -> Value @external(erlang, "glua_ffi", "coerce") pub fn float(v: Float) -> Value -pub fn table(lua: Lua, values: List(#(Value, Value))) -> #(Lua, Value) { - do_table(values, lua) |> pair.swap +pub fn table(values: List(#(Value, Value))) -> Action(Value, e) { + use state <- Action + Ok(do_table(values, state) |> pair.swap) } @external(erlang, "luerl_heap", "alloc_table") @@ -257,12 +534,28 @@ pub fn table_decoder( decode.list(of: inner) } -pub fn function( - f: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value { +/// Encodes a Gleam function into a Lua function. +/// +/// > **Note**: The function to be encoded has to return an `Action` with a `Never` type +/// > as the `error` parameter, meaning that the function cannot invoke `glua.failure` in its body. +/// > If you want to return an error inside that function, you should use `glua.error` or `glua.error_with_code`, +/// > both of which will call the Lua `error` function. +pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value { do_function(f) } +// Taken from hexdocs.pm/funtil/1.1.0/funtil.html#Never +/// This type is used to represent a value that can never happen. What does that +/// mean exactly? +/// +/// - A `Bool` is a type that has two values: `True` and `False`. +/// - `Nil` is a type that has one value: `Nil`. +/// - `Never` is a type that has zero values: it's impossible to construct! +/// +/// This library uses this type to make `glua.failure` impossible to construct in `glua.function`s +/// to encourage using `glua.error` instead since `glua.failure` wouldn't make sense in that case. +pub type Never + pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { list.map(values, encoder) } @@ -284,17 +577,19 @@ pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { /// decode.success(User(name:, is_admin:)) /// } /// -/// let state = glua.new() -/// let #(state, userdata) = glua.userdata(state, User(name: "Jhon Doe", is_admin: False)) -/// let assert Ok(state) = glua.set( -/// state:, -/// keys: ["a_user"], -/// value: userdata -/// ) -/// -/// let assert Ok(#(state, [ref])) = glua.eval(state:, code: "return a_user") -/// glua.dereference(state:, ref:, using: user_decoder) -/// // -> Ok(User("Jhon Doe", False)) +/// glua.run(glua.new(), { +/// use userdata <- glua.then(userdata(User("Jhon Doe", False))) +/// use _ <- glua.then(glua.set( +/// keys: ["a_user"], +/// value: userdata +/// )) +/// +/// use ret <- glua.then(glua.eval(code: "return a_user")) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: user_decoder) +/// }) +/// // -> Ok(#(_state, User("Jhon Doe", False))) /// ``` /// /// ```gleam @@ -302,63 +597,88 @@ pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { /// Person(name: String, email: String) /// } /// -/// let state = glua.new() -/// let #(state, userdata) = glua.userdata(state, Person(name: "Lucy", email: "lucy@example.com")) -/// let assert Ok(lua) = glua.set( -/// state:, -/// keys: ["lucy"], -/// value: userdata -/// ) -/// /// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_), _)) = -/// glua.eval(state:, code: "return lucy.email") +/// glua.run(glua.new(), { +/// use userdata <- glua.then(glua.userdata( +/// Person(name: "Lucy", email: "lucy@example.com") +/// )) +/// use _ <- glua.then(glua.set( +/// keys: ["lucy"], +/// value: userdata +/// )) +/// +/// glua.eval(code: "return lucy.email") +/// }) /// ``` -pub fn userdata(lua: Lua, v: anything) -> #(Lua, Value) { - do_userdata(v, lua) |> pair.swap +pub fn userdata(v: anything) -> Action(Value, e) { + use state <- Action + Ok(do_userdata(v, state) |> pair.swap) } @external(erlang, "luerl_heap", "alloc_userdata") fn do_userdata(v: anything, lua: Lua) -> #(Value, Lua) @external(erlang, "glua_ffi", "wrap_fun") -fn do_function( - fun: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value +fn do_function(fun: fn(List(Value)) -> Action(List(Value), e)) -> Value /// Converts a reference to a Lua value into type-safe Gleam data using the provided decoder. /// /// ## Examples /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state: glua.new(), -/// code: "return 'Hello from Lua!'" -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok("Hello from Lua!") +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "return 'Hello from Lua!'")) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// } +/// // -> Ok(#(_state, "Hello from Lua!")) /// ``` /// /// ```gleam -/// let assert Ok(#(state, [ref1, ref2])) = glua.eval( -/// state: glua.new(), -/// code: "return 1, true" +/// let assert Ok(#(state, [ref1, ref2])) = glua.run( +/// glua.new(), +/// glua.eval(code: "return 1, true") /// ) -/// assert glua.dereference(state:, ref: ref1, using: decode.int) == Ok(1) -/// assert glua.dereference(state:, ref: ref2, using: decode.bool) == Ok(True) +/// +/// let assert Ok(#(_state, 1)) = +/// glua.run(state, glua.dereference(ref: ref1, using: decode.int)) +/// let assert Ok(#(_state, True)) = +/// glua.run(state, glua.dereference(ref: ref2, using: decode.bool)) /// ``` pub fn dereference( - state lua: Lua, ref ref: Value, using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { - do_dereference(lua, ref) - |> decode.run(decoder) - |> result.map_error(UnexpectedResultType) +) -> Action(a, e) { + use state <- Action + use ret <- result.map( + do_dereference(state, ref) + |> decode.run(decoder) + |> result.map_error(UnexpectedResultType), + ) + + #(state, ret) } @external(erlang, "glua_ffi", "dereference") fn do_dereference(lua: Lua, ref: Value) -> dynamic.Dynamic +pub fn returning( + action act: Action(Value, e), + using decoder: decode.Decoder(a), +) -> Action(a, e) { + use ref <- then(act) + dereference(ref, decoder) +} + +pub fn returning_list( + action act: Action(List(Value), e), + using decoder: decode.Decoder(a), +) -> Action(List(a), e) { + use refs <- then(act) + fold(refs, dereference(_, decoder)) +} + /// Creates a new Lua VM instance @external(erlang, "luerl", "init") pub fn new() -> Lua @@ -390,7 +710,7 @@ pub const default_sandbox = [ /// In case you want to sandbox more Lua values, pass to `glua.sandbox` the returned Lua state. pub fn new_sandboxed( allow excluded: List(List(String)), -) -> Result(Lua, LuaError) { +) -> Result(Lua, LuaError(e)) { list_substraction(default_sandbox, excluded) |> list.try_fold(from: new(), with: sandbox) } @@ -403,17 +723,23 @@ fn list_substraction(a: List(a), b: List(a)) -> List(a) /// ## Examples /// /// ```gleam -/// let assert Ok(lua) = glua.new() |> glua.sandbox(["os"], ["execute"]) -/// let assert Error(glua.LuaRuntimeException(exception, _)) = glua.eval( -/// state: lua, -/// code: "os.execute(\"rm -f important_file\"); return 0", -/// ) +/// let assert Ok(state) = glua.new() |> glua.sandbox(["os"], ["execute"]) +/// let assert Error(glua.LuaRuntimeException(exception, _)) = +/// glua.run(state, glua.eval( +/// code: "os.execute(\"rm -f important_file\"); return 0", +/// )) +/// /// // 'important_file' was not deleted /// assert exception == glua.ErrorCall(["os.execute is sandboxed"]) /// ``` -pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) { +pub fn sandbox( + state lua: Lua, + keys keys: List(String), +) -> Result(Lua, LuaError(e)) { let msg = string.join(keys, with: ".") <> " is sandboxed" - set(lua, ["_G", ..keys], sandbox_fun(msg)) + + set(["_G", ..keys], sandbox_fun(msg)).function(lua) + |> result.map(pair.first) } @external(erlang, "glua_ffi", "sandbox_fun") @@ -424,30 +750,38 @@ fn sandbox_fun(msg: String) -> Value /// ## Examples /// /// ```gleam -/// let state = glua.new() -/// let assert Ok(ref) = glua.get(state:, keys: ["_VERSION"]) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok("Lua 5.3") +/// glua.run(glua.new(), { +/// use ref <- glua.then(glua.get(keys: ["_VERSION"])) +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "Lua 5.3")) /// ``` /// /// ```gleam -/// let assert Ok(state) = glua.set( -/// state: glua.new(), -/// keys: ["my_table", "my_value"], -/// value: glua.bool(True) -/// ) -/// -/// let assert Ok(ref) = glua.get(state:, keys: ["my_table", "my_value"]) -/// glua.dereference(state:, ref:, using: decode.bool) -/// // -> Ok(True) +/// glua.run(glua.new(), { +/// use _ <- glua.then(glua.set( +/// keys: ["my_table", "my_value"], +/// value: glua.bool(True) +/// )) +/// use ref <- glua.then(glua.get(keys: ["my_table", "my_value"])) +/// +/// glua.dereference(ref:, using: decode.bool) +/// }) +/// // -> Ok(#(_state, True)) /// ``` /// /// ```gleam -/// glua.get(state: glua.new(), keys: ["non_existent"]) +/// glua.run(glua.new(), glua.get(keys: ["non_existent"])) /// // -> Error(glua.KeyNotFound(["non_existent"])) /// ``` +pub fn get(keys keys: List(String)) -> Action(Value, e) { + use state <- Action + use ret <- result.map(do_get(state, keys)) + #(state, ret) +} + @external(erlang, "glua_ffi", "get_table_keys") -pub fn get(state lua: Lua, keys keys: List(String)) -> Result(Value, LuaError) +fn do_get(lua: Lua, keys: List(String)) -> Result(Value, LuaError(e)) /// Gets a private value that is not exposed to the Lua runtime. /// @@ -463,13 +797,13 @@ pub fn get_private( state lua: Lua, key key: String, using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { +) -> Result(a, LuaError(e)) { use value <- result.try(do_get_private(lua, key)) decode.run(value, decoder) |> result.map_error(UnexpectedResultType) } @external(erlang, "glua_ffi", "get_private") -fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) +fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError(e)) /// Sets a value in the Lua environment. /// @@ -481,60 +815,57 @@ fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) /// ## Examples /// /// ```gleam -/// let assert Ok(state) = glua.set( -/// state: glua.new(), -/// keys: ["my_number"], -/// value: glua.int(10) -/// ) -/// -/// let assert Ok(ref) = glua.get(state: lua, keys: ["my_number"]) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(10) +/// glua.run(glua.new(), { +/// use _ <- glua.then(glua.set( +/// keys: ["my_number"], +/// value: glua.int(10) +/// )) +/// use ref <- glua.get(keys: ["my_number"]) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 10)) /// ``` /// /// ```gleam /// let emails = ["jhondoe@example.com", "lucy@example.com"] -/// let #(state, encoded) = glua.table( -/// glua.new(), -/// list.index_map(emails, fn(email, i) { #(glua.int(i + 1), glua.string(email)) }) -/// ) -/// let assert Ok(state) = glua.set( -/// state:, -/// keys: ["info", "emails"], -/// value: encoded -/// ) +/// let assert Ok(#(_state, results)) = glua.run(glua.new(), { +/// use encoded <- glua.then(glua.table( +/// list.index_map(emails, fn(email, i) { #(glua.int(i + 1), glua.string(email)) }) +/// )) +/// use _ <- glua.then(glua.set(["info", "emails"], encoded)) +/// +/// use ret <- glua.then(glua.eval(code: "return info.emails")) +/// use ref <- glua.try(list.first(ret)) /// -/// let assert Ok(#(state, [ref])) = glua.eval(state:, code: "return info.emails") -/// let assert Ok(results) = -/// glua.dereference(state:, ref:, using: decode.dict(decode.int, decode.string)) -/// |> result.map(dict.values) +/// glua.dereference(ref:, using: decode.dict(decode.int, decode.string)) +/// |> glua.map(dict.values) +/// }) /// /// assert results == emails /// ``` -pub fn set( - state lua: Lua, - keys keys: List(String), - value val: Value, -) -> Result(Lua, LuaError) { - let state = { - use acc, key <- list.try_fold(keys, #([], lua)) - let #(keys, lua) = acc - let keys = list.append(keys, [key]) - case get(lua, keys) { - Ok(_) -> Ok(#(keys, lua)) - - Error(KeyNotFound(_)) -> { - let #(tbl, lua) = do_table([], lua) - do_set(lua, keys, tbl) - |> result.map(fn(lua) { #(keys, lua) }) +pub fn set(keys keys: List(String), value val: Value) -> Action(Nil, e) { + use state <- Action + use #(new, keys) <- result.try( + list.try_fold(keys, #(state, []), fn(acc, key) { + let #(state, keys) = acc + let keys = list.append(keys, [key]) + case do_get(state, keys) { + Ok(_) -> Ok(#(state, keys)) + + Error(KeyNotFound(_)) -> { + let #(tbl, new) = do_table([], state) + use new <- result.map(do_set(new, keys, tbl)) + #(new, keys) + } + + Error(e) -> Error(e) } + }), + ) - Error(e) -> Error(e) - } - } - - use #(keys, lua) <- result.try(state) - do_set(lua, keys, val) + do_set(new, keys, val) + |> result.map(fn(state) { #(state, Nil) }) } /// Sets a value that is not exposed to the Lua runtime and can only be accessed from Gleam. @@ -552,12 +883,14 @@ pub fn set_private(state lua: Lua, key key: String, value value: a) -> Lua { /// Sets a group of values under a particular table in the Lua environment. pub fn set_api( - lua: Lua, keys: List(String), values: List(#(String, Value)), -) -> Result(Lua, LuaError) { - use state, #(key, val) <- list.try_fold(values, lua) - set(state, list.append(keys, [key]), val) +) -> Action(Nil, e) { + use _ <- then( + fold(values, fn(pair) { set(list.append(keys, [pair.0]), pair.1) }), + ) + + success(Nil) } /// Sets the paths where the Lua runtime will look when requiring other Lua files. @@ -570,28 +903,24 @@ pub fn set_api( /// /// ```gleam /// let my_scripts_paths = ["app/scripts/lua/?.lua"] -/// let assert Ok(state) = glua.set_lua_paths( -/// state: glua.new(), -/// paths: my_scripts_paths -/// ) -/// -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state:, -/// code: "local my_math = require 'my_script'; return my_math.square(3)" -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(9) +/// glua.run(glua.new(), { +/// use _ <- glua.then(glua.set_lua_paths(paths: my_scripts_paths)) +/// use ret <- glua.then(glua.eval( +/// code: "local my_math = require 'my_script'; return my_math.square(3)" +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 9)) /// ``` -pub fn set_lua_paths( - state lua: Lua, - paths paths: List(String), -) -> Result(Lua, LuaError) { +pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil, e) { let paths = string.join(paths, with: ";") |> string - set(lua, ["package", "path"], paths) + set(["package", "path"], paths) } @external(erlang, "glua_ffi", "set_table_keys") -fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) +fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError(e)) @external(erlang, "luerl", "put_private") fn do_set_private(key: String, value: a, lua: Lua) -> Lua @@ -618,47 +947,52 @@ fn do_delete_private(key: String, lua: Lua) -> Lua /// Parses a string of Lua code and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. +pub fn load(code code: String) -> Action(Chunk, e) { + Action(do_load(_, code)) +} + @external(erlang, "glua_ffi", "load") -pub fn load( - state lua: Lua, - code code: String, -) -> Result(#(Lua, Chunk), LuaError) +fn do_load(lua: Lua, code: String) -> Result(#(Lua, Chunk), LuaError(e)) /// Parses a Lua source file and returns it as a compiled chunk. /// /// To eval the returned chunk, use `glua.eval_chunk`. +pub fn load_file(path path: String) -> Action(Chunk, e) { + Action(do_load_file(_, path)) +} + @external(erlang, "glua_ffi", "load_file") -pub fn load_file( - state lua: Lua, - path path: String, -) -> Result(#(Lua, Chunk), LuaError) +fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError(e)) /// Evaluates a string of Lua code. /// /// ## Examples /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval( -/// state: glua.new(), -/// code: "return 1 + 2", -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(3) +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "return 1 + 2")) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 3)) /// ``` /// /// ```gleam -/// let assert Ok(#(state, [ref1, ref2])) = glua.eval( -/// state: glua.new(), +/// let assert Ok(#(state, [ref1, ref2])) = glua.run(glua.new(), glua.eval( /// code: "return 'hello, world!', 10", -/// ) -/// assert glua.dereference(state:, ref: ref1, using: decode.string) == "hello, world!" -/// assert glua.dereference(state:, ref: ref2, using: decode.int) == 10 +/// )) +/// +/// let assert Ok(#(_state, "hello world")) = +/// glua.run(state, glua.dereference(ref: ref1, using: decode.string)) +/// let assert Ok(#(_state, 10)) = +/// glua.run(state, glua.dereference(ref: ref2, using: decode.int)) /// ``` /// /// ```gleam -/// glua.eval(state: glua.new(), code: "return 1 * ") -/// // -> Error(glua.LuaCompilerException( -/// messages: ["syntax error before: ", "1"] +/// glua.run(glua.new(), glua.eval(code: "return 1 * ")) +/// // -> Error(glua.LuaCompileFailure( +/// [glua.LuaCompileError(1, Parse, "syntax error before: ")] /// )) /// ``` /// @@ -666,71 +1000,89 @@ pub fn load_file( /// > instead of calling `glua.eval` repeatly it is recommended to first convert /// > the code to a chunk by passing it to `glua.load`, and then /// > evaluate that chunk using `glua.eval_chunk`. +pub fn eval(code code: String) -> Action(List(Value), e) { + Action(do_eval(_, code)) +} + @external(erlang, "glua_ffi", "eval") -pub fn eval( - state lua: Lua, - code code: String, -) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError(e)) /// Evaluates a compiled chunk of Lua code. /// /// ## Examples +/// /// ```gleam -/// let assert Ok(#(lua, chunk)) = glua.load( -/// state: glua.new(), -/// code: "return 'hello, world!'" -/// ) +/// glua.run(glua.new(), { +/// use chunk <- glua.then(glua.load( +/// code: "return 'hello, world!'" +/// )) +/// +/// use ret <- glua.then(glua.eval_chunk(chunk:)) +/// use ref <- glua.try(list.first(ret)) /// -/// let assert Ok(#(state, [ref])) = glua.eval_chunk( -/// state: lua, -/// chunk:, -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok("hello, world!") +/// glua.dereference(ref:, using: decode.string) +/// // -> Ok(#(_state, "hello, world!")) /// ``` +pub fn eval_chunk(chunk chunk: Chunk) -> Action(List(Value), e) { + Action(do_eval_chunk(_, chunk)) +} + @external(erlang, "glua_ffi", "eval_chunk") -pub fn eval_chunk( - state lua: Lua, - chunk chunk: Chunk, -) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval_chunk( + lua: Lua, + chunk: Chunk, +) -> Result(#(Lua, List(Value)), LuaError(e)) /// Evaluates a Lua source file. /// /// ## Examples +/// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.eval_file( -/// state: glua.new(), -/// path: "path/to/hello.lua", -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// Ok("hello, world!") +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval_file( +/// path: "path/to/hello.lua", +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "hello, world!")) /// ``` /// /// ```gleam -/// glua.eval_file( -/// state: glua.new(), +/// glua.run(glua.new(), glua.eval_file( /// path: "path/to/non/existent/file", -/// ) -/// //-> Error(glua.FileNotFound(["path/to/non/existent/file"])) +/// )) +/// // -> Error(glua.FileNotFound(["path/to/non/existent/file"])) /// ``` +pub fn eval_file(path path: String) -> Action(List(Value), e) { + Action(do_eval_file(_, path)) +} + @external(erlang, "glua_ffi", "eval_file") -pub fn eval_file( - state lua: Lua, - path path: String, -) -> Result(#(Lua, List(Value)), LuaError) +fn do_eval_file( + lua: Lua, + path: String, +) -> Result(#(Lua, List(Value)), LuaError(e)) /// Calls a Lua function by reference. /// /// ## Examples +/// /// ```gleam -/// let assert Ok(#(state, [fun])) = glua.eval(state: glua.new(), code: "return math.sqrt") -/// let assert Ok(#(state, [ref])) = glua.call_function( -/// state:, -/// ref: fun, -/// args: [glua.int(81)], -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(9) +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code: "return math.sqrt")) +/// use fun <- glua.try(list.first(ret)) +/// +/// use ret <- glua.then(glua.call_function( +/// ref: fun, +/// args: [glua.int(81)], +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.float) +/// }) +/// // -> Ok(#(_state, 9.0)) /// ``` /// /// ```gleam @@ -744,21 +1096,34 @@ pub fn eval_file( /// /// return fib /// " -/// let assert Ok(#(state, [fun])) = glua.eval(state: glua.new(), code:) -/// let assert Ok(#(state), [ref])) = glua.call_function( -/// state: lua, -/// ref: fun, -/// args: [glua.int(10)], -/// ) -/// glua.dereference(state:, ref:, using: decode.int) -/// // -> Ok(55) +/// +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.eval(code:)) +/// use ref <- glua.try(list.first(ret)) +/// +/// use ret <- glua.then(glua.call_function( +/// ref: fun, +/// args: [glua.int(10)], +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.int) +/// }) +/// // -> Ok(#(_state, 55)) /// ``` -@external(erlang, "glua_ffi", "call_function") pub fn call_function( - state lua: Lua, ref fun: Value, args args: List(Value), -) -> Result(#(Lua, List(Value)), LuaError) +) -> Action(List(Value), e) { + Action(do_call_function(_, fun, args)) +} + +@external(erlang, "glua_ffi", "call_function") +fn do_call_function( + lua: Lua, + fun: Value, + args: List(Value), +) -> Result(#(Lua, List(Value)), LuaError(e)) /// Gets a reference to the function at `keys`, then inmediatly calls it with the provided `args`. /// @@ -767,19 +1132,21 @@ pub fn call_function( /// ## Examples /// /// ```gleam -/// let assert Ok(#(state, [ref])) = glua.call_function_by_name( -/// state: glua.new(), -/// keys: ["string", "upper"], -/// args: [glua.string("hello from Gleam!")], -/// ) -/// glua.dereference(state:, ref:, using: decode.string) -/// // -> Ok(HELLO FROM GLEAM!") +/// glua.run(glua.new(), { +/// use ret <- glua.then(glua.call_function_by_name( +/// keys: ["string", "upper"], +/// args: [glua.string("hello from Gleam!")] +/// )) +/// use ref <- glua.try(list.first(ret)) +/// +/// glua.dereference(ref:, using: decode.string) +/// }) +/// // -> Ok(#(_state, "HELLO FROM GLEAM!")) /// ``` pub fn call_function_by_name( - state lua: Lua, keys keys: List(String), args args: List(Value), -) -> Result(#(Lua, List(Value)), LuaError) { - use fun <- result.try(get(lua, keys)) - call_function(lua, fun, args) +) -> Action(List(Value), e) { + use fun <- then(get(keys)) + call_function(fun, args) } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index eb31c1a..d585b28 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -23,9 +23,6 @@ to_gleam(Value) -> {error, {unknown_error, nil}} end. -dereference_list(St, LuerlTerms) -> - lists:map(fun (Lt) -> dereference(St, Lt) end, LuerlTerms). - %% transforms Lua values to their corresponding Erlang representation %% this is similar to `luerl:decode/2`, but returns values that are more decode-friendly in Gleam dereference(St, LT) -> @@ -125,6 +122,8 @@ map_error({lua_error, {assert_error, Msg} = Error, State}) -> end; map_error({lua_error, {badarg, F, Args}, State}) -> {lua_runtime_exception, {badarg, atom_to_binary(F), Args}, State}; +map_error({lua_error, {glua_action_error, Err}, _}) -> + Err; map_error({lua_error, _, State}) -> {lua_runtime_exception, unknown_exception, State}; map_error(Error) -> @@ -217,8 +216,12 @@ coerce_nil() -> wrap_fun(Fun) -> {erl_func, fun(Args, State) -> - {NewState, Ret} = Fun(State, dereference_list(State, Args)), - {Ret, NewState} + {action, F} = Fun(Args), + case F(State) of + {ok, {NewState, Ret}} -> {Ret, NewState}; + {error, Err} -> + {error, map_error(lua_error({glua_action_error, Err}, State))} + end end}. sandbox_fun(Msg) -> diff --git a/test/glua_test.gleam b/test/glua_test.gleam index c701e9c..3fc8e42 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,10 +1,11 @@ import gleam/dict -import gleam/dynamic import gleam/dynamic/decode +import gleam/float import gleam/int import gleam/list import gleam/option import gleam/pair +import gleam/result import gleeunit import glua @@ -13,35 +14,32 @@ pub fn main() -> Nil { } pub fn get_table_test() { - let lua = glua.new() let my_table = [ #("meaning of life", 42), #("pi", 3), #("euler's number", 3), ] let cool_numbers = - glua.function(fn(lua, _params) { - let #(lua, table) = - glua.table( - lua, - my_table - |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), - ) - - #(lua, [table]) + glua.function(fn(_params) { + use table <- glua.then(glua.table( + my_table + |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), + )) + glua.success([table]) }) - let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(lua, ["cool_numbers"], []) - let assert Ok(table) = - glua.dereference( - state: lua, - ref:, + let action = { + use Nil <- glua.then(glua.set(["cool_numbers"], cool_numbers)) + use ret <- glua.then(glua.call_function_by_name(["cool_numbers"], [])) + let assert [number] = ret + use table <- glua.then(glua.dereference( + ref: number, using: decode.dict(decode.string, decode.int), - ) - - assert table == dict.from_list(my_table) + )) + assert table == dict.from_list(my_table) + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn sandbox_test() { @@ -49,31 +47,38 @@ pub fn sandbox_test() { let args = list.map([20, 10], glua.int) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) + glua.run(lua, glua.call_function_by_name(keys: ["math", "max"], args:)) assert exception == glua.ErrorCall("math.max is sandboxed", option.None) let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = - glua.eval(state: lua, code: "return string.upper('my_string')") + glua.run(lua, glua.eval("return string.upper('my_string')")) assert index == "upper" let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.eval( - state: lua, - code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", + glua.run( + lua, + glua.eval( + // TODO: test failure case + "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", + ), ) assert exception == glua.ErrorCall("os.execute is sandboxed", option.None) let assert Ok(lua) = glua.sandbox(glua.new(), ["print"]) - let arg = glua.string("sandbox test is failing") let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name(state: lua, keys: ["print"], args: [arg]) + glua.run( + lua, + glua.call_function_by_name(keys: ["print"], args: [ + glua.string("sandbox test is failing"), + ]), + ) assert exception == glua.ErrorCall("print is sandboxed", option.None) } @@ -82,59 +87,126 @@ pub fn new_sandboxed_test() { let assert Ok(lua) = glua.new_sandboxed([]) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.eval(state: lua, code: "return load(\"return 1\")") + glua.run(lua, glua.eval("return load(\"return 1\")")) assert exception == glua.ErrorCall("load is sandboxed", option.None) - let arg = glua.int(1) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) + glua.run( + lua, + glua.call_function_by_name(keys: ["os", "exit"], args: [ + glua.int(1), + ]), + ) assert exception == glua.ErrorCall("os.exit is sandboxed", option.None) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = - glua.eval(state: lua, code: "io.write('some_message')") + glua.run(lua, glua.eval("io.write('some_message')")) assert index == "write" let assert Ok(lua) = glua.new_sandboxed([["package"], ["require"]]) - let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) + let action = { + use _ <- glua.then(glua.set_lua_paths(paths: ["./test/lua/?.lua"])) - let code = "local s = require 'example'; return s" - let assert Ok(#(state, [ref])) = glua.eval(state: lua, code:) - let assert Ok(result) = glua.dereference(state:, ref:, using: decode.string) + let code = "local s = require 'example'; return s" + use ref <- glua.then(glua.eval(code)) + use ref <- glua.try(list.first(ref)) + use result <- glua.map(glua.dereference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + glua.success(Nil) + } + let assert Ok(_) = glua.run(lua, action) } -pub fn encoding_and_decoding_nested_tables_test() { - let lua = glua.new() - let #(lua, tb1) = - glua.table(lua, [#(glua.string("deeper_key"), glua.string("deeper_value"))]) - let #(lua, tb2) = glua.table(lua, [#(glua.int(1), tb1)]) - let #(lua, tb3) = glua.table(lua, [#(glua.string("key"), tb2)]) +pub fn guard_test() { + let action = { + use ret <- glua.then( + glua.call_function_by_name(keys: ["math", "sqrt"], args: [glua.float(9.0)]), + ) + use ref <- glua.try(list.first(ret)) - let keys = ["my_nested_table"] + use n <- glua.then(glua.dereference(ref:, using: decode.float)) + use <- glua.guard(when: n <. 0.0, return: Nil) + + glua.success("the root square of 9.0 is " <> float.to_string(n)) + } - let nested_table_decoder = - decode.dict( - decode.string, - decode.dict(decode.int, decode.dict(decode.string, decode.string)), + assert glua.run(glua.new(), action) |> result.map(pair.second) + == Ok("the root square of 9.0 is 3.0") + + let action = { + use ret <- glua.then(glua.eval(code: "local a = 1")) + use <- glua.guard( + when: ret == [], + return: "Expected at least one return value", ) - let assert Ok(lua) = glua.set(state: lua, keys:, value: tb3) + glua.success("unreachable") + } - let assert Ok(ref) = glua.get(state: lua, keys:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: nested_table_decoder) + assert glua.run(glua.new(), action) + == Error(glua.CustomError("Expected at least one return value")) +} - assert result - == dict.from_list([ - #( - "key", - dict.from_list([#(1, dict.from_list([#("deeper_key", "deeper_value")]))]), - ), - ]) +pub fn map_test() { + let action = + glua.get(keys: ["math", "pi"]) + |> glua.then(glua.dereference(_, using: decode.float)) + |> glua.map(float.truncate) + + assert glua.run(glua.new(), action) |> result.map(pair.second) == Ok(3) + + let action = { + use ret <- glua.then(glua.eval("return 3 * true")) + use ref <- glua.try(list.first(ret)) + + glua.map(glua.dereference(ref:, using: decode.int), fn(n) { + "the result is " <> int.to_string(n) + }) + } + let assert Error(glua.LuaRuntimeException(exception, _state)) = + glua.run(glua.new(), action) + + assert exception == glua.BadArith("*", ["3", "true"]) +} + +pub fn encoding_and_decoding_nested_tables_test() { + let action = { + use tb1 <- glua.then( + glua.table([#(glua.string("deeper_key"), glua.string("deeper_value"))]), + ) + use tb2 <- glua.then(glua.table([#(glua.int(1), tb1)])) + use tb3 <- glua.then(glua.table([#(glua.string("key"), tb2)])) + + let keys = ["my_nested_table"] + + let nested_table_decoder = + decode.dict( + decode.string, + decode.dict(decode.int, decode.dict(decode.string, decode.string)), + ) + + use Nil <- glua.then(glua.set(keys:, value: tb3)) + use ref <- glua.then(glua.get(keys:)) + + use result <- glua.then(glua.dereference(ref:, using: nested_table_decoder)) + + assert result + == dict.from_list([ + #( + "key", + dict.from_list([ + #(1, dict.from_list([#("deeper_key", "deeper_value")])), + ]), + ), + ]) + glua.success(Nil) + } + + let assert Ok(_) = glua.run(glua.new(), action) } pub type Userdata { @@ -142,184 +214,204 @@ pub type Userdata { } pub fn userdata_test() { - let lua = glua.new() - let #(lua, userdata) = glua.userdata(lua, Userdata("my-userdata", 1)) - let userdata_decoder = { - use foo <- decode.field(1, decode.string) - use bar <- decode.field(2, decode.int) - decode.success(Userdata(foo:, bar:)) + let action = { + use userdata <- glua.then(glua.userdata(Userdata("my-userdata", 1))) + let userdata_decoder = { + use foo <- decode.field(1, decode.string) + use bar <- decode.field(2, decode.int) + decode.success(Userdata(foo:, bar:)) + } + + use Nil <- glua.then(glua.set(["my_userdata"], userdata)) + use ref <- glua.then(glua.eval("return my_userdata")) + use ref <- glua.try(list.first(ref)) + use result <- glua.then(glua.dereference(ref:, using: userdata_decoder)) + + assert result == Userdata("my-userdata", 1) + + use userdata <- glua.then(glua.userdata(Userdata("other-userdata", 2))) + use Nil <- glua.then(glua.set(["my_other_userdata"], userdata)) + glua.success(Nil) } + let assert Ok(#(lua, Nil)) = glua.run(glua.new(), action) - let assert Ok(lua) = glua.set(lua, ["my_userdata"], userdata) - let assert Ok(#(lua, [ref])) = glua.eval(lua, "return my_userdata") - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: userdata_decoder) - - assert result == Userdata("my-userdata", 1) - - let #(lua, userdata) = glua.userdata(lua, Userdata("other-userdata", 2)) - let assert Ok(lua) = glua.set(lua, ["my_other_userdata"], userdata) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(index, _), _)) = - glua.eval(lua, "return my_other_userdata.foo") + glua.run(lua, glua.eval("return my_other_userdata.foo")) assert index == "foo" } pub fn get_test() { - let state = glua.new() - - let assert Ok(ref) = glua.get(state: state, keys: ["math", "pi"]) - let assert Ok(pi) = glua.dereference(state:, ref:, using: decode.float) + let action = { + use ref <- glua.then(glua.get(keys: ["math", "pi"])) + use pi <- glua.then(glua.dereference(ref:, using: decode.float)) - assert pi >. 3.14 && pi <. 3.15 + // TODO: Replace with glua/lib/math.pi + assert pi >. 3.14 && pi <. 3.15 - let keys = ["my_table", "my_value"] - let encoded = glua.bool(True) - let assert Ok(state) = glua.set(state:, keys:, value: encoded) - let assert Ok(ref) = glua.get(state:, keys:) - let assert Ok(ret) = glua.dereference(state:, ref:, using: decode.bool) + let keys = ["my_table", "my_value"] + use Nil <- glua.then(glua.set(keys:, value: glua.bool(True))) + use ref <- glua.then(glua.get(keys:)) + use ret <- glua.then(glua.dereference(ref:, using: decode.bool)) - assert ret == True + assert ret == True + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) - let code = - " + let action = { + let code = + " my_value = 10 return 'ignored' " - let assert Ok(#(state, _)) = glua.new() |> glua.eval(code:) - let assert Ok(ref) = glua.get(state:, keys: ["my_value"]) - let assert Ok(ret) = glua.dereference(state:, ref:, using: decode.int) + use _ <- glua.then(glua.eval(code)) + use ref <- glua.then(glua.get(keys: ["my_value"])) + use ret <- glua.then(glua.dereference(ref:, using: decode.int)) - assert ret == 10 + assert ret == 10 + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn get_returns_proper_errors_test() { - let state = glua.new() - - assert glua.get(state:, keys: ["non_existent_global"]) + assert glua.run(glua.new(), glua.get(keys: ["non_existent_global"])) == Error(glua.KeyNotFound(["non_existent_global"])) - let encoded = glua.int(10) - let assert Ok(state) = - glua.set(state:, keys: ["my_table", "some_value"], value: encoded) - - assert glua.get(state:, keys: ["my_table", "my_val"]) - == Error(glua.KeyNotFound(["my_table", "my_val"])) + let action = { + use Nil <- glua.then(glua.set( + keys: ["my_table", "some_value"], + value: glua.int(10), + )) + use _ret <- glua.then(glua.get(keys: ["my_table", "my_val"])) + panic as "unreachable" + } + let assert Error(glua.KeyNotFound(["my_table", "my_val"])) = + glua.run(glua.new(), action) } pub fn set_test() { - let encoded = glua.string("custom version") + let action = { + let encoded = glua.string("custom version") - let assert Ok(lua) = - glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) - let assert Ok(ref) = glua.get(state: lua, keys: ["_VERSION"]) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use Nil <- glua.then(glua.set(keys: ["_VERSION"], value: encoded)) + use ref <- glua.then(glua.get(keys: ["_VERSION"])) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - assert result == "custom version" + assert result == "custom version" - let numbers = - [2, 4, 7, 12] - |> list.index_map(fn(n, i) { #(i + 1, n * n) }) + let numbers = + [2, 4, 7, 12] + |> list.index_map(fn(n, i) { #(i + 1, n * n) }) - let keys = ["math", "squares"] + let keys = ["math", "squares"] - let #(lua, encoded) = - glua.table( - lua, + use encoded <- glua.then(glua.table( numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), - ) - let assert Ok(lua) = glua.set(lua, keys, encoded) + )) + use Nil <- glua.then(glua.set(keys, encoded)) - let assert Ok(ref) = glua.get(lua, keys) - assert glua.dereference( - state: lua, + use ref <- glua.then(glua.get(keys)) + use check <- glua.then(glua.dereference( ref:, using: decode.dict(decode.int, decode.int), - ) - == Ok(dict.from_list([#(1, 4), #(2, 16), #(3, 49), #(4, 144)])) - - let count_odd = fn(lua: glua.Lua, args: List(dynamic.Dynamic)) { - let assert [list] = args - let assert Ok(list) = decode.run(list, decode.dict(decode.int, decode.int)) - - let count = - list.map(dict.to_list(list), pair.second) - |> list.count(int.is_odd) - - #(lua, list.map([count], glua.int)) - } - - let encoded = glua.function(count_odd) - let assert Ok(lua) = glua.set(glua.new(), ["count_odd"], encoded) - - let #(lua, arg) = - glua.table( - lua, - list.index_map(list.range(1, 10), fn(i, n) { - #(glua.int(i + 1), glua.int(n)) - }), - ) - - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(state: lua, keys: ["count_odd"], args: [arg]) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) - - assert result == 5 - - let #(lua, tbl) = - glua.table(lua, [ - #( - glua.string("is_even"), - glua.function(fn(lua, args) { - let assert [arg] = args - let assert Ok(arg) = decode.run(arg, decode.int) - #(lua, list.map([int.is_even(arg)], glua.bool)) + )) + assert check == dict.from_list([#(1, 4), #(2, 16), #(3, 49), #(4, 144)]) + + let count_odd = fn(args: List(glua.Value)) { + let assert [list] = args + use list <- glua.then(glua.dereference( + list, + decode.dict(decode.int, decode.int), + )) + + let count = + list.map(dict.to_list(list), pair.second) + |> list.count(int.is_odd) + + glua.success(list.map([count], glua.int)) + } + + let encoded = glua.function(count_odd) + use Nil <- glua.then(glua.set(["count_odd"], encoded)) + + use arg <- glua.then( + glua.table( + list.index_map(list.range(1, 10), fn(i, n) { + #(glua.int(i + 1), glua.int(n)) }), ), - #( - glua.string("is_odd"), - glua.function(fn(lua, args) { - let assert [arg] = args - let assert Ok(arg) = decode.run(arg, decode.int) - #(lua, list.map([int.is_odd(arg)], glua.bool)) - }), - ), - ]) + ) - let arg = glua.int(4) + use refs <- glua.then( + glua.call_function_by_name(keys: ["count_odd"], args: [arg]), + ) + use ref <- glua.try(list.first(refs)) + + use result <- glua.then(glua.dereference(ref:, using: decode.int)) + + assert result == 5 + + use tbl <- glua.then( + glua.table([ + #( + glua.string("is_even"), + glua.function(fn(args) { + let assert [arg] = args + use arg <- glua.then(glua.dereference(arg, decode.int)) + list.map([int.is_even(arg)], glua.bool) + |> glua.success() + }), + ), + #( + glua.string("is_odd"), + glua.function(fn(args) { + let assert [arg] = args + use arg <- glua.then(glua.dereference(arg, decode.int)) + list.map([int.is_odd(arg)], glua.bool) + |> glua.success() + }), + ), + ]), + ) - let assert Ok(lua) = glua.set(state: lua, keys: ["my_functions"], value: tbl) + use Nil <- glua.then(glua.set(keys: ["my_functions"], value: tbl)) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name( - state: lua, - keys: ["my_functions", "is_even"], - args: [arg], + use refs <- glua.then( + glua.call_function_by_name(keys: ["my_functions", "is_even"], args: [ + glua.int(4), + ]), ) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.bool) - - assert result == True + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.bool)) - let assert Ok(#(lua, [ref])) = - glua.eval(state: lua, code: "return my_functions.is_odd(4)") + assert result == True - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.bool) + use refs <- glua.then(glua.eval("return my_functions.is_odd(4)")) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.bool)) - assert result == False + assert result == False + glua.success(Nil) + } + glua.run(glua.new(), action) } pub fn set_lua_paths_test() { - let assert Ok(state) = - glua.set_lua_paths(state: glua.new(), paths: ["./test/lua/?.lua"]) + let action = { + use Nil <- glua.then(glua.set_lua_paths(paths: ["./test/lua/?.lua"])) - let code = "local s = require 'example'; return s" + let code = "local s = require 'example'; return s" - let assert Ok(#(lua, [ref])) = glua.eval(state:, code:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use refs <- glua.then(glua.eval(code)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + glua.success(Nil) + } + glua.run(glua.new(), action) } pub fn get_private_test() { @@ -345,63 +437,74 @@ pub fn delete_private_test() { } pub fn load_test() { - let assert Ok(#(lua, chunk)) = - glua.load(state: glua.new(), code: "return 5 * 5") - let assert Ok(#(lua, [ref])) = glua.eval_chunk(state: lua, chunk:) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) - - assert result == 25 + let action = { + use chunk <- glua.then(glua.load(code: "return 5 * 5")) + use refs <- glua.then(glua.eval_chunk(chunk)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) + + assert result == 25 + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn eval_load_file_test() { - let assert Ok(#(lua, chunk)) = - glua.load_file(state: glua.new(), path: "./test/lua/example.lua") - let assert Ok(#(lua, [ref])) = glua.eval_chunk(state: lua, chunk:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + let action = { + use chunk <- glua.then(glua.load_file("./test/lua/example.lua")) + use refs <- glua.then(glua.eval_chunk(chunk)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + use _ <- glua.then(glua.load_file("non_existent_file")) - let assert Error(e) = - glua.load_file(state: glua.new(), path: "non_existent_file") - assert e == glua.FileNotFound("non_existent_file") + panic as "unreachable" + } + let assert Error(glua.FileNotFound("non_existent_file")) = + glua.run(glua.new(), action) } pub fn eval_test() { - let assert Ok(#(lua, [ref])) = - glua.eval(state: glua.new(), code: "return 'hello, ' .. 'world!'") - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) - - assert result == "hello, world!" + let actions = { + use refs <- glua.then(glua.eval("return 'hello, ' .. 'world!'")) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - let assert Ok(#(lua, refs)) = - glua.eval(state: lua, code: "return 2 + 2, 3 - 1") - let assert Ok(results) = - list.try_map(refs, glua.dereference(state: lua, ref: _, using: decode.int)) + assert result == "hello, world!" - assert results == [4, 2] + use refs <- glua.then(glua.eval("return 2 + 2, 3 - 1")) + use return <- glua.then( + glua.fold(refs, glua.dereference(ref: _, using: decode.int)), + ) + assert return == [4, 2] + glua.success(Nil) + } + glua.run(glua.new(), actions) } pub fn eval_returns_proper_errors_test() { - let state = glua.new() + let lua = glua.new() - let assert Error(e) = glua.eval(state:, code: "if true then 1 + ") + let assert Error(e) = glua.run(lua, glua.eval("if true then 1 + ")) assert e == glua.LuaCompileFailure([ glua.LuaCompileError(1, glua.Parse, "syntax error before: 1"), ]) - let assert Error(e) = glua.eval(state:, code: "print(\"hi)") - + let assert Error(e) = glua.run(lua, glua.eval("print(\"hi)")) assert e == glua.LuaCompileFailure([ glua.LuaCompileError(1, glua.Tokenize, "syntax error near '\"'"), ]) - let assert Ok(#(lua, [ref])) = - glua.eval(state:, code: "return 'Hello from Lua!'") - assert glua.dereference(state: lua, ref:, using: decode.int) + let action = { + use refs <- glua.then(glua.eval("return 'Hello from Lua!'")) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.dereference(ref:, using: decode.int)) + panic as "unreachable" + } + assert glua.run(lua, action) == Error( glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), ) @@ -409,7 +512,7 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.IllegalIndex(value:, index:), state: _, - )) = glua.eval(state:, code: "return a.b") + )) = glua.run(lua, glua.eval("return a.b")) assert value == "nil" assert index == "b" @@ -417,7 +520,7 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.ErrorCall(message, level), state: _, - )) = glua.eval(state:, code: "error('error message')") + )) = glua.run(lua, glua.eval("error('error message')")) assert message == "error message" assert level == option.None @@ -425,31 +528,31 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.ErrorCall(message, level), state: _, - )) = glua.eval(state:, code: "error('error with level', 1)") + )) = glua.run(lua, glua.eval("error('error with level', 1)")) assert message == "error with level" assert level == option.Some(1) - let assert Error(_) = glua.eval(state:, code: "error({1})") + let assert Error(_) = glua.run(lua, glua.eval("error({1})")) let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), state: _, - )) = glua.eval(state:, code: "local a = 5; a()") + )) = glua.run(lua, glua.eval("local a = 5; a()")) assert value == "5" let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedMethod(_, method:), state: _, - )) = glua.eval(state:, code: "local i = function(x) return x end; i:call(1)") + )) = glua.run(lua, glua.eval("local i = function(x) return x end; i:call(1)")) assert method == "call" let assert Error(glua.LuaRuntimeException( exception: glua.BadArith(operator:, args:), state: _, - )) = glua.eval(state:, code: "return 10 / 0") + )) = glua.run(lua, glua.eval("return 10 / 0")) assert operator == "/" assert args == ["10", "0"] @@ -457,130 +560,206 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.AssertError(message:), state: _, - )) = glua.eval(state:, code: "assert(1 == 2, 'assertion failed')") + )) = glua.run(lua, glua.eval("assert(1 == 2, 'assertion failed')")) assert message == "assertion failed" - let assert Error(_) = glua.eval(state:, code: "assert(false, {1})") + let assert Error(_) = glua.run(lua, glua.eval("assert(false, {1})")) } pub fn eval_file_test() { - let assert Ok(#(state, [ref])) = - glua.eval_file(state: glua.new(), path: "./test/lua/example.lua") - let assert Ok(result) = glua.dereference(state:, ref:, using: decode.string) + let action = { + use refs <- glua.then(glua.eval_file("./test/lua/example.lua")) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + glua.success(Nil) + } + glua.run(glua.new(), action) } pub fn call_function_test() { - let assert Ok(#(lua, [fun])) = - glua.eval(state: glua.new(), code: "return string.reverse") + let action = { + use return <- glua.then(glua.eval("return string.reverse")) + use fun <- glua.try(list.first(return)) - let encoded = glua.string("auL") + let encoded = glua.string("auL") - let assert Ok(#(lua, [ref])) = - glua.call_function(state: lua, ref: fun, args: [encoded]) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use refs <- glua.then(glua.call_function(fun, [encoded])) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - assert result == "Lua" + assert result == "Lua" - let assert Ok(#(lua, [fun])) = - glua.eval(state: lua, code: "return function(a, b) return a .. b end") + use return <- glua.then(glua.eval("return function(a, b) return a .. b end")) + use fun <- glua.try(list.first(return)) - let args = list.map(["Lua in ", "Gleam"], glua.string) + let args = list.map(["Lua in ", "Gleam"], glua.string) - let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, ref: fun, args:) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.string) + use refs <- glua.then(glua.call_function(fun, args)) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.string)) - assert result == "Lua in Gleam" + assert result == "Lua in Gleam" + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn call_function_returns_proper_errors_test() { - let state = glua.new() - - let assert Ok(#(state, [ref])) = - glua.eval(state:, code: "return string.upper") - - let arg = glua.string("Hello from Gleam!") + let action = { + use refs <- glua.then(glua.eval("return string.upper")) + use ref <- glua.try(list.first(refs)) + use refs <- glua.then( + glua.call_function(ref, [glua.string("Hello from Gleam!")]), + ) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.dereference(ref:, using: decode.int)) + panic as "unreachable" + } - let assert Ok(#(lua, [ref])) = glua.call_function(state:, ref:, args: [arg]) - assert glua.dereference(state: lua, ref:, using: decode.int) + assert glua.run(glua.new(), action) == Error( glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), ) - let assert Ok(#(lua, [ref])) = glua.eval(state:, code: "return 1") + let action = { + use refs <- glua.then(glua.eval("return 1")) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.call_function(ref, [])) + panic as "unreachable" + } let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), state: _, - )) = glua.call_function(state: lua, ref:, args: []) + )) = glua.run(glua.new(), action) assert value == "1" -} -pub fn call_function_by_name_test() { - let args = list.map([20, 10], glua.int) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(state: glua.new(), keys: ["math", "max"], args:) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) + let fun = + fn(args) { + case args { + [a] -> glua.success([a]) + _ -> glua.error("called without arguments") + } + } + |> glua.function - assert result == 20 + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(message: msg, level: option.None), + state: _, + )) = glua.run(glua.new(), glua.call_function(fun, [])) - let assert Ok(#(lua, [ref])) = - glua.call_function_by_name(state: lua, keys: ["math", "min"], args:) - let assert Ok(result) = glua.dereference(state: lua, ref:, using: decode.int) + assert msg == "called without arguments" - assert result == 10 + let fun = + fn(args) { + case args { + [] -> glua.success([]) + _ -> glua.error_with_level("function takes 0 arguments", 1) + } + } + |> glua.function - let arg = glua.float(10.2) - let assert Ok(#(state, [ref])) = - glua.call_function_by_name(state: lua, keys: ["math", "type"], args: [arg]) - let assert Ok(result) = - glua.dereference(state:, ref:, using: decode.optional(decode.string)) + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(message: msg, level: option.Some(level)), + state: _, + )) = glua.run(glua.new(), glua.call_function(fun, [glua.int(2)])) - assert result == option.Some("float") + assert msg == "function takes 0 arguments" + assert level == 1 } -pub fn nested_function_references_test() { - let code = "return function() return math.sqrt end" +pub fn call_function_by_name_test() { + let action = { + let args = list.map([20, 10], glua.int) + use refs <- glua.then(glua.call_function_by_name( + keys: ["math", "max"], + args:, + )) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) + + assert result == 20 + + use refs <- glua.then(glua.call_function_by_name( + keys: ["math", "min"], + args:, + )) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.int)) + + assert result == 10 + + let arg = glua.float(10.2) + use refs <- glua.then( + glua.call_function_by_name(keys: ["math", "type"], args: [arg]), + ) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference( + ref:, + using: decode.optional(decode.string), + )) + + assert result == option.Some("float") - let assert Ok(#(lua, [ref])) = glua.eval(state: glua.new(), code:) - let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, ref:, args: []) + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) +} - let arg = glua.int(400) - let assert Ok(#(_, [ref])) = glua.call_function(state: lua, ref:, args: [arg]) - let assert Ok(result) = - glua.dereference(state: lua, ref:, using: decode.float) - assert result == 20.0 +pub fn nested_function_references_test() { + let action = { + let code = "return function() return math.sqrt end" + + use refs <- glua.then(glua.eval(code)) + use ref <- glua.try(list.first(refs)) + use refs <- glua.then(glua.call_function(ref, [])) + use ref <- glua.try(list.first(refs)) + + use refs <- glua.then(glua.call_function(ref, [glua.int(400)])) + use ref <- glua.try(list.first(refs)) + use result <- glua.then(glua.dereference(ref:, using: decode.float)) + assert result == 20.0 + glua.success(Nil) + } + let assert Ok(_) = glua.run(glua.new(), action) } pub fn format_error_test() { - let state = glua.new() + let lua = glua.new() - let assert Error(e) = glua.eval(state:, code: "1 +") + let assert Error(e) = glua.run(lua, glua.eval("1 +")) assert glua.format_error(e) == "Lua compile error: \n\nFailed to parse: error on line 1: syntax error before: 1" - let assert Error(e) = glua.eval(state:, code: "assert(false)") + let assert Error(e) = glua.run(lua, glua.eval("assert(false)")) assert glua.format_error(e) == "Lua runtime exception: Assertion failed with message: assertion failed\n\nLine 1: assert(false)" let assert Error(e) = - glua.eval(state:, code: "local a = true; local b = 1 * a") + glua.run(lua, glua.eval("local a = true; local b = 1 * a")) assert glua.format_error(e) == "Lua runtime exception: Bad arithmetic expression: 1 * true" - let assert Error(e) = glua.get(state:, keys: ["non_existent"]) + let assert Error(e) = glua.run(lua, glua.get(keys: ["non_existent"])) assert glua.format_error(e) == "Key \"non_existent\" not found" - let assert Error(e) = glua.load_file(state:, path: "non_existent_file") + let assert Error(e) = glua.run(lua, glua.load_file("non_existent_file")) assert glua.format_error(e) == "Lua source file \"non_existent_file\" not found" - let assert Ok(#(lua, [ref])) = glua.eval(state:, code: "return 1 + 1") - let assert Error(e) = glua.dereference(state: lua, ref:, using: decode.string) + let action = { + use refs <- glua.then(glua.eval("return 1 + 1")) + use ref <- glua.try(list.first(refs)) + use _ <- glua.then(glua.dereference(ref:, using: decode.string)) + panic as "unreachable" + } + let assert Error(e) = glua.run(glua.new(), action) assert glua.format_error(e) == "Expected String, but found Int" + + let assert Error(e) = glua.run(glua.new(), glua.failure(1)) + assert glua.format_error(e) == "1" } diff --git a/test/lib_test.gleam b/test/lib_test.gleam index 4eea26e..861b03d 100644 --- a/test/lib_test.gleam +++ b/test/lib_test.gleam @@ -12,7 +12,7 @@ import glua/lib/utf8 /// Checks to see if a value matches a value at the given path. fn check(val: glua.Value, path: List(String)) { - let assert Ok(found) = glua.get(glua.new(), path) + let assert Ok(#(_, found)) = glua.run(glua.new(), glua.get(path)) assert val == found }