From 304e6e1a4adcd1e1f9f43892b92697cc0f24d75b Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:47:25 -1000 Subject: [PATCH 1/4] Add deser.gleam --- src/glua/deser.gleam | 611 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 src/glua/deser.gleam diff --git a/src/glua/deser.gleam b/src/glua/deser.gleam new file mode 100644 index 0000000..2d4b0bd --- /dev/null +++ b/src/glua/deser.gleam @@ -0,0 +1,611 @@ +//// The deserialize API is similar to the gleam/dynamic/decode API but has some differences. +//// +//// The main difference is that it can change the state of a lua program due to metatables. +//// If you do not wish to keep the changed state, discard the new state. + +import gleam/bit_array +import gleam/bool +import gleam/dict.{type Dict} +import gleam/dynamic +import gleam/float +import gleam/int +import gleam/list +import gleam/option.{type Option} +import gleam/string +import glua.{type Lua, type Value} + +pub opaque type Deserializer(t) { + Deserializer(function: fn(Lua, Value) -> Return(t)) +} + +pub type DeserializeError { + DeserializeError(expected: String, found: String, path: List(Value)) +} + +type Return(t) = + #(t, List(DeserializeError)) + +pub fn field( + field_path: Value, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + subfield([field_path], field_decoder, next) +} + +pub fn subfield( + field_path: List(Value), + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(function: fn(lua, data) { + let #(out, errors1) = + index_into( + lua, + field_path, + [], + field_decoder.function, + data, + fn(lua, data, position) { + let #(default, _) = field_decoder.function(lua, data) + #(default, [DeserializeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }, + ) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) + }) +} + +fn index_into( + lua: Lua, + path: List(Value), + position: List(Value), + inner: fn(Lua, Value) -> Return(b), + data: Value, + handle_miss: fn(Lua, Value, List(Value)) -> Return(b), +) -> Return(b) { + case path { + [] -> { + data + |> inner(lua, _) + |> push_path(list.reverse(position)) + } + + [key, ..path] -> { + case classify(data) { + "Table" -> + case get_table_key(lua, data, key) { + Ok(#(lua, data)) -> { + index_into(lua, path, [key, ..position], inner, data, handle_miss) + } + Error(Nil) -> { + handle_miss(lua, data, [key, ..position]) + } + } + class -> { + let #(default, _) = inner(lua, data) + #(default, [DeserializeError("Table", class, [])]) + |> push_path(list.reverse(position)) + } + } + } + } +} + +pub fn run( + lua: Lua, + data: Value, + deser: Deserializer(t), +) -> Result(t, List(DeserializeError)) { + let #(maybe_invalid_data, errors) = deser.function(lua, data) + case errors { + [] -> Ok(maybe_invalid_data) + [_, ..] -> Error(errors) + } +} + +/// Puts all values in a MultiValue, a special container that is only addressable with `deser.item` +pub fn run_multi(lua: Lua, data: List(Value), deser: Deserializer(t)) { + let list = new_mval(data) + run(lua, list, deser) +} + +@external(erlang, "glua_ffi", "new_mval") +fn new_mval(values: List(Value)) -> Value + +pub fn item( + field_path: Int, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(fn(lua, data) { + let class = classify(data) + let pos = glua.string(" int.to_string(field_path) <> ">") + + let fail = fn(error) { + let #(default, _) = field_decoder.function(lua, data) + let #(out, errors) = next(default).function(lua, data) + #(out, list.append([error], errors)) + } + use <- bool.lazy_guard(class != "MultiValue", fn() { + fail(DeserializeError("MultiValue", classify(data), [pos])) + }) + case get_entry(data, field_path) { + Ok(val) -> { + let #(out, errors1) = + val |> field_decoder.function(lua, _) |> push_path([pos]) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) + } + Error(Nil) -> fail(DeserializeError("Field", "Nothing", [pos])) + } + }) +} + +@external(erlang, "glua_ffi", "get_entry") +fn get_entry(mval: Value, idx: Int) -> Result(Value, Nil) + +pub fn error_to_string(error: DeserializeError) { + "Expected " + <> error.expected + <> " got " + <> error.found + <> " at [" + <> error.path |> list.map(string.inspect) |> string.join(", ") + <> "]" +} + +pub fn at(path: List(Value), inner: Deserializer(a)) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + index_into(lua, path, [], inner.function, data, fn(lua, data, position) { + let #(default, _) = inner.function(lua, data) + #(default, [DeserializeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }) + }) +} + +@external(erlang, "glua_ffi", "get_table_key") +fn get_table_key( + lua: Lua, + table: Value, + key: Value, +) -> Result(#(Lua, Value), Nil) + +fn push_path(layer: Return(t), path: List(Value)) -> Return(t) { + let errors = + list.map(layer.1, fn(error) { + DeserializeError(..error, path: list.append(path, error.path)) + }) + #(layer.0, errors) +} + +pub fn success(data: t) -> Deserializer(t) { + Deserializer(function: fn(_lua, _) { #(data, []) }) +} + +pub fn deser_error( + expected expected: String, + found found: Value, +) -> List(DeserializeError) { + [DeserializeError(expected: expected, found: classify(found), path: [])] +} + +@external(erlang, "glua_ffi", "classify") +pub fn classify(a: anything) -> String + +pub fn optional_field( + key: Value, + default: t, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(function: fn(lua, data) { + let class = classify(data) + use <- bool.lazy_guard(class != "Table", fn() { + next(default).function(lua, data) + }) + let #(out, errors1) = + case get_table_key(lua, data, key) { + Ok(#(lua, data)) -> { + field_decoder.function(lua, data) + } + Error(Nil) -> #(default, []) + } + |> push_path([key]) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) + }) +} + +pub fn optionally_at( + path: List(Value), + default: a, + inner: Deserializer(a), +) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + index_into(lua, path, [], inner.function, data, fn(_, _, _) { + #(default, []) + }) + }) +} + +fn run_dynamic_function( + lua: Lua, + data: Value, + expected: String, + zero: t, +) -> Return(t) { + let got = classify(data) + case got == expected { + True -> #(decode(data, lua), []) + False -> #(zero, [ + DeserializeError(expected, got, []), + ]) + } +} + +/// Warning: Can panic +@external(erlang, "luerl", "decode") +fn decode(a: Value, lua: Lua) -> a + +pub const string: Deserializer(String) = Deserializer(deser_string) + +fn deser_string(lua, data: Value) -> Return(String) { + run_dynamic_function(lua, data, "String", "") +} + +pub const byte_string: Deserializer(BitArray) = Deserializer(deser_byte_string) + +fn deser_byte_string(lua, data: Value) -> Return(BitArray) { + run_dynamic_function(lua, data, "ByteString", bit_array.from_string("")) +} + +pub const bool: Deserializer(Bool) = Deserializer(deser_bool) + +fn deser_bool(lua, data: Value) -> Return(Bool) { + run_dynamic_function(lua, data, "Bool", True) +} + +pub const number: Deserializer(Float) = Deserializer(deser_num) + +fn deser_num(lua, data: Value) -> Return(Float) { + let got = classify(data) + case got { + "Float" -> #(decode(data, lua), []) + "Int" -> { + let int: Int = decode(data, lua) + #(int.to_float(int), []) + } + _ -> #(0.0, [ + DeserializeError("Number", got, []), + ]) + } +} + +pub const int: Deserializer(Int) = Deserializer(deser_int) + +fn deser_int(lua, data: Value) -> Return(Int) { + let got = classify(data) + let error = #(0, [ + DeserializeError("Int", got, []), + ]) + case got { + "Float" -> { + let float: Float = decode(data, lua) + let int = float.truncate(float) + case int.to_float(int) == float { + True -> #(int, []) + False -> error + } + } + "Int" -> { + #(decode(data, lua), []) + } + _ -> error + } +} + +pub const raw: Deserializer(Value) = Deserializer(decode_raw) + +fn decode_raw(_lua, data: Value) -> Return(Value) { + let class = classify(data) + case class { + "Unknown" + | // Disallow smuggling MultiValues out + "MultiValue" -> #(glua.nil(), [DeserializeError("Any", class, [])]) + _ -> #(data, []) + } +} + +pub const userdata: Deserializer(dynamic.Dynamic) = Deserializer( + deser_user_defined, +) + +@external(erlang, "glua_ffi", "unwrap_userdata") +fn unwrap_userdata(a: userdata) -> Result(dynamic.Dynamic, Nil) + +fn deser_user_defined(lua, data: Value) -> Return(dynamic.Dynamic) { + let got = classify(data) + let error = #(dynamic.nil(), [DeserializeError("UserData", got, [])]) + use <- bool.guard(got != "UserData", error) + use <- bool.guard( + !userdata_exists(lua, data), + #(dynamic.nil(), [ + DeserializeError("UserData", "NonexistentUserData", []), + ]), + ) + case unwrap_userdata(decode(data, lua)) { + Ok(dyn) -> #(dyn, []) + Error(Nil) -> error + } +} + +/// Strictly decodes a list +/// 1. first element must start with 1 +/// 2. no gaps +/// 3. no non number keys +pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { + Deserializer(fn(lua, data) { + let class = classify(data) + let not_table_list = #([], [DeserializeError("TableList", class, [])]) + case class { + "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #([], [ + DeserializeError("TableList", "NonexistentTable", []), + ]), + ) + let res = + get_table_list_transform(lua, data, #([], []), fn(it, acc) { + case acc.1 { + [] -> { + case inner.function(lua, it) { + #(value, []) -> { + #(list.prepend(acc.0, value), acc.1) + } + #(_, errors) -> + push_path(#([], errors), [glua.string("items")]) + } + } + [_, ..] -> acc + } + }) + case res { + Ok(it) -> it + Error(Nil) -> not_table_list + } + } + _ -> not_table_list + } + }) +} + +@external(erlang, "glua_ffi", "get_table_list_transform") +fn get_table_list_transform( + lua: Lua, + table: Value, + accumulator: acc, + func: fn(Value, acc) -> acc, +) -> Result(acc, Nil) + +/// An ipairs style list decoder. +/// This function consults __index +pub fn ipairs( + of inner: Deserializer(a), + limit limit: Int, +) -> Deserializer(List(a)) { + Deserializer(function: fn(lua, data) { + let class = classify(data) + case class { + "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #([], [ + DeserializeError("Table", "NonexistentTable", []), + ]), + ) + case ipair_inner(data, [], 1, lua, inner, limit) { + Ok(ok) -> #(ok, []) + Error(errs) -> #([], errs) + } + } + _ -> #([], [DeserializeError("Table", class, [])]) + } + }) +} + +fn ipair_inner( + table: Value, + out: List(a), + idx: Int, + lua: Lua, + deser: Deserializer(a), + limit: Int, +) -> Result(List(a), List(DeserializeError)) { + use <- bool.lazy_guard(idx > limit, fn() { + Error([DeserializeError("Table", "UnprocessableTable", [])]) + }) + case get_table_key(lua, table, glua.int(idx)) { + Ok(#(lua, val)) -> { + case deser.function(lua, val) { + #(value, []) -> { + ipair_inner(table, [value, ..out], idx + 1, lua, deser, limit) + } + #(_, errors) -> { + push_path(#([], errors), [glua.string("items")]).1 + |> Error + } + } + } + Error(Nil) -> Ok(list.reverse(out)) + } +} + +pub fn dict( + key: Deserializer(key), + value: Deserializer(value), +) -> Deserializer(Dict(key, value)) { + Deserializer(fn(lua, data) { + let class = classify(data) + case class { + "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #(dict.new(), [DeserializeError("Table", "NonexistentTable", [])]), + ) + get_table_transform(lua, data, #(dict.new(), []), fn(k, v, a) { + // If there are any errors from previous key-value pairs then we + // don't need to run the decoders, instead return the existing acc. + case a.1 { + [] -> fold_dict(lua, a, k, v, key.function, value.function) + [_, ..] -> a + } + }) + } + _ -> #(dict.new(), [DeserializeError("Table", class, [])]) + } + }) +} + +@external(erlang, "glua_ffi", "table_exists") +fn table_exists(state: Lua, table: Value) -> Bool + +@external(erlang, "glua_ffi", "userdata_exists") +fn userdata_exists(state: Lua, userdata: Value) -> Bool + +/// Preconditions: `table` is a lua table +@external(erlang, "glua_ffi", "get_table_transform") +fn get_table_transform( + lua: Lua, + table: Value, + accumulator: acc, + func: fn(Value, Value, acc) -> acc, +) -> acc + +fn fold_dict( + lua: Lua, + acc: #(Dict(k, v), List(DeserializeError)), + key: Value, + value: Value, + key_decoder: fn(Lua, Value) -> Return(k), + value_decoder: fn(Lua, Value) -> Return(v), +) -> Return(Dict(k, v)) { + // First we decode the key. + case key_decoder(lua, key) { + #(key, []) -> + // Then we decode the value. + case value_decoder(lua, value) { + #(value, []) -> { + // It worked! Insert the new key-value pair so we can move onto the next. + let dict = dict.insert(acc.0, key, value) + #(dict, acc.1) + } + #(_, errors) -> + push_path(#(dict.new(), errors), [glua.string("values")]) + } + #(_, errors) -> push_path(#(dict.new(), errors), [glua.string("keys")]) + } +} + +pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { + Deserializer(function: fn(lua, data) { + case classify(data) { + "Nil" -> #(option.None, []) + _ -> { + let #(data, errors) = inner.function(lua, data) + #(option.Some(data), errors) + } + } + }) +} + +pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) { + Deserializer(function: fn(lua, d) { + let #(data, errors) = decoder.function(lua, d) + #(transformer(data), errors) + }) +} + +pub fn map_errors( + decoder: Deserializer(a), + transformer: fn(List(DeserializeError)) -> List(DeserializeError), +) -> Deserializer(a) { + Deserializer(function: fn(lua, d) { + let #(data, errors) = decoder.function(lua, d) + #(data, transformer(errors)) + }) +} + +pub fn collapse_errors( + decoder: Deserializer(a), + name: String, +) -> Deserializer(a) { + Deserializer(function: fn(lua, dynamic_data) { + let #(data, errors) as layer = decoder.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, deser_error(name, dynamic_data)) + } + }) +} + +pub fn then( + decoder: Deserializer(a), + next: fn(Lua, a) -> Deserializer(b), +) -> Deserializer(b) { + Deserializer(function: fn(lua, dynamic_data) { + let #(data, errors) = decoder.function(lua, dynamic_data) + let decoder = next(lua, data) + let #(data, _) as layer = decoder.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, errors) + } + }) +} + +pub fn one_of( + first: Deserializer(a), + or alternatives: List(Deserializer(a)), +) -> Deserializer(a) { + Deserializer(function: fn(lua, dynamic_data) { + let #(_, errors) as layer = first.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> run_decoders(dynamic_data, lua, layer, alternatives) + } + }) +} + +fn run_decoders( + data: Value, + lua: Lua, + failure: Return(a), + decoders: List(Deserializer(a)), +) -> Return(a) { + case decoders { + [] -> failure + + [decoder, ..decoders] -> { + let #(_, errors) as layer = decoder.function(lua, data) + case errors { + [] -> layer + [_, ..] -> run_decoders(data, lua, failure, decoders) + } + } + } +} + +pub fn failure(zero: a, expected: String) -> Deserializer(a) { + Deserializer(function: fn(_lua, d) { #(zero, deser_error(expected, d)) }) +} + +pub fn recursive(inner: fn() -> Deserializer(a)) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + let decoder = inner() + decoder.function(lua, data) + }) +} From b4c49cedc986635b9512d7fb0a890195d181fac4 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:50:07 -1000 Subject: [PATCH 2/4] Add function deserializer --- src/glua.gleam | 2 ++ src/glua/deser.gleam | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/glua.gleam b/src/glua.gleam index 24c2f1e..8f03e8a 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -556,6 +556,8 @@ pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value { /// to encourage using `glua.error` instead since `glua.failure` wouldn't make sense in that case. pub type Never +pub type Function + pub fn function_decoder() -> decode.Decoder( fn(List(Value)) -> Action(List(Value), e), ) { diff --git a/src/glua/deser.gleam b/src/glua/deser.gleam index 2d4b0bd..c74b603 100644 --- a/src/glua/deser.gleam +++ b/src/glua/deser.gleam @@ -342,6 +342,21 @@ fn deser_user_defined(lua, data: Value) -> Return(dynamic.Dynamic) { } } +pub const function: Deserializer(glua.Function) = Deserializer(deser_function) + +fn deser_function(_lua: Lua, data: Value) -> Return(glua.Function) { + let got = classify(data) + case got == "Function" { + True -> #(coerce_funciton(data), []) + False -> #(coerce_funciton(Nil), [ + DeserializeError("Function", got, []), + ]) + } +} + +@external(erlang, "glua_ffi", "coerce") +fn coerce_funciton(func: anything) -> glua.Function + /// Strictly decodes a list /// 1. first element must start with 1 /// 2. no gaps From 247b24061db07d1d50fde86f5bae808cd9839821 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:27:22 -1000 Subject: [PATCH 3/4] Create internal module --- src/glua.gleam | 120 +++++++++++++++++++---------------- src/glua/deser.gleam | 5 +- src/glua/internal/glua.gleam | 15 +++++ 3 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 src/glua/internal/glua.gleam diff --git a/src/glua.gleam b/src/glua.gleam index 8f03e8a..02c4356 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -11,9 +11,11 @@ import gleam/option import gleam/pair import gleam/result import gleam/string +import glua/deser +import glua/internal/glua as internal -/// Represents an instance of the Lua VM. -pub type Lua +pub type Lua = + internal.Lua /// Represents the errors than can happend during the parsing and execution of Lua code pub type LuaError(error) { @@ -25,8 +27,9 @@ pub type LuaError(error) { 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)) + /// The value returned by the Lua environment could not be deserialized using the provided deserializer. + UnexpectedResultType(List(deser.DeserializeError)) + UnexpectedPrivateType(List(decode.DecodeError)) /// An app-defined error CustomError(error: error) /// An error that could not be identified. @@ -136,8 +139,11 @@ pub fn format_error(error: LuaError(e)) -> String { "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") + UnexpectedResultType(deser_errs) -> + list.map(deser_errs, format_deser_error) |> string.join(with: "\n") + UnexpectedPrivateType(decode_errs) -> + list.map(decode_errs, format_decode_error) |> string.join(with: "\n") + CustomError(error) -> string.inspect(error) UnknownError(error) -> "Unknown error: " <> format_unknown_error(error) } @@ -213,6 +219,16 @@ fn format_decode_error(error: decode.DecodeError) -> String { } } +fn format_deser_error(error: deser.DeserializeError) -> String { + let base = "Expected " <> error.expected <> ", but found " <> error.found + + case error.path { + [] -> base + path -> + base <> " at " <> string.join(list.map(path, string.inspect), with: ".") + } +} + @external(erlang, "luerl_lib", "format_value") fn format_lua_value(v: anything) -> String @@ -496,7 +512,8 @@ pub fn fold( pub type Chunk /// Represents a value that can be passed to the Lua environment. -pub type Value +pub type Value = + internal.Value @external(erlang, "glua_ffi", "coerce_nil") pub fn nil() -> Value @@ -556,7 +573,8 @@ pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value { /// to encourage using `glua.error` instead since `glua.failure` wouldn't make sense in that case. pub type Never -pub type Function +pub type Function = + internal.Function pub fn function_decoder() -> decode.Decoder( fn(List(Value)) -> Action(List(Value), e), @@ -637,51 +655,9 @@ fn do_userdata(v: anything, lua: Lua) -> #(Value, Lua) @external(erlang, "glua_ffi", "wrap_fun") 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 -/// 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.run( -/// glua.new(), -/// glua.eval(code: "return 1, 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( - ref ref: Value, - using decoder: decode.Decoder(a), -) -> 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), + using decoder: deser.Deserializer(a), ) -> Action(a, e) { use ref <- then(act) dereference(ref, decoder) @@ -689,7 +665,7 @@ pub fn returning( pub fn returning_list( action act: Action(List(Value), e), - using decoder: decode.Decoder(a), + using decoder: deser.Deserializer(a), ) -> Action(List(a), e) { use refs <- then(act) fold(refs, dereference(_, decoder)) @@ -815,7 +791,7 @@ pub fn get_private( using decoder: decode.Decoder(a), ) -> Result(a, LuaError(e)) { use value <- result.try(do_get_private(lua, key)) - decode.run(value, decoder) |> result.map_error(UnexpectedResultType) + decode.run(value, decoder) |> result.map_error(UnexpectedPrivateType) } @external(erlang, "glua_ffi", "get_private") @@ -1166,3 +1142,41 @@ pub fn call_function_by_name( use fun <- then(get(keys)) call_function(fun, args) } + +/// Converts a reference to a Lua value into type-safe Gleam data using the provided decoder. +/// +/// ## Examples +/// +/// ```gleam +/// 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.run( +/// glua.new(), +/// glua.eval(code: "return 1, 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( + ref ref: Value, + using deserializer: deser.Deserializer(a), +) -> Action(a, e) { + use state <- Action + use ret <- result.map( + deser.run(state, ref, deserializer) + |> result.map_error(UnexpectedResultType), + ) + + #(state, ret) +} diff --git a/src/glua/deser.gleam b/src/glua/deser.gleam index c74b603..3b0532d 100644 --- a/src/glua/deser.gleam +++ b/src/glua/deser.gleam @@ -12,7 +12,9 @@ import gleam/int import gleam/list import gleam/option.{type Option} import gleam/string -import glua.{type Lua, type Value} +import glua/internal/glua.{type Function, type Lua, type Value} + +// import glua.{type DeserializeError, type Lua, type Value, DeserializeError} pub opaque type Deserializer(t) { Deserializer(function: fn(Lua, Value) -> Return(t)) @@ -93,6 +95,7 @@ fn index_into( } } +@internal pub fn run( lua: Lua, data: Value, diff --git a/src/glua/internal/glua.gleam b/src/glua/internal/glua.gleam new file mode 100644 index 0000000..748782b --- /dev/null +++ b/src/glua/internal/glua.gleam @@ -0,0 +1,15 @@ +/// Represents an instance of the Lua VM. +pub type Lua + +pub type Value + +pub type Function + +@external(erlang, "glua_ffi", "coerce_nil") +pub fn nil() -> Value + +@external(erlang, "glua_ffi", "coerce") +pub fn string(v: String) -> Value + +@external(erlang, "glua_ffi", "coerce") +pub fn int(v: Int) -> Value From 2817540e11a428c47fb59d3a4803be163f2c8ff9 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:45:03 -1000 Subject: [PATCH 4/4] Add from_decoder function --- src/glua/deser.gleam | 27 +++++++++++++++++++++++++++ src/glua_ffi.erl | 6 +++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/glua/deser.gleam b/src/glua/deser.gleam index 3b0532d..9445240 100644 --- a/src/glua/deser.gleam +++ b/src/glua/deser.gleam @@ -7,6 +7,7 @@ import gleam/bit_array import gleam/bool import gleam/dict.{type Dict} import gleam/dynamic +import gleam/dynamic/decode import gleam/float import gleam/int import gleam/list @@ -27,6 +28,32 @@ pub type DeserializeError { type Return(t) = #(t, List(DeserializeError)) +pub fn from_decoder(decoder: decode.Decoder(a)) { + Deserializer(function: fn(lua, value) { + let dyn = do_dereference(lua, value) + case hijack_decoder(decoder)(dyn) { + #(ok, []) -> #(ok, []) + #(zero, errs) -> #(zero, list.map(errs, decode_to_deser_error)) + } + }) +} + +@external(erlang, "glua_ffi", "dereference") +fn do_dereference(lua: Lua, ref: Value) -> dynamic.Dynamic + +@external(erlang, "glua_ffi", "dereference") +fn hijack_decoder( + decoder: decode.Decoder(a), +) -> fn(dynamic.Dynamic) -> #(a, List(decode.DecodeError)) + +fn decode_to_deser_error(error: decode.DecodeError) { + DeserializeError( + error.expected, + error.found, + error.path |> list.map(glua.string), + ) +} + pub fn field( field_path: Value, field_decoder: Deserializer(t), diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index acf808c..cfec720 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -5,7 +5,7 @@ -export([get_stacktrace/1, dereference/2, coerce/1, coerce_nil/0, wrap_fun/1, decode_fun/1, sandbox_fun/1, get_table_keys/2, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_file/2, - eval_chunk/2, call_function/3]). + eval_chunk/2, call_function/3, hijack_decoder/1]). %% helper to convert luerl return values to a format @@ -283,3 +283,7 @@ get_private(Lua, Key) -> error:{badkey, _} -> {error, {key_not_found, [Key]}} end. + +hijack_decoder({decoder, Func}) -> + Func +