From 4409855b06b562e7f885a4e1fa56f491cbb7f5e7 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:34:22 -1000 Subject: [PATCH 01/41] Add alloc table and alloc userdata --- src/glua.gleam | 19 ++++++++++++++++--- src/glua_ffi.erl | 31 ++++++++++++++++++++++++++++--- test/glua_test.gleam | 19 +++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index cc5ae51..eab69b9 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -72,6 +72,16 @@ pub fn float(v: Float) -> Value @external(erlang, "glua_ffi", "coerce") pub fn table(values: List(#(Value, Value))) -> Value +pub fn alloc_table(lua: Lua, values: List(#(Value, Value))) -> #(Lua, Value) { + let #(val, lua) = do_alloc_table(values, lua) + #(lua, val) +} + +pub fn alloc_userdata(lua: Lua, a: anything) -> #(Lua, Value) { + let #(val, lua) = do_alloc_userdata(a, lua) + #(lua, val) +} + pub fn table_decoder( keys_decoder: decode.Decoder(a), values_decoder: decode.Decoder(b), @@ -333,7 +343,7 @@ pub fn set( Ok(_) -> Ok(#(keys, lua)) Error(KeyNotFound) -> { - let #(tbl, lua) = alloc_table([], lua) + let #(tbl, lua) = do_alloc_table([], lua) do_set(lua, keys, tbl) |> result.map(fn(lua) { #(keys, lua) }) } @@ -400,8 +410,11 @@ pub fn set_lua_paths( set(lua, ["package", "path"], paths) } -@external(erlang, "luerl_emul", "alloc_table") -fn alloc_table(content: List(a), lua: Lua) -> #(a, Lua) +@external(erlang, "luerl_heap", "alloc_table") +fn do_alloc_table(content: List(a), lua: Lua) -> #(Value, Lua) + +@external(erlang, "luerl_heap", "alloc_userdata") +fn do_alloc_userdata(a: anything, lua: Lua) -> #(Value, Lua) @external(erlang, "glua_ffi", "get_table_keys_dec") fn do_get(lua: Lua, keys: List(String)) -> Result(dynamic.Dynamic, LuaError) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1edf9e7..8e4a612 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -4,7 +4,8 @@ -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, - eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3]). + eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, + alloc/2]). %% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam maybe_process_userdata(Lst) when is_list(Lst) -> @@ -58,6 +59,21 @@ is_encoded({erl_mfa,_,_,_}) -> is_encoded(_) -> false. +encode(X, St0) -> + case is_encoded(X) of + true -> {X, St0}; + false -> luerl:encode(X, St0) + end. + +encode_list(L, St0) when is_list(L) -> + Enc = fun(X, {L1, St}) -> + {Enc, St1} = encode(X, St), + {[Enc | L1], St1} + end, + {L1, St1} = lists:foldl(Enc, {[], St0}, L), + {lists:reverse(L1), St1}. + + %% TODO: Improve compiler errors handling and try to detect more errors map_error({error, [{_, luerl_parse, Errors} | _], _}) -> FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors), @@ -97,6 +113,15 @@ coerce_nil() -> coerce_userdata(X) -> {userdata, X}. +alloc(St0, Value) when is_list(Value) -> + {Enc, St1} = luerl_heap:alloc_table(Value, St0), + {St1, Enc}; +alloc(St0, {usrdef,_}=Value) -> + {Enc, St1} = luerl_heap:alloc_userdata(Value, St0), + {St1, Enc}; +alloc(St0, Other) -> + {St0, Other}. + wrap_fun(Fun) -> fun(Args, State) -> Decoded = luerl:decode_list(Args, State), @@ -165,11 +190,11 @@ eval_file_dec(Lua, Path) -> unicode:characters_to_list(Path), Lua)). call_function(Lua, Fun, Args) -> - {EncodedArgs, State} = luerl:encode_list(Args, Lua), + {EncodedArgs, State} = encode_list(Args, Lua), to_gleam(luerl:call(Fun, EncodedArgs, State)). call_function_dec(Lua, Fun, Args) -> - {EncodedArgs, St1} = luerl:encode_list(Args, Lua), + {EncodedArgs, St1} = encode_list(Args, Lua), case luerl:call(Fun, EncodedArgs, St1) of {ok, Ret, St2} -> Values = luerl:decode_list(Ret, St2), diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 184baa2..c69acb3 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -551,3 +551,22 @@ pub fn nested_function_references_test() { glua.call_function(state: lua, ref:, args: [arg], using: decode.float) assert result == 20.0 } + +pub fn alloc_test() { + let #(lua, table) = glua.alloc_table(glua.new(), []) + let proxy = + glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) + let metatable = glua.table([#(glua.string("__index"), proxy)]) + let assert Ok(#(lua, _)) = + glua.ref_call_function_by_name(lua, ["setmetatable"], [table, metatable]) + let assert Ok(lua) = glua.set(lua, ["test_table"], table) + + let assert Ok(#(_lua, [ret1])) = + glua.eval(lua, "return test_table.any_key", decode.string) + + let assert Ok(#(_lua, [ret2])) = + glua.eval(lua, "return test_table.other_key", decode.string) + + assert ret1 == "constant" + assert ret2 == "constant" +} From 52492ac16ebc0fd58a0420cb679acf3239e5ac41 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:32:31 -1000 Subject: [PATCH 02/41] Dump gleam/dynamic/decode into de.gleam --- src/de.gleam | 372 +++++++++++++++++++++++++++++++++++++++++++++++ src/glua_ffi.erl | 2 + 2 files changed, 374 insertions(+) create mode 100644 src/de.gleam diff --git a/src/de.gleam b/src/de.gleam new file mode 100644 index 0000000..80eab1b --- /dev/null +++ b/src/de.gleam @@ -0,0 +1,372 @@ +//// 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/dynamic/decode.{type DecodeError, type Decoder} +import gleam/option.{type Option} +import glua.{type Lua, type Value, type ValueRef} + +pub opaque type Deserializer(t) { + Deserializer(function: fn(ValueRef) -> #(t, Lua, List(DeserializeError))) +} + +pub type DeserializeError { + DeserializeError(expected: String, found: String, path: List(String)) +} + +pub fn field( + field_path: glua.ValueRef, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(function: fn(data) { todo }) +} + +pub fn subfield( + field_path: List(name), + field_decoder: Decoder(t), + next: fn(t) -> Decoder(final), +) -> Decoder(final) { + todo + // Decoder(function: fn(data) { + // let #(out, errors1) = + // index(field_path, [], field_decoder.function, data, fn(data, position) { + // let #(default, _) = field_decoder.function(data) + // #(default, [DecodeError("Field", "Nothing", [])]) + // |> push_path(list.reverse(position)) + // }) + // let #(out, errors2) = next(out).function(data) + // #(out, list.append(errors1, errors2)) + // }) +} + +pub fn run( + data: ValueRef, + decoder: Decoder(t), +) -> Result(#(Lua, t), List(DecodeError)) { + todo + // let #(maybe_invalid_data, errors) = decoder.function(data) + // case errors { + // [] -> Ok(maybe_invalid_data) + // [_, ..] -> Error(errors) + // } +} + +pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) { + Decoder(function: fn(data) { + index(path, [], inner.function, data, fn(data, position) { + let #(default, _) = inner.function(data) + #(default, [DecodeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }) + }) +} + +fn index( + path: List(a), + position: List(a), + inner: fn(ValueRef) -> #(b, List(DecodeError)), + data: ValueRef, + handle_miss: fn(ValueRef, List(a)) -> #(b, List(DecodeError)), +) -> #(b, List(DecodeError)) { + todo + // case path { + // [] -> { + // data + // |> inner + // |> push_path(list.reverse(position)) + // } + // + // [key, ..path] -> { + // case bare_index(data, key) { + // Ok(Some(data)) -> { + // index(path, [key, ..position], inner, data, handle_miss) + // } + // Ok(None) -> { + // handle_miss(data, [key, ..position]) + // } + // Error(kind) -> { + // let #(default, _) = inner(data) + // #(default, [DecodeError(kind, dynamic.classify(data), [])]) + // |> push_path(list.reverse(position)) + // } + // } + // } + // } +} + +@external(erlang, "gleam_stdlib", "index") +@external(javascript, "../../gleam_stdlib.mjs", "index") +fn bare_index(data: ValueRef, key: anything) -> Result(Option(ValueRef), String) + +fn push_path( + layer: #(t, List(DecodeError)), + path: List(key), +) -> #(t, List(DecodeError)) { + todo + // let decoder = one_of(string, [int |> map(int.to_string)]) + // let path = + // list.map(path, fn(key) { + // let key = cast(key) + // case run(key, decoder) { + // Ok(key) -> key + // Error(_) -> "<" <> dynamic.classify(key) <> ">" + // } + // }) + // let errors = + // list.map(layer.1, fn(error) { + // DecodeError(..error, path: list.append(path, error.path)) + // }) + // #(layer.0, errors) +} + +pub fn success(state: Lua, data: t) -> Deserializer(t) { + Deserializer(function: fn(_) { #(data, state, []) }) +} + +pub fn de_error( + expected expected: String, + found found: ValueRef, +) -> 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: name, + default: t, + field_decoder: Decoder(t), + next: fn(t) -> Decoder(final), +) -> Decoder(final) { + todo + // Decoder(function: fn(data) { + // let #(out, errors1) = + // case bare_index(data, key) { + // Ok(Some(data)) -> field_decoder.function(data) + // Ok(None) -> #(default, []) + // Error(kind) -> #(default, [ + // DecodeError(kind, dynamic.classify(data), []), + // ]) + // } + // |> push_path([key]) + // let #(out, errors2) = next(out).function(data) + // #(out, list.append(errors1, errors2)) + // }) +} + +pub fn optionally_at( + path: List(segment), + default: a, + inner: Decoder(a), +) -> Decoder(a) { + todo + // Decoder(function: fn(data) { + // index(path, [], inner.function, data, fn(_, _) { #(default, []) }) + // }) +} + +// fn run_dynamic_function( +// data: Dynamic, +// name: String, +// f: fn(Dynamic) -> Result(t, t), +// ) -> #(t, List(DecodeError)) { +// case f(data) { +// Ok(data) -> #(data, []) +// Error(zero) -> #(zero, [DecodeError(name, dynamic.classify(data), [])]) +// } +// } + +pub const string: Decoder(String) = Decoder(decode_string) + +fn decode_string(data: Dynamic) -> #(String, List(DecodeError)) { + todo + // run_dynamic_function(data, "String", dynamic_string) +} + +pub const bool: Decoder(Bool) = Decoder(decode_bool) + +fn decode_bool(data: Dynamic) -> #(Bool, List(DecodeError)) { + todo + // case cast(True) == data { + // True -> #(True, []) + // False -> + // case cast(False) == data { + // True -> #(False, []) + // False -> #(False, decode_error("Bool", data)) + // } + // } +} + +pub const int: Decoder(Int) = Decoder(decode_int) + +fn decode_int(data: Dynamic) -> #(Int, List(DecodeError)) { + todo + // run_dynamic_function(data, "Int", dynamic_int) +} + +@external(erlang, "gleam_stdlib", "int") +@external(javascript, "../../gleam_stdlib.mjs", "int") +fn dynamic_int(data: Dynamic) -> Result(Int, Int) + +pub const float: Decoder(Float) = Decoder(decode_float) + +fn decode_float(data: Dynamic) -> #(Float, List(DecodeError)) { + run_dynamic_function(data, "Float", dynamic_float) +} + +@external(erlang, "gleam_stdlib", "float") +@external(javascript, "../../gleam_stdlib.mjs", "float") +fn dynamic_float(data: Dynamic) -> Result(Float, Float) + +pub const value_ref: Deserializer(ValueRef) = Deserializer(decode_dynamic) + +fn decode_dynamic(data: ValueRef) -> #(ValueRef, Lua, List(DecodeError)) { + #(data, []) +} + +pub const bit_array: Decoder(BitArray) = Decoder(decode_bit_array) + +fn decode_bit_array(data: Dynamic) -> #(BitArray, List(DecodeError)) { + run_dynamic_function(data, "BitArray", dynamic_bit_array) +} + +@external(erlang, "gleam_stdlib", "bit_array") +@external(javascript, "../../gleam_stdlib.mjs", "bit_array") +fn dynamic_bit_array(data: Dynamic) -> Result(BitArray, BitArray) + +pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) { + Decoder(fn(data) { + decode_list(data, inner.function, fn(p, k) { push_path(p, [k]) }, 0, []) + }) +} + +@external(erlang, "gleam_stdlib", "list") +@external(javascript, "../../gleam_stdlib.mjs", "list") +fn decode_list( + data: Dynamic, + item: fn(Dynamic) -> #(t, List(DecodeError)), + push_path: fn(#(t, List(DecodeError)), key) -> #(t, List(DecodeError)), + index: Int, + acc: List(t), +) -> #(List(t), List(DecodeError)) + +pub fn dict( + key: Decoder(key), + value: Decoder(value), +) -> Decoder(Dict(key, value)) { + Decoder(fn(data) { + case decode_dict(data) { + Error(_) -> #(dict.new(), decode_error("Dict", data)) + Ok(dict) -> + dict.fold(dict, #(dict.new(), []), fn(a, k, v) { + // 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(a, k, v, key.function, value.function) + [_, ..] -> a + } + }) + } + }) +} + +@external(erlang, "gleam_stdlib", "dict") +@external(javascript, "../../gleam_stdlib.mjs", "dict") +fn decode_dict(data: Dynamic) -> Result(Dict(Dynamic, Dynamic), Nil) + +pub fn optional(inner: Decoder(a)) -> Decoder(Option(a)) { + Decoder(function: fn(data) { + case is_null(data) { + True -> #(option.None, []) + False -> { + let #(data, errors) = inner.function(data) + #(option.Some(data), errors) + } + } + }) +} + +pub fn map(decoder: Decoder(a), transformer: fn(a) -> b) -> Decoder(b) { + Decoder(function: fn(d) { + let #(data, errors) = decoder.function(d) + #(transformer(data), errors) + }) +} + +pub fn map_errors( + decoder: Decoder(a), + transformer: fn(List(DecodeError)) -> List(DecodeError), +) -> Decoder(a) { + Decoder(function: fn(d) { + let #(data, errors) = decoder.function(d) + #(data, transformer(errors)) + }) +} + +pub fn collapse_errors(decoder: Decoder(a), name: String) -> Decoder(a) { + todo + // Decoder(function: fn(dynamic_data) { + // let #(data, errors) as layer = decoder.function(dynamic_data) + // case errors { + // [] -> layer + // [_, ..] -> #(data, decode_error(name, dynamic_data)) + // } + // }) +} + +pub fn then(decoder: Decoder(a), next: fn(a) -> Decoder(b)) -> Decoder(b) { + Decoder(function: fn(dynamic_data) { + let #(data, errors) = decoder.function(dynamic_data) + let decoder = next(data) + let #(data, _) as layer = decoder.function(dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, errors) + } + }) +} + +pub fn one_of( + first: Decoder(a), + or alternatives: List(Decoder(a)), +) -> Decoder(a) { + Decoder(function: fn(dynamic_data) { + let #(_, errors) as layer = first.function(dynamic_data) + case errors { + [] -> layer + [_, ..] -> run_decoders(dynamic_data, layer, alternatives) + } + }) +} + +fn run_decoders( + data: Dynamic, + failure: #(a, List(DecodeError)), + decoders: List(Decoder(a)), +) -> #(a, List(DecodeError)) { + case decoders { + [] -> failure + + [decoder, ..decoders] -> { + let #(_, errors) as layer = decoder.function(data) + case errors { + [] -> layer + [_, ..] -> run_decoders(data, failure, decoders) + } + } + } +} + +pub fn failure(zero: a, expected: String) -> Decoder(a) { + Decoder(function: fn(d) { #(zero, decode_error(expected, d)) }) +} + +pub fn recursive(inner: fn() -> Decoder(a)) -> Decoder(a) { + Decoder(function: fn(data) { + let decoder = inner() + decoder.function(data) + }) +} diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 8e4a612..952089c 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -32,6 +32,8 @@ to_gleam(Value) -> {error, unknown_error} end. +% TODO: Classify(Any) + %% helper to determine if a value is encoded or not %% borrowed from https://github.com/tv-labs/lua/blob/main/lib/lua/util.ex#L19-L35 is_encoded(nil) -> From 33d36c0840c86f05e96a09f65cf843b5e025712c Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:34:12 -1000 Subject: [PATCH 03/41] Add run function --- src/de.gleam | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/de.gleam b/src/de.gleam index 80eab1b..0d15d83 100644 --- a/src/de.gleam +++ b/src/de.gleam @@ -43,14 +43,13 @@ pub fn subfield( pub fn run( data: ValueRef, - decoder: Decoder(t), -) -> Result(#(Lua, t), List(DecodeError)) { - todo - // let #(maybe_invalid_data, errors) = decoder.function(data) - // case errors { - // [] -> Ok(maybe_invalid_data) - // [_, ..] -> Error(errors) - // } + deser: Deserializer(t), +) -> Result(#(Lua, t), List(DeserializeError)) { + let #(maybe_invalid_data, lua, errors) = deser.function(data) + case errors { + [] -> Ok(#(lua, maybe_invalid_data)) + [_, ..] -> Error(errors) + } } pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) { From a54b4ee7409653b85a855fdb55c948747887fdf4 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:40:21 -1000 Subject: [PATCH 04/41] Change name and remove errors The interface still isn't what it should be --- src/{de.gleam => deser.gleam} | 286 +++++++++++++++++----------------- 1 file changed, 147 insertions(+), 139 deletions(-) rename src/{de.gleam => deser.gleam} (50%) diff --git a/src/de.gleam b/src/deser.gleam similarity index 50% rename from src/de.gleam rename to src/deser.gleam index 0d15d83..ef02e9b 100644 --- a/src/de.gleam +++ b/src/deser.gleam @@ -3,16 +3,14 @@ //// 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/dynamic/decode.{type DecodeError, type Decoder} +import gleam/dict.{type Dict} +import gleam/dynamic +import gleam/dynamic/decode.{type DecodeError, type Decoder, DecodeError} import gleam/option.{type Option} import glua.{type Lua, type Value, type ValueRef} pub opaque type Deserializer(t) { - Deserializer(function: fn(ValueRef) -> #(t, Lua, List(DeserializeError))) -} - -pub type DeserializeError { - DeserializeError(expected: String, found: String, path: List(String)) + Deserializer(function: fn(ValueRef) -> #(t, Lua, List(DecodeError))) } pub fn field( @@ -25,11 +23,11 @@ pub fn field( pub fn subfield( field_path: List(name), - field_decoder: Decoder(t), - next: fn(t) -> Decoder(final), -) -> Decoder(final) { + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { todo - // Decoder(function: fn(data) { + // Deserializer(function: fn(data) { // let #(out, errors1) = // index(field_path, [], field_decoder.function, data, fn(data, position) { // let #(default, _) = field_decoder.function(data) @@ -44,7 +42,7 @@ pub fn subfield( pub fn run( data: ValueRef, deser: Deserializer(t), -) -> Result(#(Lua, t), List(DeserializeError)) { +) -> Result(#(Lua, t), List(DecodeError)) { let #(maybe_invalid_data, lua, errors) = deser.function(data) case errors { [] -> Ok(#(lua, maybe_invalid_data)) @@ -52,14 +50,15 @@ pub fn run( } } -pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) { - Decoder(function: fn(data) { - index(path, [], inner.function, data, fn(data, position) { - let #(default, _) = inner.function(data) - #(default, [DecodeError("Field", "Nothing", [])]) - |> push_path(list.reverse(position)) - }) - }) +pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { + todo + // Deserializer(function: fn(data) { + // index(path, [], inner.function, data, fn(data, position) { + // let #(default, _) = inner.function(data) + // #(default, [DecodeError("Field", "Nothing", [])]) + // |> push_path(list.reverse(position)) + // }) + // }) } fn index( @@ -127,8 +126,8 @@ pub fn success(state: Lua, data: t) -> Deserializer(t) { pub fn de_error( expected expected: String, found found: ValueRef, -) -> List(DeserializeError) { - [DeserializeError(expected: expected, found: classify(found), path: [])] +) -> List(DecodeError) { + [DecodeError(expected: expected, found: classify(found), path: [])] } @external(erlang, "glua_ffi", "classify") @@ -137,11 +136,11 @@ pub fn classify(a: anything) -> String pub fn optional_field( key: name, default: t, - field_decoder: Decoder(t), - next: fn(t) -> Decoder(final), -) -> Decoder(final) { + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { todo - // Decoder(function: fn(data) { + // Deserializer(function: fn(data) { // let #(out, errors1) = // case bare_index(data, key) { // Ok(Some(data)) -> field_decoder.function(data) @@ -159,18 +158,18 @@ pub fn optional_field( pub fn optionally_at( path: List(segment), default: a, - inner: Decoder(a), -) -> Decoder(a) { + inner: Deserializer(a), +) -> Deserializer(a) { todo - // Decoder(function: fn(data) { + // Deserializer(function: fn(data) { // index(path, [], inner.function, data, fn(_, _) { #(default, []) }) // }) } // fn run_dynamic_function( -// data: Dynamic, +// data: ValueRef, // name: String, -// f: fn(Dynamic) -> Result(t, t), +// f: fn(ValueRef) -> Result(t, t), // ) -> #(t, List(DecodeError)) { // case f(data) { // Ok(data) -> #(data, []) @@ -178,16 +177,16 @@ pub fn optionally_at( // } // } -pub const string: Decoder(String) = Decoder(decode_string) +pub const string: Deserializer(String) = Deserializer(deser_string) -fn decode_string(data: Dynamic) -> #(String, List(DecodeError)) { +fn deser_string(data: ValueRef) -> #(String, Lua, List(DecodeError)) { todo // run_dynamic_function(data, "String", dynamic_string) } -pub const bool: Decoder(Bool) = Decoder(decode_bool) +pub const bool: Deserializer(Bool) = Deserializer(deser_bool) -fn decode_bool(data: Dynamic) -> #(Bool, List(DecodeError)) { +fn deser_bool(data: ValueRef) -> #(Bool, Lua, List(DecodeError)) { todo // case cast(True) == data { // True -> #(True, []) @@ -199,115 +198,117 @@ fn decode_bool(data: Dynamic) -> #(Bool, List(DecodeError)) { // } } -pub const int: Decoder(Int) = Decoder(decode_int) +pub const int: Deserializer(Int) = Deserializer(deser_int) -fn decode_int(data: Dynamic) -> #(Int, List(DecodeError)) { +fn deser_int(data: ValueRef) -> #(Int, Lua, List(DecodeError)) { todo // run_dynamic_function(data, "Int", dynamic_int) } -@external(erlang, "gleam_stdlib", "int") -@external(javascript, "../../gleam_stdlib.mjs", "int") -fn dynamic_int(data: Dynamic) -> Result(Int, Int) - -pub const float: Decoder(Float) = Decoder(decode_float) +pub const float: Deserializer(Float) = Deserializer(deser_float) -fn decode_float(data: Dynamic) -> #(Float, List(DecodeError)) { - run_dynamic_function(data, "Float", dynamic_float) +fn deser_float(data: ValueRef) -> #(Float, Lua, List(DecodeError)) { + todo + // run_dynamic_function(data, "Float", dynamic_float) } -@external(erlang, "gleam_stdlib", "float") -@external(javascript, "../../gleam_stdlib.mjs", "float") -fn dynamic_float(data: Dynamic) -> Result(Float, Float) - pub const value_ref: Deserializer(ValueRef) = Deserializer(decode_dynamic) fn decode_dynamic(data: ValueRef) -> #(ValueRef, Lua, List(DecodeError)) { - #(data, []) + #(data, todo, []) } -pub const bit_array: Decoder(BitArray) = Decoder(decode_bit_array) +pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( + deser_user_defined, +) -fn decode_bit_array(data: Dynamic) -> #(BitArray, List(DecodeError)) { - run_dynamic_function(data, "BitArray", dynamic_bit_array) +fn deser_user_defined( + data: ValueRef, +) -> #(dynamic.Dynamic, Lua, List(DecodeError)) { + todo + // run_dynamic_function(data, "BitArray", dynamic_bit_array) } @external(erlang, "gleam_stdlib", "bit_array") @external(javascript, "../../gleam_stdlib.mjs", "bit_array") -fn dynamic_bit_array(data: Dynamic) -> Result(BitArray, BitArray) +fn dynamic_bit_array(data: ValueRef) -> Result(BitArray, BitArray) -pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) { - Decoder(fn(data) { - decode_list(data, inner.function, fn(p, k) { push_path(p, [k]) }, 0, []) - }) +pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { + todo + // Deserializer(fn(data) { + // decode_list(data, inner.function, fn(p, k) { push_path(p, [k]) }, 0, []) + // }) } @external(erlang, "gleam_stdlib", "list") @external(javascript, "../../gleam_stdlib.mjs", "list") fn decode_list( - data: Dynamic, - item: fn(Dynamic) -> #(t, List(DecodeError)), + data: ValueRef, + item: fn(ValueRef) -> #(t, List(DecodeError)), push_path: fn(#(t, List(DecodeError)), key) -> #(t, List(DecodeError)), index: Int, acc: List(t), ) -> #(List(t), List(DecodeError)) pub fn dict( - key: Decoder(key), - value: Decoder(value), -) -> Decoder(Dict(key, value)) { - Decoder(fn(data) { - case decode_dict(data) { - Error(_) -> #(dict.new(), decode_error("Dict", data)) - Ok(dict) -> - dict.fold(dict, #(dict.new(), []), fn(a, k, v) { - // 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(a, k, v, key.function, value.function) - [_, ..] -> a - } - }) - } - }) + key: Deserializer(key), + value: Deserializer(value), +) -> Deserializer(Dict(key, value)) { + todo + // Deserializer(fn(data) { + // case decode_dict(data) { + // Error(_) -> #(dict.new(), decode_error("Dict", data)) + // Ok(dict) -> + // dict.fold(dict, #(dict.new(), []), fn(a, k, v) { + // // 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(a, k, v, key.function, value.function) + // [_, ..] -> a + // } + // }) + // } + // }) } -@external(erlang, "gleam_stdlib", "dict") -@external(javascript, "../../gleam_stdlib.mjs", "dict") -fn decode_dict(data: Dynamic) -> Result(Dict(Dynamic, Dynamic), Nil) - -pub fn optional(inner: Decoder(a)) -> Decoder(Option(a)) { - Decoder(function: fn(data) { - case is_null(data) { - True -> #(option.None, []) - False -> { - let #(data, errors) = inner.function(data) - #(option.Some(data), errors) - } - } - }) +pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { + todo + // Deserializer(function: fn(data) { + // case is_null(data) { + // True -> #(option.None, []) + // False -> { + // let #(data, errors) = inner.function(data) + // #(option.Some(data), errors) + // } + // } + // }) } -pub fn map(decoder: Decoder(a), transformer: fn(a) -> b) -> Decoder(b) { - Decoder(function: fn(d) { - let #(data, errors) = decoder.function(d) - #(transformer(data), errors) - }) +pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) { + todo + // Deserializer(function: fn(d) { + // let #(data, errors) = decoder.function(d) + // #(transformer(data), errors) + // }) } pub fn map_errors( - decoder: Decoder(a), + decoder: Deserializer(a), transformer: fn(List(DecodeError)) -> List(DecodeError), -) -> Decoder(a) { - Decoder(function: fn(d) { - let #(data, errors) = decoder.function(d) - #(data, transformer(errors)) - }) +) -> Deserializer(a) { + todo + // Deserializer(function: fn(d) { + // let #(data, errors) = decoder.function(d) + // #(data, transformer(errors)) + // }) } -pub fn collapse_errors(decoder: Decoder(a), name: String) -> Decoder(a) { +pub fn collapse_errors( + decoder: Deserializer(a), + name: String, +) -> Deserializer(a) { todo - // Decoder(function: fn(dynamic_data) { + // Deserializer(function: fn(dynamic_data) { // let #(data, errors) as layer = decoder.function(dynamic_data) // case errors { // [] -> layer @@ -316,55 +317,62 @@ pub fn collapse_errors(decoder: Decoder(a), name: String) -> Decoder(a) { // }) } -pub fn then(decoder: Decoder(a), next: fn(a) -> Decoder(b)) -> Decoder(b) { - Decoder(function: fn(dynamic_data) { - let #(data, errors) = decoder.function(dynamic_data) - let decoder = next(data) - let #(data, _) as layer = decoder.function(dynamic_data) - case errors { - [] -> layer - [_, ..] -> #(data, errors) - } - }) +pub fn then( + decoder: Deserializer(a), + next: fn(a) -> Deserializer(b), +) -> Deserializer(b) { + todo + // Deserializer(function: fn(dynamic_data) { + // let #(data, errors) = decoder.function(dynamic_data) + // let decoder = next(data) + // let #(data, _) as layer = decoder.function(dynamic_data) + // case errors { + // [] -> layer + // [_, ..] -> #(data, errors) + // } + // }) } pub fn one_of( - first: Decoder(a), - or alternatives: List(Decoder(a)), -) -> Decoder(a) { - Decoder(function: fn(dynamic_data) { - let #(_, errors) as layer = first.function(dynamic_data) - case errors { - [] -> layer - [_, ..] -> run_decoders(dynamic_data, layer, alternatives) - } - }) + first: Deserializer(a), + or alternatives: List(Deserializer(a)), +) -> Deserializer(a) { + todo + // Deserializer(function: fn(dynamic_data) { + // let #(_, errors) as layer = first.function(dynamic_data) + // case errors { + // [] -> layer + // [_, ..] -> run_decoders(dynamic_data, layer, alternatives) + // } + // }) } fn run_decoders( - data: Dynamic, + data: ValueRef, failure: #(a, List(DecodeError)), - decoders: List(Decoder(a)), + decoders: List(Deserializer(a)), ) -> #(a, List(DecodeError)) { - case decoders { - [] -> failure - - [decoder, ..decoders] -> { - let #(_, errors) as layer = decoder.function(data) - case errors { - [] -> layer - [_, ..] -> run_decoders(data, failure, decoders) - } - } - } + todo + // case decoders { + // [] -> failure + // + // [decoder, ..decoders] -> { + // let #(_, errors) as layer = decoder.function(data) + // case errors { + // [] -> layer + // [_, ..] -> run_decoders(data, failure, decoders) + // } + // } + // } } -pub fn failure(zero: a, expected: String) -> Decoder(a) { - Decoder(function: fn(d) { #(zero, decode_error(expected, d)) }) +pub fn failure(zero: a, expected: String) -> Deserializer(a) { + todo + // Deserializer(function: fn(d) { #(zero, glua.new(), deser_error(expected, d)) }) } -pub fn recursive(inner: fn() -> Decoder(a)) -> Decoder(a) { - Decoder(function: fn(data) { +pub fn recursive(inner: fn() -> Deserializer(a)) -> Deserializer(a) { + Deserializer(function: fn(data) { let decoder = inner() decoder.function(data) }) From 191e948255b4a6b16bc8c7082fae9361e8c30ae5 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:41:12 -1000 Subject: [PATCH 05/41] Add classify function in erlang --- src/glua_ffi.erl | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 952089c..f759e03 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -32,10 +32,31 @@ to_gleam(Value) -> {error, unknown_error} end. -% TODO: Classify(Any) +classify(nil) -> + "Nil"; +classify(Bool) when is_boolean(Bool) -> + "Bool"; +classify(N) when is_number(N) -> + "Number"; +classify({tref,_}) -> + "Table"; +classify({usrdef,_}) -> + "UserDef"; +classify({eref,_}) -> + "Unknown"; +classify({funref,_,_}) -> + "Function"; +classify({erl_func,_}) -> + "Function"; +classify({erl_mfa,_,_,_}) -> + "Function"; +classify(_) -> + "Unknown". + %% helper to determine if a value is encoded or not -%% borrowed from https://github.com/tv-labs/lua/blob/main/lib/lua/util.ex#L19-L35 +%% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 +%% Also see (luerl 1.5.1): https://hexdocs.pm/luerl/luerl.html#t:luerldata/0 is_encoded(nil) -> true; is_encoded(true) -> @@ -50,8 +71,10 @@ is_encoded({tref,_}) -> true; is_encoded({usrdef,_}) -> true; +% Rationale: https://github.com/rvirding/luerl/blob/8756287ed2083795e7456855edf56a065a49b5aa/src/luerl.erl#L924-L939 +% has no mentions of eref is_encoded({eref,_}) -> - true; + false; is_encoded({funref,_,_}) -> true; is_encoded({erl_func,_}) -> From 4c2135355865420e018386b6cc710b65097c5651 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Mon, 29 Dec 2025 03:33:12 -1000 Subject: [PATCH 06/41] Make a custom error type --- src/deser.gleam | 55 +++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index ef02e9b..1721798 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -5,24 +5,28 @@ import gleam/dict.{type Dict} import gleam/dynamic -import gleam/dynamic/decode.{type DecodeError, type Decoder, DecodeError} +import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} import glua.{type Lua, type Value, type ValueRef} pub opaque type Deserializer(t) { - Deserializer(function: fn(ValueRef) -> #(t, Lua, List(DecodeError))) + Deserializer(function: fn(ValueRef) -> #(t, Lua, List(DeserializeError))) +} + +pub type DeserializeError { + DeserializeError(expected: String, found: String, path: List(ValueRef)) } pub fn field( - field_path: glua.ValueRef, + field_path: ValueRef, field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { - Deserializer(function: fn(data) { todo }) + subfield([field_path], field_decoder, next) } pub fn subfield( - field_path: List(name), + field_path: List(ValueRef), field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { @@ -42,7 +46,7 @@ pub fn subfield( pub fn run( data: ValueRef, deser: Deserializer(t), -) -> Result(#(Lua, t), List(DecodeError)) { +) -> Result(#(Lua, t), List(DeserializeError)) { let #(maybe_invalid_data, lua, errors) = deser.function(data) case errors { [] -> Ok(#(lua, maybe_invalid_data)) @@ -64,10 +68,10 @@ pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { fn index( path: List(a), position: List(a), - inner: fn(ValueRef) -> #(b, List(DecodeError)), + inner: fn(ValueRef) -> #(b, List(DeserializeError)), data: ValueRef, - handle_miss: fn(ValueRef, List(a)) -> #(b, List(DecodeError)), -) -> #(b, List(DecodeError)) { + handle_miss: fn(ValueRef, List(a)) -> #(b, List(DeserializeError)), +) -> #(b, List(DeserializeError)) { todo // case path { // [] -> { @@ -99,9 +103,9 @@ fn index( fn bare_index(data: ValueRef, key: anything) -> Result(Option(ValueRef), String) fn push_path( - layer: #(t, List(DecodeError)), + layer: #(t, List(DeserializeError)), path: List(key), -) -> #(t, List(DecodeError)) { +) -> #(t, List(DeserializeError)) { todo // let decoder = one_of(string, [int |> map(int.to_string)]) // let path = @@ -126,8 +130,8 @@ pub fn success(state: Lua, data: t) -> Deserializer(t) { pub fn de_error( expected expected: String, found found: ValueRef, -) -> List(DecodeError) { - [DecodeError(expected: expected, found: classify(found), path: [])] +) -> List(DeserializeError) { + [DeserializeError(expected: expected, found: classify(found), path: [])] } @external(erlang, "glua_ffi", "classify") @@ -179,14 +183,14 @@ pub fn optionally_at( pub const string: Deserializer(String) = Deserializer(deser_string) -fn deser_string(data: ValueRef) -> #(String, Lua, List(DecodeError)) { +fn deser_string(data: ValueRef) -> #(String, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "String", dynamic_string) } pub const bool: Deserializer(Bool) = Deserializer(deser_bool) -fn deser_bool(data: ValueRef) -> #(Bool, Lua, List(DecodeError)) { +fn deser_bool(data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { todo // case cast(True) == data { // True -> #(True, []) @@ -200,21 +204,21 @@ fn deser_bool(data: ValueRef) -> #(Bool, Lua, List(DecodeError)) { pub const int: Deserializer(Int) = Deserializer(deser_int) -fn deser_int(data: ValueRef) -> #(Int, Lua, List(DecodeError)) { +fn deser_int(data: ValueRef) -> #(Int, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "Int", dynamic_int) } pub const float: Deserializer(Float) = Deserializer(deser_float) -fn deser_float(data: ValueRef) -> #(Float, Lua, List(DecodeError)) { +fn deser_float(data: ValueRef) -> #(Float, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "Float", dynamic_float) } pub const value_ref: Deserializer(ValueRef) = Deserializer(decode_dynamic) -fn decode_dynamic(data: ValueRef) -> #(ValueRef, Lua, List(DecodeError)) { +fn decode_dynamic(data: ValueRef) -> #(ValueRef, Lua, List(DeserializeError)) { #(data, todo, []) } @@ -224,7 +228,7 @@ pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( fn deser_user_defined( data: ValueRef, -) -> #(dynamic.Dynamic, Lua, List(DecodeError)) { +) -> #(dynamic.Dynamic, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "BitArray", dynamic_bit_array) } @@ -244,11 +248,12 @@ pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { @external(javascript, "../../gleam_stdlib.mjs", "list") fn decode_list( data: ValueRef, - item: fn(ValueRef) -> #(t, List(DecodeError)), - push_path: fn(#(t, List(DecodeError)), key) -> #(t, List(DecodeError)), + item: fn(ValueRef) -> #(t, List(DeserializeError)), + push_path: fn(#(t, List(DeserializeError)), key) -> + #(t, List(DeserializeError)), index: Int, acc: List(t), -) -> #(List(t), List(DecodeError)) +) -> #(List(t), List(DeserializeError)) pub fn dict( key: Deserializer(key), @@ -294,7 +299,7 @@ pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) pub fn map_errors( decoder: Deserializer(a), - transformer: fn(List(DecodeError)) -> List(DecodeError), + transformer: fn(List(DeserializeError)) -> List(DeserializeError), ) -> Deserializer(a) { todo // Deserializer(function: fn(d) { @@ -349,9 +354,9 @@ pub fn one_of( fn run_decoders( data: ValueRef, - failure: #(a, List(DecodeError)), + failure: #(a, List(DeserializeError)), decoders: List(Deserializer(a)), -) -> #(a, List(DecodeError)) { +) -> #(a, List(DeserializeError)) { todo // case decoders { // [] -> failure From 4ad3aae03f4a7b41852d7e8b38ee843465c2526b Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:42:26 -1000 Subject: [PATCH 07/41] Remove decoding functions --- src/glua.gleam | 321 ++---------- src/glua_ffi.erl | 3 + test/glua_test.gleam | 1134 +++++++++++++++++++++--------------------- 3 files changed, 604 insertions(+), 854 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index eab69b9..113366f 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -19,8 +19,6 @@ pub type LuaError { LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua) /// A certain key was not found in the Lua environment. KeyNotFound - /// The value returned by the Lua environment could not be decoded using the provided decoder. - UnexpectedResultType(List(decode.DecodeError)) /// An error that could not be identified. UnknownError } @@ -218,49 +216,6 @@ pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) @external(erlang, "glua_ffi", "sandbox_fun") fn sandbox_fun(msg: String) -> Value -/// Gets a value in the Lua environment. -/// -/// ## Examples -/// -/// ```gleam -/// glua.get(state: glua.new(), keys: ["_VERSION"], using: decode.string) -/// // -> Ok("Lua 5.3") -/// ``` -/// -/// ```gleam -/// let #(lua, encoded) = glua.new() |> glua.bool(True) -/// let assert Ok(lua) = glua.set( -/// state: lua, -/// keys: ["my_table", "my_value"], -/// value: encoded -/// ) -/// -/// glua.get( -/// state: lua, -/// keys: ["my_table", "my_value"], -/// using: decode.bool -/// ) -/// // -> Ok(True) -/// ``` -/// -/// ```gleam -/// glua.get(state: glua.new(), keys: ["non_existent"], using: decode.string) -/// // -> Error(glua.KeyNotFound) -/// ``` -pub fn get( - state lua: Lua, - keys keys: List(String), - using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { - use value <- result.try(do_get(lua, keys)) - - use decoded <- result.try( - decode.run(value, decoder) |> result.map_error(UnexpectedResultType), - ) - - Ok(decoded) -} - /// Gets a private value that is not exposed to the Lua runtime. /// /// ## Examples @@ -274,22 +229,17 @@ pub fn get( pub fn get_private( state lua: Lua, key key: String, - using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { +) -> Result(dynamic.Dynamic, LuaError) { use value <- result.try(do_get_private(lua, key)) - use decoded <- result.try( - decode.run(value, decoder) |> result.map_error(UnexpectedResultType), - ) - - Ok(decoded) + Ok(value) } /// Same as `glua.get`, but returns a reference to the value instead of decoding it -pub fn ref_get( +pub fn get( state lua: Lua, keys keys: List(String), ) -> Result(ValueRef, LuaError) { - do_ref_get(lua, keys) + do_get(lua, keys) } /// Sets a value in the Lua environment. @@ -339,7 +289,7 @@ pub fn set( use acc, key <- list.try_fold(keys, #([], lua)) let #(keys, lua) = acc let keys = list.append(keys, [key]) - case do_ref_get(lua, keys) { + case do_get(lua, keys) { Ok(_) -> Ok(#(keys, lua)) Error(KeyNotFound) -> { @@ -416,14 +366,11 @@ fn do_alloc_table(content: List(a), lua: Lua) -> #(Value, Lua) @external(erlang, "luerl_heap", "alloc_userdata") fn do_alloc_userdata(a: anything, lua: Lua) -> #(Value, Lua) -@external(erlang, "glua_ffi", "get_table_keys_dec") -fn do_get(lua: Lua, keys: List(String)) -> Result(dynamic.Dynamic, LuaError) - @external(erlang, "glua_ffi", "get_private") fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) @external(erlang, "glua_ffi", "get_table_keys") -fn do_ref_get(lua: Lua, keys: List(String)) -> Result(ValueRef, LuaError) +fn do_get(lua: Lua, keys: List(String)) -> Result(ValueRef, LuaError) @external(erlang, "glua_ffi", "set_table_keys") fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) @@ -476,292 +423,92 @@ pub fn load_file( @external(erlang, "glua_ffi", "load_file") fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError) -/// Evaluates a string of Lua code. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(#(_, results)) = glua.eval( -/// state: glua.new(), -/// code: "return 1 + 2", -/// using: decode.int -/// ) -/// assert results == [3] -/// ``` -/// -/// ```gleam -/// let my_decoder = decode.one_of(decode.string, or: [ -/// decode.int |> decode.map(int.to_string) -/// ]) -/// -/// let assert Ok(#(_, results)) = glua.eval( -/// state: glua.new(), -/// code: "return 'hello, world!', 10", -/// using: my_decoder -/// ) -/// assert results == ["hello, world!", "10"] -/// ``` -/// -/// ```gleam -/// glua.eval(state: glua.new(), code: "return 1 * ", using: decode.int) -/// // -> Error(glua.LuaCompilerException( -/// messages: ["syntax error before: ", "1"] -/// )) -/// ``` -/// -/// ```gleam -/// glua.eval(state: glua.new(), code: "return 'Hello, world!'", using: decode.int) -/// // -> Error(glua.UnexpectedResultType( -/// [decode.DecodeError("Int", "String", [])] -/// )) -/// ``` -/// -/// > **Note**: If you are evaluating the same piece of code multiple times, -/// > 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` or `glua.ref_eval_chunk`. -pub fn eval( - state lua: Lua, - code code: String, - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_eval(lua, code)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} - -@external(erlang, "glua_ffi", "eval_dec") -fn do_eval( - lua: Lua, - code: String, -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) - /// Same as `glua.eval`, but returns references to the values instead of decode them -pub fn ref_eval( +pub fn eval( state lua: Lua, code code: String, ) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_eval(lua, code) + do_eval(lua, code) } @external(erlang, "glua_ffi", "eval") -fn do_ref_eval( - lua: Lua, - code: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) - -/// Evaluates a compiled chunk of Lua code. -/// -/// ## Examples -/// ```gleam -/// let assert Ok(#(lua, chunk)) = glua.load( -/// state: glua.new(), -/// code: "return 'hello, world!'" -/// ) -/// -/// let assert Ok(#(_, results)) = glua.eval_chunk( -/// state: lua, -/// chunk:, -/// using: decode.string -/// ) -/// -/// assert results == ["hello, world!"] -/// ``` -pub fn eval_chunk( - state lua: Lua, - chunk chunk: Chunk, - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_eval_chunk(lua, chunk)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} - -@external(erlang, "glua_ffi", "eval_chunk_dec") -fn do_eval_chunk( - lua: Lua, - chunk: Chunk, -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) +fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(ValueRef)), LuaError) /// Same as `glua.eval_chunk`, but returns references to the values instead of decode them -pub fn ref_eval_chunk( +pub fn eval_chunk( state lua: Lua, chunk chunk: Chunk, ) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_eval_chunk(lua, chunk) + do_eval_chunk(lua, chunk) } @external(erlang, "glua_ffi", "eval_chunk") -fn do_ref_eval_chunk( +fn do_eval_chunk( lua: Lua, chunk: Chunk, ) -> Result(#(Lua, List(ValueRef)), LuaError) -/// Evaluates a Lua source file. -/// -/// ## Examples -/// ```gleam -/// let assert Ok(#(_, results)) = glua.eval_file( -/// state: glua.new(), -/// path: "path/to/hello.lua", -/// using: decode.string -/// ) -/// -/// assert results == ["hello, world!"] -/// ``` -pub fn eval_file( - state lua: Lua, - path path: String, - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_eval_file(lua, path)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} - -@external(erlang, "glua_ffi", "eval_file_dec") -fn do_eval_file( - lua: Lua, - path: String, -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) - /// Same as `glua.eval_file`, but returns references to the values instead of decode them. -pub fn ref_eval_file( +pub fn eval_file( state lua: Lua, path path: String, ) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_eval_file(lua, path) + do_eval_file(lua, path) } @external(erlang, "glua_ffi", "eval_file") -fn do_ref_eval_file( +fn do_eval_file( lua: Lua, path: String, ) -> Result(#(Lua, List(ValueRef)), LuaError) -/// Calls a Lua function by reference. -/// -/// ## Examples -/// ```gleam -/// let assert Ok(#(lua, fun)) = glua.ref_eval(state: glua.new(), code: "return math.sqrt") -/// -/// let #(lua, encoded) = glua.int(lua, 81) -/// let assert Ok(#(_, [result])) = glua.call_function( -/// state: lua, -/// ref: fun, -/// args: [encoded], -/// using: decode.int -/// ) -/// -/// assert result == 9 -/// ``` -/// -/// ```gleam -/// let code = "function fib(n) -/// if n <= 1 then -/// return n -/// else -/// return fib(n - 1) + fib(n - 2) -/// end -/// end -/// -/// return fib -/// " -/// let assert Ok(#(lua, fun)) = glua.ref_eval(state: glua.new(), code:) -/// -/// let #(lua, encoded) = glua.int(lua, 10) -/// let assert Ok(#(_, [result])) = glua.call_function( -/// state: lua, -/// ref: fun, -/// args: [encoded], -/// using: decode.int -/// ) -/// -/// assert result == 55 -/// ``` +/// Same as `glua.call_function`, but returns references to the values instead of decode them. pub fn call_function( state lua: Lua, ref fun: ValueRef, args args: List(Value), - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_call_function(lua, fun, args)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) +) -> Result(#(Lua, List(ValueRef)), LuaError) { + do_call_function(lua, fun, args) } -@external(erlang, "glua_ffi", "call_function_dec") +@external(erlang, "glua_ffi", "call_function") fn do_call_function( lua: Lua, fun: ValueRef, args: List(Value), -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) +) -> Result(#(Lua, List(ValueRef)), LuaError) -/// Same as `glua.call_function`, but returns references to the values instead of decode them. -pub fn ref_call_function( +/// Same as `glua.call_function_by_name`, but it chains `glua.ref_get` with `glua.ref_call_function` instead of `glua.call_function` +pub fn call_function_by_name( state lua: Lua, - ref fun: ValueRef, + keys keys: List(String), args args: List(Value), ) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_call_function(lua, fun, args) + use fun <- result.try(get(lua, keys)) + call_function(lua, fun, args) } -@external(erlang, "glua_ffi", "call_function") +@external(erlang, "glua_ffi", "ref_call_function") fn do_ref_call_function( lua: Lua, fun: ValueRef, - args: List(Value), + args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) -/// Gets a reference to the function at `keys`, then inmediatly calls it with the provided `args`. -/// -/// This is a shorthand for `glua.ref_get` followed by `glua.call_function`. -/// -/// ## Examples -/// -/// ```gleam -/// let #(lua, encoded) = glua.new() |> glua.string("hello from gleam!") -/// let assert Ok(#(_, [s])) = glua.call_function_by_name( -/// state: lua, -/// keys: ["string", "upper"], -/// args: [encoded], -/// using: decode.string -/// ) -/// -/// assert s == "HELLO FROM GLEAM!" -/// ``` -pub fn call_function_by_name( +pub fn ref_call_function( state lua: Lua, - keys keys: List(String), - args args: List(Value), - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use fun <- result.try(ref_get(lua, keys)) - call_function(lua, fun, args, decoder) + ref fun: ValueRef, + args args: List(ValueRef), +) -> Result(#(Lua, List(ValueRef)), LuaError) { + do_ref_call_function(lua, fun, args) } /// Same as `glua.call_function_by_name`, but it chains `glua.ref_get` with `glua.ref_call_function` instead of `glua.call_function` pub fn ref_call_function_by_name( state lua: Lua, keys keys: List(String), - args args: List(Value), + args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { - use fun <- result.try(ref_get(lua, keys)) + use fun <- result.try(get(lua, keys)) ref_call_function(lua, fun, args) } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index f759e03..73afb31 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -218,6 +218,9 @@ call_function(Lua, Fun, Args) -> {EncodedArgs, State} = encode_list(Args, Lua), to_gleam(luerl:call(Fun, EncodedArgs, State)). +ref_call_function(Lua, Func, Args) -> + to_gleam(luerl:call(Fun, Args, Lua)). + call_function_dec(Lua, Fun, Args) -> {EncodedArgs, St1} = encode_list(Args, Lua), case luerl:call(Fun, EncodedArgs, St1) of diff --git a/test/glua_test.gleam b/test/glua_test.gleam index c69acb3..f0579d6 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,572 +1,572 @@ -import gleam/dict -import gleam/dynamic -import gleam/dynamic/decode -import gleam/int -import gleam/list -import gleam/option -import gleam/pair +// import gleam/dict +// import gleam/dynamic +// import gleam/dynamic/decode +// import gleam/int +// import gleam/list +// import gleam/option +// import gleam/pair import gleeunit -import glua + +// import glua pub fn main() -> Nil { gleeunit.main() } - -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 table = - glua.table( - my_table - |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), - ) - #(lua, [table]) - }) - - let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) - let assert Ok(#(_lua, [table])) = - glua.call_function_by_name( - lua, - ["cool_numbers"], - [], - using: glua.table_decoder(decode.string, decode.int), - ) - - assert dict.from_list(table) == dict.from_list(my_table) -} - -pub fn sandbox_test() { - let assert Ok(lua) = glua.sandbox(glua.new(), ["math", "max"]) - 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:, - using: decode.int, - ) - - assert exception == glua.ErrorCall(["math.max is sandboxed"]) - - let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) - - let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = - glua.eval( - state: lua, - code: "return string.upper('my_string')", - using: decode.string, - ) - - assert name == "upper" - - let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) - - let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.ref_eval( - state: lua, - code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", - ) - - assert exception == glua.ErrorCall(["os.execute is sandboxed"]) - - 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], - using: decode.string, - ) - - assert exception == glua.ErrorCall(["print is sandboxed"]) -} - -pub fn new_sandboxed_test() { - let assert Ok(lua) = glua.new_sandboxed([]) - - let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.ref_eval(state: lua, code: "return load(\"return 1\")") - - assert exception == glua.ErrorCall(["load is sandboxed"]) - - let arg = glua.int(1) - let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.ref_call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) - - assert exception == glua.ErrorCall(["os.exit is sandboxed"]) - - let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = - glua.ref_eval(state: lua, code: "io.write('some_message')") - - assert name == "write" - - let assert Ok(lua) = glua.new_sandboxed([["package"], ["require"]]) - let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) - - let code = "local s = require 'example'; return s" - let assert Ok(#(_, [result])) = - glua.eval(state: lua, code:, using: decode.string) - - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -} - -pub fn encoding_and_decoding_nested_tables_test() { - let nested_table = [ - #( - glua.string("key"), - glua.table([ - #( - glua.int(1), - glua.table([#(glua.string("deeper_key"), glua.string("deeper_value"))]), - ), - ]), - ), - ] - - let keys = ["my_nested_table"] - - let nested_table_decoder = - glua.table_decoder( - decode.string, - glua.table_decoder( - decode.int, - glua.table_decoder(decode.string, decode.string), - ), - ) - let tbl = glua.table(nested_table) - - let assert Ok(lua) = glua.set(state: glua.new(), keys:, value: tbl) - - let assert Ok(result) = - glua.get(state: lua, keys:, using: nested_table_decoder) - - assert result == [#("key", [#(1, [#("deeper_key", "deeper_value")])])] -} - -pub type Userdata { - Userdata(foo: String, bar: Int) -} - -pub fn userdata_test() { - let lua = glua.new() - let 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:)) - } - - let assert Ok(lua) = glua.set(lua, ["my_userdata"], glua.userdata(userdata)) - let assert Ok(#(lua, [result])) = - glua.eval(lua, "return my_userdata", userdata_decoder) - - assert result == userdata - - let userdata = Userdata("other_userdata", 2) - let assert Ok(lua) = - glua.set(lua, ["my_other_userdata"], glua.userdata(userdata)) - let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(value, index), _)) = - glua.eval(lua, "return my_other_userdata.foo", decode.string) - - assert value == "{usdref,1}" - assert index == "foo" -} - -pub fn get_test() { - let state = glua.new() - - let assert Ok(pi) = - glua.get(state: state, keys: ["math", "pi"], using: decode.float) - - 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(ret) = glua.get(state:, keys:, using: decode.bool) - - assert ret == True - - let code = - " - my_value = 10 - return 'ignored' -" - let assert Ok(#(state, _)) = - glua.new() |> glua.eval(code:, using: decode.string) - let assert Ok(ret) = glua.get(state:, keys: ["my_value"], using: decode.int) - - assert ret == 10 -} - -pub fn get_returns_proper_errors_test() { - let state = glua.new() - - assert glua.get(state:, keys: ["non_existent_global"], using: decode.string) - == Error(glua.KeyNotFound) - - let encoded = glua.int(10) - let assert Ok(state) = - glua.set(state:, keys: ["my_table", "some_value"], value: encoded) - - assert glua.get(state:, keys: ["my_table", "my_val"], using: decode.int) - == Error(glua.KeyNotFound) -} - -pub fn set_test() { - let encoded = glua.string("custom version") - - let assert Ok(lua) = - glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) - let assert Ok(result) = - glua.get(state: lua, keys: ["_VERSION"], using: decode.string) - - assert result == "custom version" - - let numbers = - [2, 4, 7, 12] - |> list.index_map(fn(n, i) { #(i + 1, n * n) }) - - let keys = ["math", "squares"] - - let encoded = - glua.table( - numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), - ) - let assert Ok(lua) = glua.set(lua, keys, encoded) - - assert glua.get(lua, keys, glua.table_decoder(decode.int, decode.int)) - == Ok([#(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, glua.table_decoder(decode.int, decode.int)) - - let count = - list.map(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 arg = - glua.table( - list.index_map(list.range(1, 10), fn(i, n) { - #(glua.int(i + 1), glua.int(n)) - }), - ) - - let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["count_odd"], - args: [arg], - using: decode.int, - ) - - assert result == 5 - - let tbl = - glua.table([ - #( - 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)) - }), - ), - #( - 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) - - let assert Ok(lua) = glua.set(state: lua, keys: ["my_functions"], value: tbl) - - let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["my_functions", "is_even"], - args: [arg], - using: decode.bool, - ) - - assert result == True - - let assert Ok(#(_, [result])) = - glua.eval( - state: lua, - code: "return my_functions.is_odd(4)", - using: decode.bool, - ) - - assert result == False -} - -pub fn set_lua_paths_test() { - let assert Ok(state) = - glua.set_lua_paths(state: glua.new(), paths: ["./test/lua/?.lua"]) - - let code = "local s = require 'example'; return s" - - let assert Ok(#(_, [result])) = glua.eval(state:, code:, using: decode.string) - - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -} - -pub fn get_private_test() { - assert glua.new() - |> glua.set_private("test", [1, 2, 3]) - |> glua.get_private("test", using: decode.list(decode.int)) - == Ok([1, 2, 3]) - - assert glua.new() - |> glua.get_private("non_existent", using: decode.string) - == Error(glua.KeyNotFound) -} - -pub fn delete_private_test() { - let lua = glua.set_private(glua.new(), "the_value", "that_will_be_deleted") - - assert glua.get_private(lua, "the_value", using: decode.string) - == Ok("that_will_be_deleted") - - assert glua.delete_private(lua, "the_value") - |> glua.get_private(key: "the_value", using: decode.string) - == Error(glua.KeyNotFound) -} - -pub fn load_test() { - let assert Ok(#(lua, chunk)) = - glua.load(state: glua.new(), code: "return 5 * 5") - let assert Ok(#(_, [result])) = - glua.eval_chunk(state: lua, chunk:, using: decode.int) - - assert result == 25 -} - -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(#(_, [result])) = - glua.eval_chunk(state: lua, chunk:, using: decode.string) - - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -} - -pub fn eval_test() { - let assert Ok(#(lua, [result])) = - glua.eval( - state: glua.new(), - code: "return 'hello, ' .. 'world!'", - using: decode.string, - ) - - assert result == "hello, world!" - - let assert Ok(#(_, results)) = - glua.eval(state: lua, code: "return 2 + 2, 3 - 1", using: decode.int) - - assert results == [4, 2] -} - -pub fn eval_returns_proper_errors_test() { - let state = glua.new() - - assert glua.eval(state:, code: "if true then 1 + ", using: decode.int) - == Error( - glua.LuaCompilerException(messages: ["syntax error before: ", "1"]), - ) - - assert glua.eval(state:, code: "return 'Hello from Lua!'", using: decode.int) - == Error( - glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), - ) - - let assert Error(glua.LuaRuntimeException( - exception: glua.IllegalIndex(value:, index:), - state: _, - )) = glua.eval(state:, code: "return a.b", using: decode.int) - - assert value == "nil" - assert index == "b" - - let assert Error(glua.LuaRuntimeException( - exception: glua.ErrorCall(messages:), - state: _, - )) = glua.eval(state:, code: "error('error message')", using: decode.int) - - assert messages == ["error message"] - - let assert Error(glua.LuaRuntimeException( - exception: glua.UndefinedFunction(value:), - state: _, - )) = glua.eval(state:, code: "local a = 5; a()", using: decode.int) - - assert value == "5" - let assert Error(glua.LuaRuntimeException( - exception: glua.BadArith(operator:, args:), - state: _, - )) = glua.eval(state:, code: "return 10 / 0", using: decode.int) - - assert operator == "/" - assert args == ["10", "0"] - - let assert Error(glua.LuaRuntimeException( - exception: glua.AssertError(message:), - state: _, - )) = - glua.eval( - state:, - code: "assert(1 == 2, 'assertion failed')", - using: decode.int, - ) - - assert message == "assertion failed" -} - -pub fn eval_file_test() { - let assert Ok(#(_, [result])) = - glua.eval_file( - state: glua.new(), - path: "./test/lua/example.lua", - using: decode.string, - ) - - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -} - -pub fn call_function_test() { - let assert Ok(#(lua, [fun])) = - glua.ref_eval(state: glua.new(), code: "return string.reverse") - - let encoded = glua.string("auL") - - let assert Ok(#(lua, [result])) = - glua.call_function( - state: lua, - ref: fun, - args: [encoded], - using: decode.string, - ) - - assert result == "Lua" - - let assert Ok(#(lua, [fun])) = - glua.ref_eval(state: lua, code: "return function(a, b) return a .. b end") - - let args = list.map(["Lua in ", "Gleam"], glua.string) - - let assert Ok(#(_, [result])) = - glua.call_function(state: lua, ref: fun, args:, using: decode.string) - - assert result == "Lua in Gleam" -} - -pub fn call_function_returns_proper_errors_test() { - let state = glua.new() - - let assert Ok(#(state, [ref])) = - glua.ref_eval(state:, code: "return string.upper") - - let arg = glua.string("Hello from Gleam!") - - assert glua.call_function(state:, ref:, args: [arg], using: decode.int) - == Error( - glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), - ) - - let assert Ok(#(lua, [ref])) = glua.ref_eval(state:, code: "return 1") - - let assert Error(glua.LuaRuntimeException( - exception: glua.UndefinedFunction(value:), - state: _, - )) = glua.call_function(state: lua, ref:, args: [], using: decode.string) - - assert value == "1" -} - -pub fn call_function_by_name_test() { - let args = list.map([20, 10], glua.int) - let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: glua.new(), - keys: ["math", "max"], - args:, - using: decode.int, - ) - - assert result == 20 - - let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["math", "min"], - args:, - using: decode.int, - ) - - assert result == 10 - - let arg = glua.float(10.2) - let assert Ok(#(_, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["math", "type"], - args: [arg], - using: decode.optional(decode.string), - ) - - assert result == option.Some("float") -} - -pub fn nested_function_references_test() { - let code = "return function() return math.sqrt end" - - let assert Ok(#(lua, [ref])) = glua.ref_eval(state: glua.new(), code:) - let assert Ok(#(lua, [ref])) = - glua.ref_call_function(state: lua, ref:, args: []) - - let arg = glua.int(400) - let assert Ok(#(_, [result])) = - glua.call_function(state: lua, ref:, args: [arg], using: decode.float) - assert result == 20.0 -} - -pub fn alloc_test() { - let #(lua, table) = glua.alloc_table(glua.new(), []) - let proxy = - glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) - let metatable = glua.table([#(glua.string("__index"), proxy)]) - let assert Ok(#(lua, _)) = - glua.ref_call_function_by_name(lua, ["setmetatable"], [table, metatable]) - let assert Ok(lua) = glua.set(lua, ["test_table"], table) - - let assert Ok(#(_lua, [ret1])) = - glua.eval(lua, "return test_table.any_key", decode.string) - - let assert Ok(#(_lua, [ret2])) = - glua.eval(lua, "return test_table.other_key", decode.string) - - assert ret1 == "constant" - assert ret2 == "constant" -} +// 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 table = +// glua.table( +// my_table +// |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), +// ) +// #(lua, [table]) +// }) +// +// let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) +// let assert Ok(#(_lua, [table])) = +// glua.call_function_by_name( +// lua, +// ["cool_numbers"], +// [], +// using: glua.table_decoder(decode.string, decode.int), +// ) +// +// assert dict.from_list(table) == dict.from_list(my_table) +// } +// +// pub fn sandbox_test() { +// let assert Ok(lua) = glua.sandbox(glua.new(), ["math", "max"]) +// 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:, +// using: decode.int, +// ) +// +// assert exception == glua.ErrorCall(["math.max is sandboxed"]) +// +// let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) +// +// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = +// glua.eval( +// state: lua, +// code: "return string.upper('my_string')", +// using: decode.string, +// ) +// +// assert name == "upper" +// +// let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) +// +// let assert Error(glua.LuaRuntimeException(exception, _)) = +// glua.ref_eval( +// state: lua, +// code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", +// ) +// +// assert exception == glua.ErrorCall(["os.execute is sandboxed"]) +// +// 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], +// using: decode.string, +// ) +// +// assert exception == glua.ErrorCall(["print is sandboxed"]) +// } +// +// pub fn new_sandboxed_test() { +// let assert Ok(lua) = glua.new_sandboxed([]) +// +// let assert Error(glua.LuaRuntimeException(exception, _)) = +// glua.ref_eval(state: lua, code: "return load(\"return 1\")") +// +// assert exception == glua.ErrorCall(["load is sandboxed"]) +// +// let arg = glua.int(1) +// let assert Error(glua.LuaRuntimeException(exception, _)) = +// glua.ref_call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) +// +// assert exception == glua.ErrorCall(["os.exit is sandboxed"]) +// +// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = +// glua.ref_eval(state: lua, code: "io.write('some_message')") +// +// assert name == "write" +// +// let assert Ok(lua) = glua.new_sandboxed([["package"], ["require"]]) +// let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) +// +// let code = "local s = require 'example'; return s" +// let assert Ok(#(_, [result])) = +// glua.eval(state: lua, code:, using: decode.string) +// +// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" +// } +// +// pub fn encoding_and_decoding_nested_tables_test() { +// let nested_table = [ +// #( +// glua.string("key"), +// glua.table([ +// #( +// glua.int(1), +// glua.table([#(glua.string("deeper_key"), glua.string("deeper_value"))]), +// ), +// ]), +// ), +// ] +// +// let keys = ["my_nested_table"] +// +// let nested_table_decoder = +// glua.table_decoder( +// decode.string, +// glua.table_decoder( +// decode.int, +// glua.table_decoder(decode.string, decode.string), +// ), +// ) +// let tbl = glua.table(nested_table) +// +// let assert Ok(lua) = glua.set(state: glua.new(), keys:, value: tbl) +// +// let assert Ok(result) = +// glua.get(state: lua, keys:, using: nested_table_decoder) +// +// assert result == [#("key", [#(1, [#("deeper_key", "deeper_value")])])] +// } +// +// pub type Userdata { +// Userdata(foo: String, bar: Int) +// } +// +// pub fn userdata_test() { +// let lua = glua.new() +// let 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:)) +// } +// +// let assert Ok(lua) = glua.set(lua, ["my_userdata"], glua.userdata(userdata)) +// let assert Ok(#(lua, [result])) = +// glua.eval(lua, "return my_userdata", userdata_decoder) +// +// assert result == userdata +// +// let userdata = Userdata("other_userdata", 2) +// let assert Ok(lua) = +// glua.set(lua, ["my_other_userdata"], glua.userdata(userdata)) +// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(value, index), _)) = +// glua.eval(lua, "return my_other_userdata.foo", decode.string) +// +// assert value == "{usdref,1}" +// assert index == "foo" +// } +// +// pub fn get_test() { +// let state = glua.new() +// +// let assert Ok(pi) = +// glua.get(state: state, keys: ["math", "pi"], using: decode.float) +// +// 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(ret) = glua.get(state:, keys:, using: decode.bool) +// +// assert ret == True +// +// let code = +// " +// my_value = 10 +// return 'ignored' +// " +// let assert Ok(#(state, _)) = +// glua.new() |> glua.eval(code:, using: decode.string) +// let assert Ok(ret) = glua.get(state:, keys: ["my_value"], using: decode.int) +// +// assert ret == 10 +// } +// +// pub fn get_returns_proper_errors_test() { +// let state = glua.new() +// +// assert glua.get(state:, keys: ["non_existent_global"], using: decode.string) +// == Error(glua.KeyNotFound) +// +// let encoded = glua.int(10) +// let assert Ok(state) = +// glua.set(state:, keys: ["my_table", "some_value"], value: encoded) +// +// assert glua.get(state:, keys: ["my_table", "my_val"], using: decode.int) +// == Error(glua.KeyNotFound) +// } +// +// pub fn set_test() { +// let encoded = glua.string("custom version") +// +// let assert Ok(lua) = +// glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) +// let assert Ok(result) = +// glua.get(state: lua, keys: ["_VERSION"], using: decode.string) +// +// assert result == "custom version" +// +// let numbers = +// [2, 4, 7, 12] +// |> list.index_map(fn(n, i) { #(i + 1, n * n) }) +// +// let keys = ["math", "squares"] +// +// let encoded = +// glua.table( +// numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), +// ) +// let assert Ok(lua) = glua.set(lua, keys, encoded) +// +// assert glua.get(lua, keys, glua.table_decoder(decode.int, decode.int)) +// == Ok([#(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, glua.table_decoder(decode.int, decode.int)) +// +// let count = +// list.map(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 arg = +// glua.table( +// list.index_map(list.range(1, 10), fn(i, n) { +// #(glua.int(i + 1), glua.int(n)) +// }), +// ) +// +// let assert Ok(#(lua, [result])) = +// glua.call_function_by_name( +// state: lua, +// keys: ["count_odd"], +// args: [arg], +// using: decode.int, +// ) +// +// assert result == 5 +// +// let tbl = +// glua.table([ +// #( +// 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)) +// }), +// ), +// #( +// 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) +// +// let assert Ok(lua) = glua.set(state: lua, keys: ["my_functions"], value: tbl) +// +// let assert Ok(#(lua, [result])) = +// glua.call_function_by_name( +// state: lua, +// keys: ["my_functions", "is_even"], +// args: [arg], +// using: decode.bool, +// ) +// +// assert result == True +// +// let assert Ok(#(_, [result])) = +// glua.eval( +// state: lua, +// code: "return my_functions.is_odd(4)", +// using: decode.bool, +// ) +// +// assert result == False +// } +// +// pub fn set_lua_paths_test() { +// let assert Ok(state) = +// glua.set_lua_paths(state: glua.new(), paths: ["./test/lua/?.lua"]) +// +// let code = "local s = require 'example'; return s" +// +// let assert Ok(#(_, [result])) = glua.eval(state:, code:, using: decode.string) +// +// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" +// } +// +// pub fn get_private_test() { +// assert glua.new() +// |> glua.set_private("test", [1, 2, 3]) +// |> glua.get_private("test", using: decode.list(decode.int)) +// == Ok([1, 2, 3]) +// +// assert glua.new() +// |> glua.get_private("non_existent", using: decode.string) +// == Error(glua.KeyNotFound) +// } +// +// pub fn delete_private_test() { +// let lua = glua.set_private(glua.new(), "the_value", "that_will_be_deleted") +// +// assert glua.get_private(lua, "the_value", using: decode.string) +// == Ok("that_will_be_deleted") +// +// assert glua.delete_private(lua, "the_value") +// |> glua.get_private(key: "the_value", using: decode.string) +// == Error(glua.KeyNotFound) +// } +// +// pub fn load_test() { +// let assert Ok(#(lua, chunk)) = +// glua.load(state: glua.new(), code: "return 5 * 5") +// let assert Ok(#(_, [result])) = +// glua.eval_chunk(state: lua, chunk:, using: decode.int) +// +// assert result == 25 +// } +// +// 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(#(_, [result])) = +// glua.eval_chunk(state: lua, chunk:, using: decode.string) +// +// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" +// } +// +// pub fn eval_test() { +// let assert Ok(#(lua, [result])) = +// glua.eval( +// state: glua.new(), +// code: "return 'hello, ' .. 'world!'", +// using: decode.string, +// ) +// +// assert result == "hello, world!" +// +// let assert Ok(#(_, results)) = +// glua.eval(state: lua, code: "return 2 + 2, 3 - 1", using: decode.int) +// +// assert results == [4, 2] +// } +// +// pub fn eval_returns_proper_errors_test() { +// let state = glua.new() +// +// assert glua.eval(state:, code: "if true then 1 + ", using: decode.int) +// == Error( +// glua.LuaCompilerException(messages: ["syntax error before: ", "1"]), +// ) +// +// assert glua.eval(state:, code: "return 'Hello from Lua!'", using: decode.int) +// == Error( +// glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), +// ) +// +// let assert Error(glua.LuaRuntimeException( +// exception: glua.IllegalIndex(value:, index:), +// state: _, +// )) = glua.eval(state:, code: "return a.b", using: decode.int) +// +// assert value == "nil" +// assert index == "b" +// +// let assert Error(glua.LuaRuntimeException( +// exception: glua.ErrorCall(messages:), +// state: _, +// )) = glua.eval(state:, code: "error('error message')", using: decode.int) +// +// assert messages == ["error message"] +// +// let assert Error(glua.LuaRuntimeException( +// exception: glua.UndefinedFunction(value:), +// state: _, +// )) = glua.eval(state:, code: "local a = 5; a()", using: decode.int) +// +// assert value == "5" +// let assert Error(glua.LuaRuntimeException( +// exception: glua.BadArith(operator:, args:), +// state: _, +// )) = glua.eval(state:, code: "return 10 / 0", using: decode.int) +// +// assert operator == "/" +// assert args == ["10", "0"] +// +// let assert Error(glua.LuaRuntimeException( +// exception: glua.AssertError(message:), +// state: _, +// )) = +// glua.eval( +// state:, +// code: "assert(1 == 2, 'assertion failed')", +// using: decode.int, +// ) +// +// assert message == "assertion failed" +// } +// +// pub fn eval_file_test() { +// let assert Ok(#(_, [result])) = +// glua.eval_file( +// state: glua.new(), +// path: "./test/lua/example.lua", +// using: decode.string, +// ) +// +// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" +// } +// +// pub fn call_function_test() { +// let assert Ok(#(lua, [fun])) = +// glua.ref_eval(state: glua.new(), code: "return string.reverse") +// +// let encoded = glua.string("auL") +// +// let assert Ok(#(lua, [result])) = +// glua.call_function( +// state: lua, +// ref: fun, +// args: [encoded], +// using: decode.string, +// ) +// +// assert result == "Lua" +// +// let assert Ok(#(lua, [fun])) = +// glua.ref_eval(state: lua, code: "return function(a, b) return a .. b end") +// +// let args = list.map(["Lua in ", "Gleam"], glua.string) +// +// let assert Ok(#(_, [result])) = +// glua.call_function(state: lua, ref: fun, args:, using: decode.string) +// +// assert result == "Lua in Gleam" +// } +// +// pub fn call_function_returns_proper_errors_test() { +// let state = glua.new() +// +// let assert Ok(#(state, [ref])) = +// glua.ref_eval(state:, code: "return string.upper") +// +// let arg = glua.string("Hello from Gleam!") +// +// assert glua.call_function(state:, ref:, args: [arg], using: decode.int) +// == Error( +// glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), +// ) +// +// let assert Ok(#(lua, [ref])) = glua.ref_eval(state:, code: "return 1") +// +// let assert Error(glua.LuaRuntimeException( +// exception: glua.UndefinedFunction(value:), +// state: _, +// )) = glua.call_function(state: lua, ref:, args: [], using: decode.string) +// +// assert value == "1" +// } +// +// pub fn call_function_by_name_test() { +// let args = list.map([20, 10], glua.int) +// let assert Ok(#(lua, [result])) = +// glua.call_function_by_name( +// state: glua.new(), +// keys: ["math", "max"], +// args:, +// using: decode.int, +// ) +// +// assert result == 20 +// +// let assert Ok(#(lua, [result])) = +// glua.call_function_by_name( +// state: lua, +// keys: ["math", "min"], +// args:, +// using: decode.int, +// ) +// +// assert result == 10 +// +// let arg = glua.float(10.2) +// let assert Ok(#(_, [result])) = +// glua.call_function_by_name( +// state: lua, +// keys: ["math", "type"], +// args: [arg], +// using: decode.optional(decode.string), +// ) +// +// assert result == option.Some("float") +// } +// +// pub fn nested_function_references_test() { +// let code = "return function() return math.sqrt end" +// +// let assert Ok(#(lua, [ref])) = glua.ref_eval(state: glua.new(), code:) +// let assert Ok(#(lua, [ref])) = +// glua.ref_call_function(state: lua, ref:, args: []) +// +// let arg = glua.int(400) +// let assert Ok(#(_, [result])) = +// glua.call_function(state: lua, ref:, args: [arg], using: decode.float) +// assert result == 20.0 +// } +// +// pub fn alloc_test() { +// let #(lua, table) = glua.alloc_table(glua.new(), []) +// let proxy = +// glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) +// let metatable = glua.table([#(glua.string("__index"), proxy)]) +// let assert Ok(#(lua, _)) = +// glua.ref_call_function_by_name(lua, ["setmetatable"], [table, metatable]) +// let assert Ok(lua) = glua.set(lua, ["test_table"], table) +// +// let assert Ok(#(_lua, [ret1])) = +// glua.eval(lua, "return test_table.any_key", decode.string) +// +// let assert Ok(#(_lua, [ret2])) = +// glua.eval(lua, "return test_table.other_key", decode.string) +// +// assert ret1 == "constant" +// assert ret2 == "constant" +// } From bd79990ca71e84ef6086e5ac84e9bdb1af7db6ca Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:56:59 -1000 Subject: [PATCH 08/41] Add push path and error --- src/deser.gleam | 111 +++++++++++++++++++++++++++++------------------ src/glua_ffi.erl | 24 +++++----- 2 files changed, 82 insertions(+), 53 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 1721798..47dc300 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -6,7 +6,9 @@ import gleam/dict.{type Dict} import gleam/dynamic import gleam/dynamic/decode.{type Decoder} +import gleam/list import gleam/option.{type Option} +import gleam/string import glua.{type Lua, type Value, type ValueRef} pub opaque type Deserializer(t) { @@ -14,7 +16,7 @@ pub opaque type Deserializer(t) { } pub type DeserializeError { - DeserializeError(expected: String, found: String, path: List(ValueRef)) + DeserializeError(expected: RefType, found: RefType, path: List(ValueRef)) } pub fn field( @@ -43,28 +45,6 @@ pub fn subfield( // }) } -pub fn run( - data: ValueRef, - deser: Deserializer(t), -) -> Result(#(Lua, t), List(DeserializeError)) { - let #(maybe_invalid_data, lua, errors) = deser.function(data) - case errors { - [] -> Ok(#(lua, maybe_invalid_data)) - [_, ..] -> Error(errors) - } -} - -pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { - todo - // Deserializer(function: fn(data) { - // index(path, [], inner.function, data, fn(data, position) { - // let #(default, _) = inner.function(data) - // #(default, [DecodeError("Field", "Nothing", [])]) - // |> push_path(list.reverse(position)) - // }) - // }) -} - fn index( path: List(a), position: List(a), @@ -98,44 +78,91 @@ fn index( // } } +pub fn run( + data: ValueRef, + deser: Deserializer(t), +) -> Result(#(Lua, t), List(DeserializeError)) { + let #(maybe_invalid_data, lua, errors) = deser.function(data) + case errors { + [] -> Ok(#(lua, maybe_invalid_data)) + [_, ..] -> Error(errors) + } +} + +pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { + todo + // Deserializer(function: fn(data) { + // index(path, [], inner.function, data, fn(data, position) { + // let #(default, _) = inner.function(data) + // #(default, [DecodeError("Field", "Nothing", [])]) + // |> push_path(list.reverse(position)) + // }) + // }) +} + +@external(erlang, "luerl", "decode") +fn decode(val: ValueRef, state: Lua) -> a + @external(erlang, "gleam_stdlib", "index") @external(javascript, "../../gleam_stdlib.mjs", "index") fn bare_index(data: ValueRef, key: anything) -> Result(Option(ValueRef), String) +fn to_string(lua: Lua, val: ValueRef) { + case classify(val) { + Null -> "" + Number -> decode(val, lua) + String -> decode(val, lua) + Unknown -> "" + UserDef -> "userdefined: " <> string.inspect(val) + _ -> { + { + case glua.ref_call_function_by_name(lua, ["tostring"], [val]) { + Ok(#(lua, [value])) -> { + todo as "deserialize value" + } + Error(err) -> " string.inspect(err) <> ")>" + _ -> "" + } + } + } + } +} + +pub type RefType { + Null + Bool + Number + String + Table + UserDef + Function + Unknown +} + fn push_path( layer: #(t, List(DeserializeError)), - path: List(key), + path: List(ValueRef), ) -> #(t, List(DeserializeError)) { - todo - // let decoder = one_of(string, [int |> map(int.to_string)]) - // let path = - // list.map(path, fn(key) { - // let key = cast(key) - // case run(key, decoder) { - // Ok(key) -> key - // Error(_) -> "<" <> dynamic.classify(key) <> ">" - // } - // }) - // let errors = - // list.map(layer.1, fn(error) { - // DecodeError(..error, path: list.append(path, error.path)) - // }) - // #(layer.0, errors) + let errors = + list.map(layer.1, fn(error) { + DeserializeError(..error, path: list.append(path, error.path)) + }) + #(layer.0, errors) } pub fn success(state: Lua, data: t) -> Deserializer(t) { Deserializer(function: fn(_) { #(data, state, []) }) } -pub fn de_error( - expected expected: String, +pub fn deser_error( + expected expected: RefType, found found: ValueRef, ) -> List(DeserializeError) { [DeserializeError(expected: expected, found: classify(found), path: [])] } @external(erlang, "glua_ffi", "classify") -pub fn classify(a: anything) -> String +pub fn classify(a: anything) -> RefType pub fn optional_field( key: name, diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 73afb31..1eacfe1 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -33,25 +33,27 @@ to_gleam(Value) -> end. classify(nil) -> - "Nil"; + null; classify(Bool) when is_boolean(Bool) -> - "Bool"; + bool; +is_encoded(Binary) when is_binary(Binary) -> + string; classify(N) when is_number(N) -> - "Number"; + number; classify({tref,_}) -> - "Table"; + table; classify({usrdef,_}) -> - "UserDef"; + user_def; classify({eref,_}) -> - "Unknown"; + unknown; classify({funref,_,_}) -> - "Function"; + function; classify({erl_func,_}) -> - "Function"; + function; classify({erl_mfa,_,_,_}) -> - "Function"; + function; classify(_) -> - "Unknown". + unknown. %% helper to determine if a value is encoded or not @@ -218,7 +220,7 @@ call_function(Lua, Fun, Args) -> {EncodedArgs, State} = encode_list(Args, Lua), to_gleam(luerl:call(Fun, EncodedArgs, State)). -ref_call_function(Lua, Func, Args) -> +ref_call_function(Lua, Fun, Args) -> to_gleam(luerl:call(Fun, Args, Lua)). call_function_dec(Lua, Fun, Args) -> From 7699817c60f89720b783ebf6ce81589f49d744e0 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:23:18 -1000 Subject: [PATCH 09/41] Write field code --- src/deser.gleam | 166 +++++++++++++++++++++++++---------------------- src/glua_ffi.erl | 38 +++++++---- 2 files changed, 113 insertions(+), 91 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 47dc300..11b321e 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -7,18 +7,23 @@ import gleam/dict.{type Dict} import gleam/dynamic import gleam/dynamic/decode.{type Decoder} import gleam/list -import gleam/option.{type Option} +import gleam/option.{type Option, None, Some} +import gleam/pair +import gleam/result import gleam/string import glua.{type Lua, type Value, type ValueRef} pub opaque type Deserializer(t) { - Deserializer(function: fn(ValueRef) -> #(t, Lua, List(DeserializeError))) + Deserializer(function: fn(Lua, ValueRef) -> Return(t)) } pub type DeserializeError { - DeserializeError(expected: RefType, found: RefType, path: List(ValueRef)) + DeserializeError(expected: String, found: String, path: List(ValueRef)) } +type Return(t) = + #(t, Lua, List(DeserializeError)) + pub fn field( field_path: ValueRef, field_decoder: Deserializer(t), @@ -32,57 +37,65 @@ pub fn subfield( field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { - todo - // Deserializer(function: fn(data) { - // let #(out, errors1) = - // index(field_path, [], field_decoder.function, data, fn(data, position) { - // let #(default, _) = field_decoder.function(data) - // #(default, [DecodeError("Field", "Nothing", [])]) - // |> push_path(list.reverse(position)) - // }) - // let #(out, errors2) = next(out).function(data) - // #(out, list.append(errors1, errors2)) - // }) + Deserializer(function: fn(lua, data) { + let #(out, lua, errors1) = + index( + lua, + field_path, + [], + field_decoder.function, + data, + fn(lua, data, position) { + let #(default, _lua, _) = field_decoder.function(lua, data) + #(default, lua, [DeserializeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }, + ) + let #(out, lua, errors2) = next(out).function(lua, data) + #(out, lua, list.append(errors1, errors2)) + }) } fn index( - path: List(a), - position: List(a), - inner: fn(ValueRef) -> #(b, List(DeserializeError)), + lua: Lua, + path: List(ValueRef), + position: List(ValueRef), + inner: fn(Lua, ValueRef) -> Return(b), data: ValueRef, - handle_miss: fn(ValueRef, List(a)) -> #(b, List(DeserializeError)), -) -> #(b, List(DeserializeError)) { - todo - // case path { - // [] -> { - // data - // |> inner - // |> push_path(list.reverse(position)) - // } - // - // [key, ..path] -> { - // case bare_index(data, key) { - // Ok(Some(data)) -> { - // index(path, [key, ..position], inner, data, handle_miss) - // } - // Ok(None) -> { - // handle_miss(data, [key, ..position]) - // } - // Error(kind) -> { - // let #(default, _) = inner(data) - // #(default, [DecodeError(kind, dynamic.classify(data), [])]) - // |> push_path(list.reverse(position)) - // } - // } - // } - // } + handle_miss: fn(Lua, ValueRef, List(ValueRef)) -> Return(b), +) -> Return(b) { + case path { + [] -> { + data + |> inner(lua, _) + |> push_path(list.reverse(position)) + } + + [key, ..path] -> { + case get_table_key(lua, data, key) { + Ok(#(lua, data)) -> { + index(lua, path, [key, ..position], inner, data, handle_miss) + } + // NOTE: I don't feel comfortable matching on this + Error(glua.KeyNotFound) -> { + handle_miss(lua, data, [key, ..position]) + } + Error(_err) -> { + let #(default, lua, _) = inner(lua, data) + #(default, lua, [DeserializeError("Table", classify(data), [])]) + |> push_path(list.reverse(position)) + } + } + } + } } pub fn run( + lua: Lua, data: ValueRef, deser: Deserializer(t), ) -> Result(#(Lua, t), List(DeserializeError)) { - let #(maybe_invalid_data, lua, errors) = deser.function(data) + let #(maybe_invalid_data, lua, errors) = deser.function(lua, data) case errors { [] -> Ok(#(lua, maybe_invalid_data)) [_, ..] -> Error(errors) @@ -107,18 +120,27 @@ fn decode(val: ValueRef, state: Lua) -> a @external(javascript, "../../gleam_stdlib.mjs", "index") fn bare_index(data: ValueRef, key: anything) -> Result(Option(ValueRef), String) +@external(erlang, "glua_ffi", "get_table_key") +fn get_table_key( + lua: Lua, + table: ValueRef, + key: ValueRef, +) -> Result(#(Lua, ValueRef), glua.LuaError) + fn to_string(lua: Lua, val: ValueRef) { case classify(val) { - Null -> "" - Number -> decode(val, lua) - String -> decode(val, lua) - Unknown -> "" - UserDef -> "userdefined: " <> string.inspect(val) + "Null" -> "" + "Number" -> decode(val, lua) + "String" -> decode(val, lua) + "Unknown" -> "" + "UserDef" -> "userdefined: " <> string.inspect(val) _ -> { { case glua.ref_call_function_by_name(lua, ["tostring"], [val]) { Ok(#(lua, [value])) -> { - todo as "deserialize value" + run(lua, value, string) + |> result.map(pair.second) + |> result.unwrap("") } Error(err) -> " string.inspect(err) <> ")>" _ -> "" @@ -128,41 +150,27 @@ fn to_string(lua: Lua, val: ValueRef) { } } -pub type RefType { - Null - Bool - Number - String - Table - UserDef - Function - Unknown -} - -fn push_path( - layer: #(t, List(DeserializeError)), - path: List(ValueRef), -) -> #(t, List(DeserializeError)) { +fn push_path(layer: Return(t), path: List(ValueRef)) -> Return(t) { let errors = - list.map(layer.1, fn(error) { + list.map(layer.2, fn(error) { DeserializeError(..error, path: list.append(path, error.path)) }) - #(layer.0, errors) + #(layer.0, layer.1, errors) } pub fn success(state: Lua, data: t) -> Deserializer(t) { - Deserializer(function: fn(_) { #(data, state, []) }) + Deserializer(function: fn(_, _) { #(data, state, []) }) } pub fn deser_error( - expected expected: RefType, + expected expected: String, found found: ValueRef, ) -> List(DeserializeError) { [DeserializeError(expected: expected, found: classify(found), path: [])] } @external(erlang, "glua_ffi", "classify") -pub fn classify(a: anything) -> RefType +pub fn classify(a: anything) -> String pub fn optional_field( key: name, @@ -210,14 +218,14 @@ pub fn optionally_at( pub const string: Deserializer(String) = Deserializer(deser_string) -fn deser_string(data: ValueRef) -> #(String, Lua, List(DeserializeError)) { +fn deser_string(_lua, data: ValueRef) -> #(String, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "String", dynamic_string) } pub const bool: Deserializer(Bool) = Deserializer(deser_bool) -fn deser_bool(data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { +fn deser_bool(_lua, data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { todo // case cast(True) == data { // True -> #(True, []) @@ -231,21 +239,24 @@ fn deser_bool(data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { pub const int: Deserializer(Int) = Deserializer(deser_int) -fn deser_int(data: ValueRef) -> #(Int, Lua, List(DeserializeError)) { +fn deser_int(_lua, data: ValueRef) -> #(Int, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "Int", dynamic_int) } pub const float: Deserializer(Float) = Deserializer(deser_float) -fn deser_float(data: ValueRef) -> #(Float, Lua, List(DeserializeError)) { +fn deser_float(_lua, data: ValueRef) -> #(Float, Lua, List(DeserializeError)) { todo // run_dynamic_function(data, "Float", dynamic_float) } pub const value_ref: Deserializer(ValueRef) = Deserializer(decode_dynamic) -fn decode_dynamic(data: ValueRef) -> #(ValueRef, Lua, List(DeserializeError)) { +fn decode_dynamic( + _lua, + data: ValueRef, +) -> #(ValueRef, Lua, List(DeserializeError)) { #(data, todo, []) } @@ -254,6 +265,7 @@ pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( ) fn deser_user_defined( + _lua, data: ValueRef, ) -> #(dynamic.Dynamic, Lua, List(DeserializeError)) { todo @@ -404,8 +416,8 @@ pub fn failure(zero: a, expected: String) -> Deserializer(a) { } pub fn recursive(inner: fn() -> Deserializer(a)) -> Deserializer(a) { - Deserializer(function: fn(data) { + Deserializer(function: fn(lua, data) { let decoder = inner() - decoder.function(data) + decoder.function(lua, data) }) } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1eacfe1..4b35a1d 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -2,7 +2,7 @@ -import(luerl_lib, [lua_error/2]). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, +-export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, alloc/2]). @@ -33,28 +33,27 @@ to_gleam(Value) -> end. classify(nil) -> - null; + "Nil"; classify(Bool) when is_boolean(Bool) -> - bool; + "Bool"; is_encoded(Binary) when is_binary(Binary) -> - string; -classify(N) when is_number(N) -> - number; + "String"; +classify(N) when is_float(N) -> + "Number"; classify({tref,_}) -> - table; + "Table"; classify({usrdef,_}) -> - user_def; + "UserDef"; classify({eref,_}) -> - unknown; + "Unknown"; classify({funref,_,_}) -> - function; + "Function"; classify({erl_func,_}) -> - function; + "Function"; classify({erl_mfa,_,_,_}) -> - function; + "Function"; classify(_) -> - unknown. - + "Unknown". %% helper to determine if a value is encoded or not %% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 @@ -159,6 +158,17 @@ wrap_fun(Fun) -> sandbox_fun(Msg) -> fun(_, State) -> {error, map_error(lua_error({error_call, [Msg]}, State))} end. + +get_table_key(Lua, Table, Key) -> + case luerl:get_table_keys(Table, Keys, Lua) of + {ok, nil, _} -> + {error, nil}; + {ok, Value, Lua} -> + {ok, {Lua, Value}}; + Other -> + to_gleam(Other) + end. + get_table_keys(Lua, Keys) -> case luerl:get_table_keys(Keys, Lua) of {ok, nil, _} -> From ed6b36671e28591207a967f99b3ba221947b2684 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:45:25 -1000 Subject: [PATCH 10/41] Do basic decoders Still not tested! --- src/deser.gleam | 104 +++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 71 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 11b321e..121eca2 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -113,9 +113,6 @@ pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { // }) } -@external(erlang, "luerl", "decode") -fn decode(val: ValueRef, state: Lua) -> a - @external(erlang, "gleam_stdlib", "index") @external(javascript, "../../gleam_stdlib.mjs", "index") fn bare_index(data: ValueRef, key: anything) -> Result(Option(ValueRef), String) @@ -205,59 +202,41 @@ pub fn optionally_at( // }) } -// fn run_dynamic_function( -// data: ValueRef, -// name: String, -// f: fn(ValueRef) -> Result(t, t), -// ) -> #(t, List(DecodeError)) { -// case f(data) { -// Ok(data) -> #(data, []) -// Error(zero) -> #(zero, [DecodeError(name, dynamic.classify(data), [])]) -// } -// } +fn run_dynamic_function(lua: Lua, data: ValueRef, name: String) -> Return(t) { + case decode(data, lua) { + Ok(data) -> #(data, lua, []) + Error(zero) -> #(zero, lua, [ + DeserializeError(name, classify(data), []), + ]) + } +} + +/// Warning: Can panic +@external(erlang, "luerl", "decode") +fn decode(a: ValueRef, lua: Lua) -> a pub const string: Deserializer(String) = Deserializer(deser_string) -fn deser_string(_lua, data: ValueRef) -> #(String, Lua, List(DeserializeError)) { - todo - // run_dynamic_function(data, "String", dynamic_string) +fn deser_string(lua, data: ValueRef) -> #(String, Lua, List(DeserializeError)) { + run_dynamic_function(lua, data, "String") } pub const bool: Deserializer(Bool) = Deserializer(deser_bool) -fn deser_bool(_lua, data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { - todo - // case cast(True) == data { - // True -> #(True, []) - // False -> - // case cast(False) == data { - // True -> #(False, []) - // False -> #(False, decode_error("Bool", data)) - // } - // } +fn deser_bool(lua, data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { + run_dynamic_function(lua, data, "Bool") } -pub const int: Deserializer(Int) = Deserializer(deser_int) +pub const number: Deserializer(Float) = Deserializer(deser_num) -fn deser_int(_lua, data: ValueRef) -> #(Int, Lua, List(DeserializeError)) { - todo - // run_dynamic_function(data, "Int", dynamic_int) +fn deser_num(lua, data: ValueRef) -> #(Float, Lua, List(DeserializeError)) { + run_dynamic_function(lua, data, "Float") } -pub const float: Deserializer(Float) = Deserializer(deser_float) +pub const raw: Deserializer(ValueRef) = Deserializer(decode_raw) -fn deser_float(_lua, data: ValueRef) -> #(Float, Lua, List(DeserializeError)) { - todo - // run_dynamic_function(data, "Float", dynamic_float) -} - -pub const value_ref: Deserializer(ValueRef) = Deserializer(decode_dynamic) - -fn decode_dynamic( - _lua, - data: ValueRef, -) -> #(ValueRef, Lua, List(DeserializeError)) { - #(data, todo, []) +fn decode_raw(lua, data: ValueRef) -> #(ValueRef, Lua, List(DeserializeError)) { + #(data, lua, []) } pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( @@ -265,17 +244,12 @@ pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( ) fn deser_user_defined( - _lua, + lua, data: ValueRef, ) -> #(dynamic.Dynamic, Lua, List(DeserializeError)) { - todo - // run_dynamic_function(data, "BitArray", dynamic_bit_array) + run_dynamic_function(lua, data, "UserDef") } -@external(erlang, "gleam_stdlib", "bit_array") -@external(javascript, "../../gleam_stdlib.mjs", "bit_array") -fn dynamic_bit_array(data: ValueRef) -> Result(BitArray, BitArray) - pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { todo // Deserializer(fn(data) { @@ -283,17 +257,6 @@ pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { // }) } -@external(erlang, "gleam_stdlib", "list") -@external(javascript, "../../gleam_stdlib.mjs", "list") -fn decode_list( - data: ValueRef, - item: fn(ValueRef) -> #(t, List(DeserializeError)), - push_path: fn(#(t, List(DeserializeError)), key) -> - #(t, List(DeserializeError)), - index: Int, - acc: List(t), -) -> #(List(t), List(DeserializeError)) - pub fn dict( key: Deserializer(key), value: Deserializer(value), @@ -316,16 +279,15 @@ pub fn dict( } pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { - todo - // Deserializer(function: fn(data) { - // case is_null(data) { - // True -> #(option.None, []) - // False -> { - // let #(data, errors) = inner.function(data) - // #(option.Some(data), errors) - // } - // } - // }) + Deserializer(function: fn(lua, data) { + case classify(data) { + "Nil" -> #(option.None, lua, []) + _ -> { + let #(data, lua, errors) = inner.function(lua, data) + #(option.Some(data), lua, errors) + } + } + }) } pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) { From 3c765370fe2194c19f95712b4b18879a8335a465 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:29:52 -1000 Subject: [PATCH 11/41] Make first tests --- src/deser.gleam | 37 ++++++++++++++++++++----------------- src/glua_ffi.erl | 30 +++++++++++++++--------------- test/deserialize_test.gleam | 29 +++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 test/deserialize_test.gleam diff --git a/src/deser.gleam b/src/deser.gleam index 121eca2..97878f5 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -202,11 +202,17 @@ pub fn optionally_at( // }) } -fn run_dynamic_function(lua: Lua, data: ValueRef, name: String) -> Return(t) { - case decode(data, lua) { - Ok(data) -> #(data, lua, []) - Error(zero) -> #(zero, lua, [ - DeserializeError(name, classify(data), []), +fn run_dynamic_function( + lua: Lua, + data: ValueRef, + expected: String, + zero: t, +) -> Return(t) { + let got = classify(data) + case got == expected { + True -> #(decode(data, lua), lua, []) + False -> #(zero, lua, [ + DeserializeError(expected, got, []), ]) } } @@ -217,25 +223,25 @@ fn decode(a: ValueRef, lua: Lua) -> a pub const string: Deserializer(String) = Deserializer(deser_string) -fn deser_string(lua, data: ValueRef) -> #(String, Lua, List(DeserializeError)) { - run_dynamic_function(lua, data, "String") +fn deser_string(lua, data: ValueRef) -> Return(String) { + run_dynamic_function(lua, data, "String", "") } pub const bool: Deserializer(Bool) = Deserializer(deser_bool) -fn deser_bool(lua, data: ValueRef) -> #(Bool, Lua, List(DeserializeError)) { - run_dynamic_function(lua, data, "Bool") +fn deser_bool(lua, data: ValueRef) -> Return(Bool) { + run_dynamic_function(lua, data, "Bool", True) } pub const number: Deserializer(Float) = Deserializer(deser_num) -fn deser_num(lua, data: ValueRef) -> #(Float, Lua, List(DeserializeError)) { - run_dynamic_function(lua, data, "Float") +fn deser_num(lua, data: ValueRef) -> Return(Float) { + run_dynamic_function(lua, data, "Number", 0.0) } pub const raw: Deserializer(ValueRef) = Deserializer(decode_raw) -fn decode_raw(lua, data: ValueRef) -> #(ValueRef, Lua, List(DeserializeError)) { +fn decode_raw(lua, data: ValueRef) -> Return(ValueRef) { #(data, lua, []) } @@ -243,11 +249,8 @@ pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( deser_user_defined, ) -fn deser_user_defined( - lua, - data: ValueRef, -) -> #(dynamic.Dynamic, Lua, List(DeserializeError)) { - run_dynamic_function(lua, data, "UserDef") +fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { + run_dynamic_function(lua, data, "UserDef", dynamic.nil()) } pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 4b35a1d..b4b3a18 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -5,7 +5,7 @@ -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, - alloc/2]). + alloc/2, classify/1, ref_call_function/3]). %% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam maybe_process_userdata(Lst) when is_list(Lst) -> @@ -33,27 +33,27 @@ to_gleam(Value) -> end. classify(nil) -> - "Nil"; + <<"Nil">>; classify(Bool) when is_boolean(Bool) -> - "Bool"; -is_encoded(Binary) when is_binary(Binary) -> - "String"; + <<"Bool">>; +classify(Binary) when is_binary(Binary) -> + <<"String">>; classify(N) when is_float(N) -> - "Number"; + <<"Number">>; classify({tref,_}) -> - "Table"; -classify({usrdef,_}) -> - "UserDef"; + <<"Table">>; +classify({usdref,_}) -> + <<"UserDef">>; classify({eref,_}) -> - "Unknown"; + <<"Unknown">>; classify({funref,_,_}) -> - "Function"; + <<"Function">>; classify({erl_func,_}) -> - "Function"; + <<"Function">>; classify({erl_mfa,_,_,_}) -> - "Function"; + <<"Function">>; classify(_) -> - "Unknown". + <<"Unknown">>. %% helper to determine if a value is encoded or not %% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 @@ -160,7 +160,7 @@ sandbox_fun(Msg) -> get_table_key(Lua, Table, Key) -> - case luerl:get_table_keys(Table, Keys, Lua) of + case luerl:get_table_keys(Table, Key, Lua) of {ok, nil, _} -> {error, nil}; {ok, Value, Lua} -> diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam new file mode 100644 index 0000000..b018dcc --- /dev/null +++ b/test/deserialize_test.gleam @@ -0,0 +1,29 @@ +import deser +import gleam/dynamic +import gleam/dynamic/decode +import gleam/list +import glua + +/// Warning: Can throw +@external(erlang, "luerl", "encode") +fn encode(a: anything, lua: glua.Lua) -> #(glua.ValueRef, glua.Lua) + +@external(erlang, "glua_ffi", "coerce") +fn coerce_dynamic(a: anything) -> dynamic.Dynamic + +pub fn basic_test() { + let lua = glua.new() + let #(ref, lua) = encode("Hello", lua) + let assert Ok(#(_lua, "Hello")) = deser.run(lua, ref, deser.string) + + let #(ref, lua) = encode(42.0, lua) + let assert Ok(#(_lua, 42.0)) = deser.run(lua, ref, deser.number) + + let #(ref, lua) = encode(False, lua) + let assert Ok(#(_lua, False)) = deser.run(lua, ref, deser.bool) + + let userdef = ["this", "is", "some", "random", "data"] |> glua.userdata + let #(ref, lua) = encode(userdef, lua) + let assert Ok(#(_lua, udef)) = deser.run(lua, ref, deser.user_defined) + assert udef == coerce_dynamic(userdef) +} From f64a689ed348f2652f4f37e14592bef3a64ddabc Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:51:26 -1000 Subject: [PATCH 12/41] Test field --- src/deser.gleam | 10 +++------- src/glua.gleam | 3 +++ src/glua_ffi.erl | 2 +- test/deserialize_test.gleam | 16 +++++++++++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 97878f5..6f37558 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -46,7 +46,7 @@ pub fn subfield( field_decoder.function, data, fn(lua, data, position) { - let #(default, _lua, _) = field_decoder.function(lua, data) + let #(default, lua, _) = field_decoder.function(lua, data) #(default, lua, [DeserializeError("Field", "Nothing", [])]) |> push_path(list.reverse(position)) }, @@ -113,10 +113,6 @@ pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { // }) } -@external(erlang, "gleam_stdlib", "index") -@external(javascript, "../../gleam_stdlib.mjs", "index") -fn bare_index(data: ValueRef, key: anything) -> Result(Option(ValueRef), String) - @external(erlang, "glua_ffi", "get_table_key") fn get_table_key( lua: Lua, @@ -155,8 +151,8 @@ fn push_path(layer: Return(t), path: List(ValueRef)) -> Return(t) { #(layer.0, layer.1, errors) } -pub fn success(state: Lua, data: t) -> Deserializer(t) { - Deserializer(function: fn(_, _) { #(data, state, []) }) +pub fn success(data: t) -> Deserializer(t) { + Deserializer(function: fn(lua, _) { #(data, lua, []) }) } pub fn deser_error( diff --git a/src/glua.gleam b/src/glua.gleam index 113366f..7e1e8df 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -52,6 +52,9 @@ pub type Value /// that will return references to the values instead of decoding them. pub type ValueRef +@external(erlang, "glua_ffi", "coerce") +pub fn str_ref(str: String) -> ValueRef + @external(erlang, "glua_ffi", "coerce_nil") pub fn nil() -> Value diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index b4b3a18..3951dab 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -160,7 +160,7 @@ sandbox_fun(Msg) -> get_table_key(Lua, Table, Key) -> - case luerl:get_table_keys(Table, Key, Lua) of + case luerl:get_table_key(Table, Key, Lua) of {ok, nil, _} -> {error, nil}; {ok, Value, Lua} -> diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index b018dcc..462e7bd 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -1,7 +1,5 @@ import deser import gleam/dynamic -import gleam/dynamic/decode -import gleam/list import glua /// Warning: Can throw @@ -11,7 +9,7 @@ fn encode(a: anything, lua: glua.Lua) -> #(glua.ValueRef, glua.Lua) @external(erlang, "glua_ffi", "coerce") fn coerce_dynamic(a: anything) -> dynamic.Dynamic -pub fn basic_test() { +pub fn decoder_test() { let lua = glua.new() let #(ref, lua) = encode("Hello", lua) let assert Ok(#(_lua, "Hello")) = deser.run(lua, ref, deser.string) @@ -27,3 +25,15 @@ pub fn basic_test() { let assert Ok(#(_lua, udef)) = deser.run(lua, ref, deser.user_defined) assert udef == coerce_dynamic(userdef) } + +pub fn field_ok_test() { + let lua = glua.new() + let data = glua.table([#(glua.string("name"), glua.string("Hina"))]) + let #(ref, lua) = encode(data, lua) + let assert Ok(#(_lua, val)) = + deser.run(lua, ref, { + use str <- deser.field(glua.str_ref("name"), deser.string) + deser.success(str) + }) + assert val == "Hina" +} From 85485e2ba08a0a4f7c8dd9b9e1de3d6cffc1b67a Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:00:10 -1000 Subject: [PATCH 13/41] Add subfield ok test --- src/glua.gleam | 3 +++ test/deserialize_test.gleam | 31 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/glua.gleam b/src/glua.gleam index 7e1e8df..d18ab8b 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -55,6 +55,9 @@ pub type ValueRef @external(erlang, "glua_ffi", "coerce") pub fn str_ref(str: String) -> ValueRef +@external(erlang, "glua_ffi", "coerce") +pub fn int_ref(str: Int) -> ValueRef + @external(erlang, "glua_ffi", "coerce_nil") pub fn nil() -> Value diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 462e7bd..a7255ca 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -28,7 +28,11 @@ pub fn decoder_test() { pub fn field_ok_test() { let lua = glua.new() - let data = glua.table([#(glua.string("name"), glua.string("Hina"))]) + let data = + glua.table([ + #(glua.string("red herring"), glua.string("not here!")), + #(glua.string("name"), glua.string("Hina")), + ]) let #(ref, lua) = encode(data, lua) let assert Ok(#(_lua, val)) = deser.run(lua, ref, { @@ -37,3 +41,28 @@ pub fn field_ok_test() { }) assert val == "Hina" } + +pub fn subfield_ok_test() { + let lua = glua.new() + let data = + glua.table([ + #(glua.string("name"), glua.string("Hina")), + #( + glua.string("friends"), + glua.table([ + #(glua.int(1), glua.string("Puffy")), + #(glua.int(2), glua.string("Lucy")), + ]), + ), + ]) + let #(ref, lua) = encode(data, lua) + let assert Ok(#(_lua, val)) = + deser.run(lua, ref, { + use first <- deser.subfield( + [glua.str_ref("friends"), glua.int_ref(1)], + deser.string, + ) + deser.success(first) + }) + assert val == "Puffy" +} From 9c5b7f8c8a74e6e1182e1566e9e8dc3c36744cca Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:35:59 -1000 Subject: [PATCH 14/41] Add field metatable test --- test/deserialize_test.gleam | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index a7255ca..4069346 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -66,3 +66,26 @@ pub fn subfield_ok_test() { }) assert val == "Puffy" } + +pub fn field_metatable_test() { + let lua = glua.new() + let #(lua, data) = glua.alloc_table(lua, []) + let metatable = + glua.table([ + #( + glua.string("__index"), + glua.function(fn(lua, _args) { #(lua, [glua.string("pong")]) }), + ), + ]) + let assert Ok(#(lua, [table])) = + glua.call_function_by_name(lua, ["setmetatable"], [data, metatable]) + let assert Ok(#(_lua, val)) = + deser.run(lua, table, { + use pong <- deser.field( + glua.str_ref("aasdlkjghasddlkjghasddklgjh;ksjdh"), + deser.string, + ) + deser.success(pong) + }) + assert val == "pong" +} From ca0bcea8ae4db16d6cc80ebd37914e3e1eca7242 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:44:10 -1000 Subject: [PATCH 15/41] Remove Value --- src/deser.gleam | 28 +++++-- src/glua.gleam | 148 +++++++----------------------------- src/glua_ffi.erl | 61 +++++++-------- test/deserialize_test.gleam | 70 ++++++++--------- test/glua_test.gleam | 1 + 5 files changed, 110 insertions(+), 198 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 6f37558..014e743 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -11,7 +11,7 @@ import gleam/option.{type Option, None, Some} import gleam/pair import gleam/result import gleam/string -import glua.{type Lua, type Value, type ValueRef} +import glua.{type Lua, type ValueRef} pub opaque type Deserializer(t) { Deserializer(function: fn(Lua, ValueRef) -> Return(t)) @@ -77,10 +77,14 @@ fn index( index(lua, path, [key, ..position], inner, data, handle_miss) } // NOTE: I don't feel comfortable matching on this - Error(glua.KeyNotFound) -> { + Error(glua.KeyNotFound) + | Error(glua.LuaRuntimeException( + exception: glua.IllegalIndex(_, _), + state: _, + )) -> { handle_miss(lua, data, [key, ..position]) } - Error(_err) -> { + Error(err) -> { let #(default, lua, _) = inner(lua, data) #(default, lua, [DeserializeError("Table", classify(data), [])]) |> push_path(list.reverse(position)) @@ -129,7 +133,7 @@ fn to_string(lua: Lua, val: ValueRef) { "UserDef" -> "userdefined: " <> string.inspect(val) _ -> { { - case glua.ref_call_function_by_name(lua, ["tostring"], [val]) { + case glua.call_function_by_name(lua, ["tostring"], [val]) { Ok(#(lua, [value])) -> { run(lua, value, string) |> result.map(pair.second) @@ -245,8 +249,22 @@ pub const user_defined: 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: ValueRef) -> Return(dynamic.Dynamic) { - run_dynamic_function(lua, data, "UserDef", dynamic.nil()) + let ret = run_dynamic_function(lua, data, "UserDef", dynamic.nil()) + let #(dyn, lua, errs) = ret + case errs { + [] -> + case unwrap_userdata(dyn) { + Ok(dyn) -> #(dyn, lua, []) + Error(Nil) -> #(dynamic.nil(), lua, [ + DeserializeError("UserDef", classify(data), []), + ]) + } + _ -> ret + } } pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { diff --git a/src/glua.gleam b/src/glua.gleam index d18ab8b..da4de0d 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -4,6 +4,7 @@ import gleam/dynamic import gleam/dynamic/decode +import gleam/int import gleam/list import gleam/result import gleam/string @@ -36,55 +37,40 @@ pub type LuaRuntimeExceptionKind { /// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument. AssertError(message: String) /// An exception that could not be identified - UnknownException + UnknownException(dynamic.Dynamic) } /// The exception that happens when a functi /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk -/// Represents a value that can be passed to the Lua environment. -pub type Value - /// Represents a reference to a value inside the Lua environment. /// /// Each one of the functions that returns values from the Lua environment has a `ref_` counterpart /// that will return references to the values instead of decoding them. pub type ValueRef -@external(erlang, "glua_ffi", "coerce") -pub fn str_ref(str: String) -> ValueRef - -@external(erlang, "glua_ffi", "coerce") -pub fn int_ref(str: Int) -> ValueRef - @external(erlang, "glua_ffi", "coerce_nil") -pub fn nil() -> Value - -@external(erlang, "glua_ffi", "coerce") -pub fn string(v: String) -> Value +pub fn nil() -> ValueRef @external(erlang, "glua_ffi", "coerce") -pub fn bool(v: Bool) -> Value +pub fn string(v: String) -> ValueRef @external(erlang, "glua_ffi", "coerce") -pub fn int(v: Int) -> Value +pub fn bool(v: Bool) -> ValueRef -@external(erlang, "glua_ffi", "coerce") -pub fn float(v: Float) -> Value +pub fn int(v: Int) -> ValueRef { + float(int.to_float(v)) +} @external(erlang, "glua_ffi", "coerce") -pub fn table(values: List(#(Value, Value))) -> Value +pub fn float(v: Float) -> ValueRef -pub fn alloc_table(lua: Lua, values: List(#(Value, Value))) -> #(Lua, Value) { - let #(val, lua) = do_alloc_table(values, lua) - #(lua, val) -} +@external(erlang, "glua_ffi", "encode_table") +pub fn table(lua: Lua, values: List(#(ValueRef, ValueRef))) -> #(Lua, ValueRef) -pub fn alloc_userdata(lua: Lua, a: anything) -> #(Lua, Value) { - let #(val, lua) = do_alloc_userdata(a, lua) - #(lua, val) -} +@external(erlang, "glua_ffi", "encode_userdata") +pub fn userdata(lua: Lua, val: anything) -> #(Lua, ValueRef) pub fn table_decoder( keys_decoder: decode.Decoder(a), @@ -99,67 +85,15 @@ pub fn table_decoder( decode.list(of: inner) } -pub fn function( - f: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value { - // we need a little wrapper for functions to satisfy luerl's order of arguments and return value type - wrap_function(f) -} - -pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { +pub fn list(values: List(a), encoder: fn(a) -> ValueRef) -> List(ValueRef) { list.map(values, encoder) } -/// Encodes any Gleam value as a reference that can be passed to a Lua program. -/// -/// Deferencing a userdata value inside Lua code will cause a Lua exception. -/// -/// ## Examples -/// -/// ```gleam -/// pub type User { -/// User(name: String, is_admin: Bool) -/// } -/// -/// let user_decoder = { -/// use name <- decode.field(1, decode.string) -/// use is_admin <- decode.field(2, decode.bool) -/// decode.success(User(name:, is_admin:)) -/// } -/// -/// let state = glua.new() -/// let assert Ok(state) = glua.set( -/// state:, -/// keys: ["a_user"], -/// value: glua.userdata(User(name: "Jhon Doe", is_admin: False)) -/// ) -/// -/// let assert Ok(#(_, [result])) = glua.eval(state:, code: "return a_user", using: user_decoder) -/// assert result == User("Jhon Doe", False) -/// ``` -/// -/// ```gleam -/// pub type Person { -/// Person(name: String, email: String) -/// } -/// -/// let state = glua.new() -/// let assert Ok(lua) = glua.set( -/// state:, -/// keys: ["lucy"], -/// value: glua.userdata(Person(name: "Lucy", email: "lucy@example.com")) -/// ) -/// -/// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_), _)) = -/// glua.eval(state:, code: "return lucy.email", using: decode.string) -/// ``` -@external(erlang, "glua_ffi", "coerce_userdata") -pub fn userdata(v: anything) -> Value - @external(erlang, "glua_ffi", "wrap_fun") -fn wrap_function( - fun: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value +pub fn function( + lua: Lua, + fun: fn(Lua, List(ValueRef)) -> #(Lua, List(ValueRef)), +) -> #(Lua, ValueRef) /// Creates a new Lua VM instance @external(erlang, "luerl", "init") @@ -220,7 +154,7 @@ pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) } @external(erlang, "glua_ffi", "sandbox_fun") -fn sandbox_fun(msg: String) -> Value +fn sandbox_fun(msg: String) -> ValueRef /// Gets a private value that is not exposed to the Lua runtime. /// @@ -289,7 +223,7 @@ pub fn get( pub fn set( state lua: Lua, keys keys: List(String), - value val: Value, + value val: ValueRef, ) -> Result(Lua, LuaError) { let state = { use acc, key <- list.try_fold(keys, #([], lua)) @@ -329,7 +263,7 @@ pub fn set_private(state lua: Lua, key key: String, value value: a) -> Lua { pub fn set_api( lua: Lua, keys: List(String), - values: List(#(String, Value)), + values: List(#(String, ValueRef)), ) -> Result(Lua, LuaError) { use state, #(key, val) <- list.try_fold(values, lua) set(state, list.append(keys, [key]), val) @@ -366,11 +300,9 @@ pub fn set_lua_paths( set(lua, ["package", "path"], paths) } +// TODO: Fix @external(erlang, "luerl_heap", "alloc_table") -fn do_alloc_table(content: List(a), lua: Lua) -> #(Value, Lua) - -@external(erlang, "luerl_heap", "alloc_userdata") -fn do_alloc_userdata(a: anything, lua: Lua) -> #(Value, Lua) +fn do_alloc_table(content: List(a), lua: Lua) -> #(ValueRef, Lua) @external(erlang, "glua_ffi", "get_private") fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) @@ -468,53 +400,27 @@ fn do_eval_file( path: String, ) -> Result(#(Lua, List(ValueRef)), LuaError) -/// Same as `glua.call_function`, but returns references to the values instead of decode them. -pub fn call_function( - state lua: Lua, - ref fun: ValueRef, - args args: List(Value), -) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_call_function(lua, fun, args) -} - @external(erlang, "glua_ffi", "call_function") fn do_call_function( - lua: Lua, - fun: ValueRef, - args: List(Value), -) -> Result(#(Lua, List(ValueRef)), LuaError) - -/// Same as `glua.call_function_by_name`, but it chains `glua.ref_get` with `glua.ref_call_function` instead of `glua.call_function` -pub fn call_function_by_name( - state lua: Lua, - keys keys: List(String), - args args: List(Value), -) -> Result(#(Lua, List(ValueRef)), LuaError) { - use fun <- result.try(get(lua, keys)) - call_function(lua, fun, args) -} - -@external(erlang, "glua_ffi", "ref_call_function") -fn do_ref_call_function( lua: Lua, fun: ValueRef, args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) -pub fn ref_call_function( +pub fn call_function( state lua: Lua, ref fun: ValueRef, args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_call_function(lua, fun, args) + do_call_function(lua, fun, args) } /// Same as `glua.call_function_by_name`, but it chains `glua.ref_get` with `glua.ref_call_function` instead of `glua.call_function` -pub fn ref_call_function_by_name( +pub fn call_function_by_name( state lua: Lua, keys keys: List(String), args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { use fun <- result.try(get(lua, keys)) - ref_call_function(lua, fun, args) + call_function(lua, fun, args) } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 3951dab..1090a00 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -2,10 +2,9 @@ -import(luerl_lib, [lua_error/2]). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, - get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, - eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, - alloc/2, classify/1, ref_call_function/3]). +-export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, + get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, + eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1]). %% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam maybe_process_userdata(Lst) when is_list(Lst) -> @@ -38,7 +37,7 @@ classify(Bool) when is_boolean(Bool) -> <<"Bool">>; classify(Binary) when is_binary(Binary) -> <<"String">>; -classify(N) when is_float(N) -> +classify(N) when is_number(N) -> <<"Number">>; classify({tref,_}) -> <<"Table">>; @@ -125,8 +124,8 @@ map_error({lua_error, {badarith, Operator, Args}, State}) -> {lua_runtime_exception, {bad_arith, FormattedOperator, FormattedArgs}, State}; map_error({lua_error, {assert_error, _} = Error, State}) -> {lua_runtime_exception, Error, State}; -map_error({lua_error, _, State}) -> - {lua_runtime_exception, unknown_exception, State}; +map_error({lua_error, Dynamic, State}) -> + {lua_runtime_exception, {unknown_exception, Dynamic}, State}; map_error(_) -> unknown_error. @@ -139,21 +138,27 @@ coerce_nil() -> coerce_userdata(X) -> {userdata, X}. -alloc(St0, Value) when is_list(Value) -> - {Enc, St1} = luerl_heap:alloc_table(Value, St0), - {St1, Enc}; -alloc(St0, {usrdef,_}=Value) -> - {Enc, St1} = luerl_heap:alloc_userdata(Value, St0), - {St1, Enc}; -alloc(St0, Other) -> - {St0, Other}. - -wrap_fun(Fun) -> - fun(Args, State) -> - Decoded = luerl:decode_list(Args, State), - {NewState, Ret} = Fun(State, Decoded), - luerl:encode_list(Ret, NewState) - end. +unwrap_userdata({userdata, Data}) -> + {ok, Data}; +unwrap_userdata(_) -> + {error, nil}. + +encode_table(State, Values) -> + % io:format("THING ~p~n, ~p~n", [State, Values]), + {Data, St} = luerl_heap:alloc_table(Values, State), + {St, Data}. + +encode_userdata(State, Values) -> + {Data, St} = luerl_heap:alloc_userdata(Values, State), + {St, Data}. + +wrap_fun(State, Fun) -> + NewFun = fun(Args, St0) -> + {St1, Return} = Fun(St0, Args), + {Return, St1} + end, + {T, St} = luerl:encode(NewFun, State), + {St, T}. sandbox_fun(Msg) -> fun(_, State) -> {error, map_error(lua_error({error_call, [Msg]}, State))} end. @@ -162,7 +167,7 @@ sandbox_fun(Msg) -> get_table_key(Lua, Table, Key) -> case luerl:get_table_key(Table, Key, Lua) of {ok, nil, _} -> - {error, nil}; + {error, key_not_found}; {ok, Value, Lua} -> {ok, {Lua, Value}}; Other -> @@ -190,11 +195,7 @@ get_table_keys_dec(Lua, Keys) -> end. set_table_keys(Lua, Keys, Value) -> - SetFun = case is_encoded(Value) of - true -> fun luerl:set_table_keys/3; - false -> fun luerl:set_table_keys_dec/3 - end, - to_gleam(SetFun(Keys, Value, Lua)). + to_gleam(luerl:set_table_keys(Keys, Value, Lua)). load(Lua, Code) -> to_gleam(luerl:load( @@ -227,10 +228,6 @@ eval_file_dec(Lua, Path) -> unicode:characters_to_list(Path), Lua)). call_function(Lua, Fun, Args) -> - {EncodedArgs, State} = encode_list(Args, Lua), - to_gleam(luerl:call(Fun, EncodedArgs, State)). - -ref_call_function(Lua, Fun, Args) -> to_gleam(luerl:call(Fun, Args, Lua)). call_function_dec(Lua, Fun, Args) -> diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 4069346..2406242 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -2,41 +2,34 @@ import deser import gleam/dynamic import glua -/// Warning: Can throw -@external(erlang, "luerl", "encode") -fn encode(a: anything, lua: glua.Lua) -> #(glua.ValueRef, glua.Lua) - @external(erlang, "glua_ffi", "coerce") fn coerce_dynamic(a: anything) -> dynamic.Dynamic -pub fn decoder_test() { +pub fn deserializer_test() { let lua = glua.new() - let #(ref, lua) = encode("Hello", lua) - let assert Ok(#(_lua, "Hello")) = deser.run(lua, ref, deser.string) + let assert Ok(#(_lua, "Hello")) = + deser.run(lua, glua.string("Hello"), deser.string) - let #(ref, lua) = encode(42.0, lua) - let assert Ok(#(_lua, 42.0)) = deser.run(lua, ref, deser.number) + let assert Ok(#(_lua, 42.0)) = deser.run(lua, glua.int(42), deser.number) - let #(ref, lua) = encode(False, lua) - let assert Ok(#(_lua, False)) = deser.run(lua, ref, deser.bool) + let assert Ok(#(_lua, False)) = deser.run(lua, glua.bool(False), deser.bool) - let userdef = ["this", "is", "some", "random", "data"] |> glua.userdata - let #(ref, lua) = encode(userdef, lua) - let assert Ok(#(_lua, udef)) = deser.run(lua, ref, deser.user_defined) - assert udef == coerce_dynamic(userdef) + let data = ["this", "is", "some", "random", "data"] + let #(lua, ref) = data |> glua.userdata(lua, _) + let assert Ok(#(_lua, userdefined)) = deser.run(lua, ref, deser.user_defined) + assert userdefined == coerce_dynamic(data) } pub fn field_ok_test() { let lua = glua.new() - let data = - glua.table([ + let #(lua, data) = + glua.table(lua, [ #(glua.string("red herring"), glua.string("not here!")), #(glua.string("name"), glua.string("Hina")), ]) - let #(ref, lua) = encode(data, lua) let assert Ok(#(_lua, val)) = - deser.run(lua, ref, { - use str <- deser.field(glua.str_ref("name"), deser.string) + deser.run(lua, data, { + use str <- deser.field(glua.string("name"), deser.string) deser.success(str) }) assert val == "Hina" @@ -44,22 +37,20 @@ pub fn field_ok_test() { pub fn subfield_ok_test() { let lua = glua.new() - let data = - glua.table([ + let #(lua, inner) = + glua.table(lua, [ + #(glua.int(1), glua.string("Puffy")), + #(glua.int(2), glua.string("Lucy")), + ]) + let #(lua, data) = + glua.table(lua, [ #(glua.string("name"), glua.string("Hina")), - #( - glua.string("friends"), - glua.table([ - #(glua.int(1), glua.string("Puffy")), - #(glua.int(2), glua.string("Lucy")), - ]), - ), + #(glua.string("friends"), inner), ]) - let #(ref, lua) = encode(data, lua) let assert Ok(#(_lua, val)) = - deser.run(lua, ref, { + deser.run(lua, data, { use first <- deser.subfield( - [glua.str_ref("friends"), glua.int_ref(1)], + [glua.string("friends"), glua.int(1)], deser.string, ) deser.success(first) @@ -69,20 +60,19 @@ pub fn subfield_ok_test() { pub fn field_metatable_test() { let lua = glua.new() - let #(lua, data) = glua.alloc_table(lua, []) - let metatable = - glua.table([ - #( - glua.string("__index"), - glua.function(fn(lua, _args) { #(lua, [glua.string("pong")]) }), - ), + let #(lua, data) = glua.table(lua, []) + let #(lua, func) = + glua.function(lua, fn(lua, _args) { #(lua, [glua.string("pong")]) }) + let #(lua, metatable) = + glua.table(lua, [ + #(glua.string("__index"), func), ]) let assert Ok(#(lua, [table])) = glua.call_function_by_name(lua, ["setmetatable"], [data, metatable]) let assert Ok(#(_lua, val)) = deser.run(lua, table, { use pong <- deser.field( - glua.str_ref("aasdlkjghasddlkjghasddklgjh;ksjdh"), + glua.string("aasdlkjghasddlkjghasddklgjh;ksjdh"), deser.string, ) deser.success(pong) diff --git a/test/glua_test.gleam b/test/glua_test.gleam index f0579d6..eb52093 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -5,6 +5,7 @@ // import gleam/list // import gleam/option // import gleam/pair +import deserialize_test import gleeunit // import glua From 2ff56106007fa54dca90f2e8f4fc857a2a81821f Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Wed, 31 Dec 2025 01:31:18 -1000 Subject: [PATCH 16/41] Add at --- src/deser.gleam | 17 ++++++++--------- test/deserialize_test.gleam | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 014e743..8f35c85 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -106,15 +106,14 @@ pub fn run( } } -pub fn at(path: List(segment), inner: Deserializer(a)) -> Deserializer(a) { - todo - // Deserializer(function: fn(data) { - // index(path, [], inner.function, data, fn(data, position) { - // let #(default, _) = inner.function(data) - // #(default, [DecodeError("Field", "Nothing", [])]) - // |> push_path(list.reverse(position)) - // }) - // }) +pub fn at(path: List(ValueRef), inner: Deserializer(a)) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + index(lua, path, [], inner.function, data, fn(lua, data, position) { + let #(default, lua, _) = inner.function(lua, data) + #(default, lua, [DeserializeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }) + }) } @external(erlang, "glua_ffi", "get_table_key") diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 2406242..57df566 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -79,3 +79,19 @@ pub fn field_metatable_test() { }) assert val == "pong" } + +pub fn at_ok_test() { + let lua = glua.new() + let #(lua, third) = + glua.table(lua, [#(glua.string("third"), glua.string("hi"))]) + let #(lua, second) = glua.table(lua, [#(glua.string("second"), third)]) + let #(lua, first) = glua.table(lua, [#(glua.string("first"), second)]) + + let third = + deser.at( + [glua.string("first"), glua.string("second"), glua.string("third")], + deser.string, + ) + let assert Ok(#(_lua, hi)) = deser.run(lua, first, third) + assert hi == "hi" +} From 0b1f42202a1a9111fafeee427ecb6ac99d5479fc Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:16:47 -1000 Subject: [PATCH 17/41] Add optionally at --- src/deser.gleam | 12 ++++++------ src/glua.gleam | 7 +++++++ test/deserialize_test.gleam | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 8f35c85..3175a2d 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -5,7 +5,6 @@ import gleam/dict.{type Dict} import gleam/dynamic -import gleam/dynamic/decode.{type Decoder} import gleam/list import gleam/option.{type Option, None, Some} import gleam/pair @@ -191,14 +190,15 @@ pub fn optional_field( } pub fn optionally_at( - path: List(segment), + path: List(ValueRef), default: a, inner: Deserializer(a), ) -> Deserializer(a) { - todo - // Deserializer(function: fn(data) { - // index(path, [], inner.function, data, fn(_, _) { #(default, []) }) - // }) + Deserializer(function: fn(lua, data) { + index(lua, path, [], inner.function, data, fn(_, _, _) { + #(default, lua, []) + }) + }) } fn run_dynamic_function( diff --git a/src/glua.gleam b/src/glua.gleam index da4de0d..021588c 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -6,6 +6,7 @@ import gleam/dynamic import gleam/dynamic/decode import gleam/int import gleam/list +import gleam/pair import gleam/result import gleam/string @@ -69,6 +70,12 @@ pub fn float(v: Float) -> ValueRef @external(erlang, "glua_ffi", "encode_table") pub fn table(lua: Lua, values: List(#(ValueRef, ValueRef))) -> #(Lua, ValueRef) +pub fn table_list(lua: Lua, values: List(ValueRef)) -> #(Lua, ValueRef) { + list.map_fold(values, 1, fn(acc, ref) { #(acc, #(int(acc), ref)) }) + |> pair.second() + |> table(lua, _) +} + @external(erlang, "glua_ffi", "encode_userdata") pub fn userdata(lua: Lua, val: anything) -> #(Lua, ValueRef) diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 57df566..0dbea75 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -95,3 +95,32 @@ pub fn at_ok_test() { let assert Ok(#(_lua, hi)) = deser.run(lua, first, third) assert hi == "hi" } + +pub fn optionally_at_ok_test() { + let lua = glua.new() + let #(lua, nest) = + glua.table(lua, [#(glua.string("ping"), glua.string("pong"))]) + let #(lua, table) = glua.table(lua, [#(glua.string("nested"), nest)]) + let assert Ok(#(_lua, pong)) = + deser.run( + lua, + table, + deser.optionally_at( + [glua.string("nested"), glua.string("ping")], + "miss", + deser.string, + ), + ) + assert "pong" == pong + let assert Ok(#(_lua, miss)) = + deser.run( + lua, + table, + deser.optionally_at( + [glua.string("nestedd"), glua.string("ping")], + "miss", + deser.string, + ), + ) + assert "miss" == miss +} From 131d97860e119a690304380b2176cc05507515fd Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:06:53 -1000 Subject: [PATCH 18/41] Add optional field --- src/deser.gleam | 40 +++++++++++++++++++++++-------------- test/deserialize_test.gleam | 27 +++++++++++++++++++++++++ test/glua_test.gleam | 1 - 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 3175a2d..f865ada 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -168,25 +168,35 @@ pub fn deser_error( pub fn classify(a: anything) -> String pub fn optional_field( - key: name, + key: ValueRef, default: t, field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { - todo - // Deserializer(function: fn(data) { - // let #(out, errors1) = - // case bare_index(data, key) { - // Ok(Some(data)) -> field_decoder.function(data) - // Ok(None) -> #(default, []) - // Error(kind) -> #(default, [ - // DecodeError(kind, dynamic.classify(data), []), - // ]) - // } - // |> push_path([key]) - // let #(out, errors2) = next(out).function(data) - // #(out, list.append(errors1, errors2)) - // }) + Deserializer(function: fn(lua, data) { + let #(out, lua, errors1) = + case get_table_key(lua, data, key) { + Ok(#(lua, data)) -> { + field_decoder.function(lua, data) + } + // NOTE: I don't feel comfortable matching on this + Error(glua.KeyNotFound) + | Error(glua.LuaRuntimeException( + exception: glua.IllegalIndex(_, _), + state: _, + )) -> { + #(default, lua, []) + } + Error(_err) -> { + #(default, lua, [ + DeserializeError("Table", classify(data), []), + ]) + } + } + |> push_path([key]) + let #(out, lua, errors2) = next(out).function(lua, data) + #(out, lua, list.append(errors1, errors2)) + }) } pub fn optionally_at( diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 0dbea75..28d5fac 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -124,3 +124,30 @@ pub fn optionally_at_ok_test() { ) assert "miss" == miss } + +pub fn optional_field_ok_test() { + let lua = glua.new() + let #(lua, table) = + glua.table(lua, [#(glua.string("bullseye"), glua.string("hit"))]) + let assert Ok(#(_lua, hit)) = + deser.run(lua, table, { + use hit <- deser.optional_field( + glua.string("bullseye"), + "doh i missed", + deser.string, + ) + deser.success(hit) + }) + assert hit == "hit" + let assert Ok(#(_lua, miss)) = + deser.run(lua, table, { + use hit <- deser.optional_field( + glua.string("bull'seye"), + "doh i missed", + deser.string, + ) + deser.success(hit) + }) + + assert miss == "doh i missed" +} diff --git a/test/glua_test.gleam b/test/glua_test.gleam index eb52093..f0579d6 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -5,7 +5,6 @@ // import gleam/list // import gleam/option // import gleam/pair -import deserialize_test import gleeunit // import glua From 27be6efe495360fd86032951bed7dfa844fe9f53 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:15:16 -1000 Subject: [PATCH 19/41] Add some err tests --- test/deserialize_test.gleam | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 28d5fac..5067dfc 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -1,5 +1,6 @@ import deser import gleam/dynamic +import gleam/list import glua @external(erlang, "glua_ffi", "coerce") @@ -35,6 +36,18 @@ pub fn field_ok_test() { assert val == "Hina" } +pub fn field_err_test() { + let lua = glua.new() + let #(lua, data) = + glua.table(lua, [#(glua.string("red herring"), glua.string("nope"))]) + let assert Error([deser.DeserializeError("Field", "Nothing", path)]) = + deser.run(lua, data, { + use str <- deser.field(glua.string("name"), deser.string) + deser.success(str) + }) + assert path == [glua.string("name")] +} + pub fn subfield_ok_test() { let lua = glua.new() let #(lua, inner) = @@ -58,6 +71,29 @@ pub fn subfield_ok_test() { assert val == "Puffy" } +pub fn subfield_err_test() { + let lua = glua.new() + let #(lua, inner) = + glua.table(lua, [ + #(glua.int(4), glua.string("Puffy")), + #(glua.int(2), glua.string("Lucy")), + ]) + let #(lua, data) = + glua.table(lua, [ + #(glua.string("name"), glua.string("Hina")), + #(glua.string("friends"), inner), + ]) + let assert Error([deser.DeserializeError("Field", "Nothing", path)]) = + deser.run(lua, data, { + use first <- deser.subfield( + [glua.string("friends"), glua.int(1)], + deser.string, + ) + deser.success(first) + }) + assert path == [glua.string("friends"), glua.int(1)] +} + pub fn field_metatable_test() { let lua = glua.new() let #(lua, data) = glua.table(lua, []) @@ -96,6 +132,27 @@ pub fn at_ok_test() { assert hi == "hi" } +pub fn at_err_test() { + let lua = glua.new() + let #(lua, third) = + glua.table(lua, [#(glua.string("third"), glua.string("hi"))]) + let #(lua, second) = glua.table(lua, [#(glua.string("second"), third)]) + let #(lua, first) = glua.table(lua, [#(glua.string("first"), second)]) + + let third = + deser.at( + [ + glua.string("first"), + glua.string("third"), + glua.string("second"), + ], + deser.string, + ) + let assert Error([deser.DeserializeError("Field", "Nothing", path)]) = + deser.run(lua, first, third) + assert path == ["first", "third"] |> list.map(glua.string) +} + pub fn optionally_at_ok_test() { let lua = glua.new() let #(lua, nest) = From f6fdd8557a3891c8969ead8d371032ed2f9a94e6 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:13:12 -1000 Subject: [PATCH 20/41] Add deser.dict --- src/deser.gleam | 108 +++++++++++++++++++++++++++--------- src/glua_ffi.erl | 10 +++- test/deserialize_test.gleam | 37 +++++++++++- 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index f865ada..7a80492 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -5,6 +5,8 @@ import gleam/dict.{type Dict} import gleam/dynamic +import gleam/dynamic/decode +import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/pair @@ -38,7 +40,7 @@ pub fn subfield( ) -> Deserializer(final) { Deserializer(function: fn(lua, data) { let #(out, lua, errors1) = - index( + index_into( lua, field_path, [], @@ -55,7 +57,7 @@ pub fn subfield( }) } -fn index( +fn index_into( lua: Lua, path: List(ValueRef), position: List(ValueRef), @@ -73,7 +75,7 @@ fn index( [key, ..path] -> { case get_table_key(lua, data, key) { Ok(#(lua, data)) -> { - index(lua, path, [key, ..position], inner, data, handle_miss) + index_into(lua, path, [key, ..position], inner, data, handle_miss) } // NOTE: I don't feel comfortable matching on this Error(glua.KeyNotFound) @@ -107,7 +109,7 @@ pub fn run( pub fn at(path: List(ValueRef), inner: Deserializer(a)) -> Deserializer(a) { Deserializer(function: fn(lua, data) { - index(lua, path, [], inner.function, data, fn(lua, data, position) { + index_into(lua, path, [], inner.function, data, fn(lua, data, position) { let #(default, lua, _) = inner.function(lua, data) #(default, lua, [DeserializeError("Field", "Nothing", [])]) |> push_path(list.reverse(position)) @@ -125,7 +127,8 @@ fn get_table_key( fn to_string(lua: Lua, val: ValueRef) { case classify(val) { "Null" -> "" - "Number" -> decode(val, lua) + "Int" -> decode(val, lua) + "Float" -> decode(val, lua) "String" -> decode(val, lua) "Unknown" -> "" "UserDef" -> "userdefined: " <> string.inspect(val) @@ -205,7 +208,7 @@ pub fn optionally_at( inner: Deserializer(a), ) -> Deserializer(a) { Deserializer(function: fn(lua, data) { - index(lua, path, [], inner.function, data, fn(_, _, _) { + index_into(lua, path, [], inner.function, data, fn(_, _, _) { #(default, lua, []) }) }) @@ -245,7 +248,23 @@ fn deser_bool(lua, data: ValueRef) -> Return(Bool) { pub const number: Deserializer(Float) = Deserializer(deser_num) fn deser_num(lua, data: ValueRef) -> Return(Float) { - run_dynamic_function(lua, data, "Number", 0.0) + let got = classify(data) + case got { + "Float" -> #(decode(data, lua), lua, []) + "Int" -> { + let int: Int = decode(data, lua) + #(int.to_float(int), lua, []) + } + _ -> #(0.0, lua, [ + DeserializeError("Number", got, []), + ]) + } +} + +pub const index: Deserializer(Int) = Deserializer(deser_index) + +fn deser_index(lua, data: ValueRef) -> Return(Int) { + run_dynamic_function(lua, data, "Int", 0) } pub const raw: Deserializer(ValueRef) = Deserializer(decode_raw) @@ -277,33 +296,70 @@ fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { } pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { - todo - // Deserializer(fn(data) { - // decode_list(data, inner.function, fn(p, k) { push_path(p, [k]) }, 0, []) - // }) + Deserializer(fn(lua, data) { todo }) +} + +pub fn list_strict(of inner: Deserializer(a)) -> Deserializer(List(a)) { + Deserializer(fn(lua, data) { todo }) } pub fn dict( key: Deserializer(key), value: Deserializer(value), ) -> Deserializer(Dict(key, value)) { - todo - // Deserializer(fn(data) { - // case decode_dict(data) { - // Error(_) -> #(dict.new(), decode_error("Dict", data)) - // Ok(dict) -> - // dict.fold(dict, #(dict.new(), []), fn(a, k, v) { - // // 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(a, k, v, key.function, value.function) - // [_, ..] -> a - // } - // }) - // } - // }) + Deserializer(fn(lua, data) { + let class = classify(data) + case class { + "Table" -> { + let pairs = get_table_pairs(lua, data) + list.fold(pairs, #(dict.new(), lua, []), fn(a, pair) { + let #(k, v) = pair + // 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.2 { + [] -> fold_dict(lua, a, k, v, key.function, value.function) + [_, ..] -> a + } + }) + } + _ -> #(dict.new(), lua, [DeserializeError("Table", class, [])]) + } + }) } +fn fold_dict( + lua: Lua, + acc: #(Dict(k, v), Lua, List(DeserializeError)), + key: ValueRef, + value: ValueRef, + key_decoder: fn(Lua, ValueRef) -> Return(k), + value_decoder: fn(Lua, ValueRef) -> Return(v), +) -> Return(Dict(k, v)) { + // First we decode the key. + case key_decoder(lua, key) { + #(key, lua, []) -> + // Then we decode the value. + case value_decoder(lua, value) { + #(value, lua, []) -> { + // 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, acc.2) + } + #(_, lua, errors) -> + push_path(#(dict.new(), lua, errors), [glua.string("values")]) + } + #(_, lua, errors) -> + push_path(#(dict.new(), lua, errors), [glua.string("keys")]) + } +} + +/// Preconditions: `table` is a lua table +@external(erlang, "glua_ffi", "get_table_pairs") +pub fn get_table_pairs( + state: glua.Lua, + table: ValueRef, +) -> List(#(ValueRef, ValueRef)) + pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { Deserializer(function: fn(lua, data) { case classify(data) { diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1090a00..40fce42 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -1,10 +1,12 @@ -module(glua_ffi). -import(luerl_lib, [lua_error/2]). +-include_lib("luerl/include/luerl.hrl"). -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, - eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1]). + eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, + get_table_pairs/2]). %% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam maybe_process_userdata(Lst) when is_list(Lst) -> @@ -54,6 +56,12 @@ classify({erl_mfa,_,_,_}) -> classify(_) -> <<"Unknown">>. +get_table_pairs(St, #tref{}=T) -> + #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), + Fun = fun (K, V, Acc) -> [{K, V} | Acc] end, + Ts = ttdict:fold(Fun, [], Dict), + array:sparse_foldr(Fun, Ts, Arr). + %% helper to determine if a value is encoded or not %% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 %% Also see (luerl 1.5.1): https://hexdocs.pm/luerl/luerl.html#t:luerldata/0 diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 5067dfc..f179fc0 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -1,7 +1,10 @@ import deser +import gleam/dict import gleam/dynamic +import gleam/int import gleam/list -import glua +import gleam/pair +import glua.{type ValueRef} @external(erlang, "glua_ffi", "coerce") fn coerce_dynamic(a: anything) -> dynamic.Dynamic @@ -208,3 +211,35 @@ pub fn optional_field_ok_test() { assert miss == "doh i missed" } + +pub fn table_decode_test() { + let lua = glua.new() + let points = [#("Mark", 39.0), #("Mason", 66.0), #("Mabel", 42.0)] + let #(lua, data) = + glua.table( + lua, + points + |> list.map(fn(pair) { #(glua.string(pair.0), glua.float(pair.1)) }), + ) + let assert Ok(#(_lua, dict)) = + deser.run(lua, data, deser.dict(deser.string, deser.number)) + assert dict == dict.from_list(points) +} + +pub fn table_list_decode_test() { + let lua = glua.new() + let meanings = [ + #(42, "the universe"), + #(39, "HATSUNE MIKU"), + #(4, "death"), + ] + let #(lua, data) = + glua.table( + lua, + meanings + |> list.map(fn(pair) { #(glua.int(pair.0), glua.string(pair.1)) }), + ) + let assert Ok(#(_lua, dict)) = + deser.run(lua, data, deser.dict(deser.index, deser.string)) + assert dict == dict.from_list(meanings) +} From 435eb6618a0dac67e14e145f616113bef010c5fe Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:43:32 -1000 Subject: [PATCH 21/41] Alternative dict implementation --- src/deser.gleam | 14 ++++++++++---- src/glua_ffi.erl | 15 ++++++++++++--- test/glua_test.gleam | 4 +++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 7a80492..e59bea0 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -311,9 +311,7 @@ pub fn dict( let class = classify(data) case class { "Table" -> { - let pairs = get_table_pairs(lua, data) - list.fold(pairs, #(dict.new(), lua, []), fn(a, pair) { - let #(k, v) = pair + get_table_transform(lua, data, #(dict.new(), lua, []), 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.2 { @@ -327,6 +325,14 @@ pub fn dict( }) } +@external(erlang, "glua_ffi", "get_table_transform") +fn get_table_transform( + lua: Lua, + table: ValueRef, + accumulator: acc, + func: fn(ValueRef, ValueRef, acc) -> acc, +) -> acc + fn fold_dict( lua: Lua, acc: #(Dict(k, v), Lua, List(DeserializeError)), @@ -343,7 +349,7 @@ fn fold_dict( #(value, lua, []) -> { // 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, acc.2) + #(dict, lua, acc.2) } #(_, lua, errors) -> push_path(#(dict.new(), lua, errors), [glua.string("values")]) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 40fce42..b4c3e93 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -6,7 +6,7 @@ -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, - get_table_pairs/2]). + get_table_pairs/2, get_table_transform/4]). %% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam maybe_process_userdata(Lst) when is_list(Lst) -> @@ -39,8 +39,10 @@ classify(Bool) when is_boolean(Bool) -> <<"Bool">>; classify(Binary) when is_binary(Binary) -> <<"String">>; -classify(N) when is_number(N) -> - <<"Number">>; +classify(N) when is_integer(N) -> + <<"Int">>; +classify(N) when is_float(N) -> + <<"Float">>; classify({tref,_}) -> <<"Table">>; classify({usdref,_}) -> @@ -62,6 +64,13 @@ get_table_pairs(St, #tref{}=T) -> Ts = ttdict:fold(Fun, [], Dict), array:sparse_foldr(Fun, Ts, Arr). +get_table_transform(St, #tref{}=T, Acc, Func) + when is_function(Func, 3) -> + #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), + Acc1 = ttdict:fold(Func, Acc, Dict), + array:sparse_foldl(Func, Acc1, Arr). + + %% helper to determine if a value is encoded or not %% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 %% Also see (luerl 1.5.1): https://hexdocs.pm/luerl/luerl.html#t:luerldata/0 diff --git a/test/glua_test.gleam b/test/glua_test.gleam index f0579d6..2b1f0fc 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -5,12 +5,14 @@ // import gleam/list // import gleam/option // import gleam/pair +import deserialize_test import gleeunit // import glua pub fn main() -> Nil { - gleeunit.main() + // gleeunit.main() + deserialize_test.deserializer_test() } // pub fn get_table_test() { // let lua = glua.new() From 6e781bbc065ea6773e693c5f8ec81c75a155bce2 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:50:08 -1000 Subject: [PATCH 22/41] Add map --- src/deser.gleam | 17 +++++------------ src/glua_ffi.erl | 6 ------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index e59bea0..81892c3 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -325,6 +325,7 @@ pub fn dict( }) } +/// Preconditions: `table` is a lua table @external(erlang, "glua_ffi", "get_table_transform") fn get_table_transform( lua: Lua, @@ -359,13 +360,6 @@ fn fold_dict( } } -/// Preconditions: `table` is a lua table -@external(erlang, "glua_ffi", "get_table_pairs") -pub fn get_table_pairs( - state: glua.Lua, - table: ValueRef, -) -> List(#(ValueRef, ValueRef)) - pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { Deserializer(function: fn(lua, data) { case classify(data) { @@ -379,11 +373,10 @@ pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { } pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) { - todo - // Deserializer(function: fn(d) { - // let #(data, errors) = decoder.function(d) - // #(transformer(data), errors) - // }) + Deserializer(function: fn(lua, d) { + let #(data, lua, errors) = decoder.function(lua, d) + #(transformer(data), lua, errors) + }) } pub fn map_errors( diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index b4c3e93..1cd3fdd 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -58,12 +58,6 @@ classify({erl_mfa,_,_,_}) -> classify(_) -> <<"Unknown">>. -get_table_pairs(St, #tref{}=T) -> - #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), - Fun = fun (K, V, Acc) -> [{K, V} | Acc] end, - Ts = ttdict:fold(Fun, [], Dict), - array:sparse_foldr(Fun, Ts, Arr). - get_table_transform(St, #tref{}=T, Acc, Func) when is_function(Func, 3) -> #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), From c200f524be4012cf8ce366d35417367d069a4f9a Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Thu, 1 Jan 2026 03:43:06 -1000 Subject: [PATCH 23/41] Add list deserializer --- src/deser.gleam | 43 ++++++++++++++++++++++++--- src/glua.gleam | 2 +- src/glua_ffi.erl | 31 +++++++++++++++++++- test/deserialize_test.gleam | 58 +++++++++++++++++++++++++++++++++++-- test/glua_test.gleam | 3 +- 5 files changed, 126 insertions(+), 11 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 81892c3..f6b78d2 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -295,13 +295,48 @@ fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { } } +/// 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) { todo }) + Deserializer(fn(lua, data) { + let class = classify(data) + let not_table_list = #([], lua, [DeserializeError("TableList", class, [])]) + case class { + "Table" -> { + let res = + get_table_list_transform(lua, data, #([], lua, []), fn(it, acc) { + case acc.2 { + [] -> { + case inner.function(lua, it) { + #(value, lua, []) -> { + #(list.prepend(acc.0, value), lua, acc.2) + } + #(_, lua, errors) -> + push_path(#([], lua, errors), [glua.string("items")]) + } + } + [_, ..] -> acc + } + }) + case res { + Ok(it) -> it + Error(Nil) -> not_table_list + } + } + _ -> not_table_list + } + }) } -pub fn list_strict(of inner: Deserializer(a)) -> Deserializer(List(a)) { - Deserializer(fn(lua, data) { todo }) -} +@external(erlang, "glua_ffi", "get_table_list_transform") +fn get_table_list_transform( + lua: Lua, + table: ValueRef, + accumulator: acc, + func: fn(ValueRef, acc) -> acc, +) -> Result(acc, Nil) pub fn dict( key: Deserializer(key), diff --git a/src/glua.gleam b/src/glua.gleam index 021588c..4ced40b 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -71,7 +71,7 @@ pub fn float(v: Float) -> ValueRef pub fn table(lua: Lua, values: List(#(ValueRef, ValueRef))) -> #(Lua, ValueRef) pub fn table_list(lua: Lua, values: List(ValueRef)) -> #(Lua, ValueRef) { - list.map_fold(values, 1, fn(acc, ref) { #(acc, #(int(acc), ref)) }) + list.map_fold(values, 1, fn(acc, ref) { #(acc + 1, #(int(acc), ref)) }) |> pair.second() |> table(lua, _) } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1cd3fdd..c196b3d 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -6,7 +6,7 @@ -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, - get_table_pairs/2, get_table_transform/4]). + get_table_transform/4, get_table_list_transform/4]). %% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam maybe_process_userdata(Lst) when is_list(Lst) -> @@ -64,6 +64,35 @@ get_table_transform(St, #tref{}=T, Acc, Func) Acc1 = ttdict:fold(Func, Acc, Dict), array:sparse_foldl(Func, Acc1, Arr). +get_table_list_transform(St, #tref{}=T, Acc, Func) + when is_function(Func, 2) -> + #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), + case Dict of + empty -> do_list_transform(Arr, Acc, Func); + _ -> {error, nil} + end. + +do_list_transform(Arr, Acc, Func) -> + N = array:size(Arr), + try + Wrapped = fun(Idx, Val, {Expected, UserAcc}) -> + case Idx of + Expected -> + {Expected - 1, Func(Val, UserAcc)}; + _ -> throw(hole) + end + end, + {End, Final} = + array:sparse_foldr(Wrapped, {N - 1, Acc}, Arr), + case {N, End} of + {0, _} -> {ok, Final}; + {_, 0} -> {ok, Final}; + _ -> {error, nil} + end + catch + throw:hole -> {error, nil} + end. + %% helper to determine if a value is encoded or not %% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index f179fc0..5203b88 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -1,10 +1,8 @@ import deser import gleam/dict import gleam/dynamic -import gleam/int import gleam/list -import gleam/pair -import glua.{type ValueRef} +import glua @external(erlang, "glua_ffi", "coerce") fn coerce_dynamic(a: anything) -> dynamic.Dynamic @@ -243,3 +241,57 @@ pub fn table_list_decode_test() { deser.run(lua, data, deser.dict(deser.index, deser.string)) assert dict == dict.from_list(meanings) } + +pub fn table_list_ok_test() { + let lua = glua.new() + let greetings = [ + "Hi there", + "Hello there", + "Hey!", + "Hi everyone", + "Hello all", + "Good morning everyone", + ] + let #(lua, data) = + glua.table_list( + lua, + greetings + |> list.map(glua.string), + ) + let assert Ok(#(lua, list)) = deser.run(lua, data, deser.list(deser.string)) + assert list == greetings + + let #(lua, data) = glua.table(lua, []) + let assert Ok(#(_lua, [])) = deser.run(lua, data, deser.list(deser.string)) +} + +pub fn table_list_err_test() { + let lua = glua.new() + let tf = fn(pair: #(Int, String)) { #(glua.int(pair.0), glua.string(pair.1)) } + let not_table_list = Error([deser.DeserializeError("TableList", "Table", [])]) + + // Missing start + let #(lua, data) = + glua.table(lua, [#(2, "a"), #(3, "b"), #(4, "c")] |> list.map(tf)) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list + + // Early start + let #(lua, data) = + glua.table(lua, [#(0, "a"), #(1, "b"), #(2, "c")] |> list.map(tf)) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list + + // Gap + let #(lua, data) = + glua.table(lua, [#(1, "a"), #(2, "b"), #(4, "c")] |> list.map(tf)) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list + + // non number key + let #(lua, data) = + glua.table( + lua, + [#(1, "a"), #(2, "b"), #(3, "c")] + |> list.map(tf) + |> list.prepend(#(glua.string("wrench"), glua.string("in this table"))), + ) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list +} diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 2b1f0fc..eb52093 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -11,8 +11,7 @@ import gleeunit // import glua pub fn main() -> Nil { - // gleeunit.main() - deserialize_test.deserializer_test() + gleeunit.main() } // pub fn get_table_test() { // let lua = glua.new() From 4ed11e3c354f0c8488c590342535f0867f38ffb1 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:55:33 -1000 Subject: [PATCH 24/41] Add then --- src/deser.gleam | 24 +++++++++++------------- test/deserialize_test.gleam | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index f6b78d2..daf942a 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -441,18 +441,17 @@ pub fn collapse_errors( pub fn then( decoder: Deserializer(a), - next: fn(a) -> Deserializer(b), + next: fn(Lua, a) -> Deserializer(b), ) -> Deserializer(b) { - todo - // Deserializer(function: fn(dynamic_data) { - // let #(data, errors) = decoder.function(dynamic_data) - // let decoder = next(data) - // let #(data, _) as layer = decoder.function(dynamic_data) - // case errors { - // [] -> layer - // [_, ..] -> #(data, errors) - // } - // }) + Deserializer(function: fn(lua, dynamic_data) { + let #(data, lua, errors) = decoder.function(lua, dynamic_data) + let decoder = next(lua, data) + let #(data, lua, _) as layer = decoder.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, lua, errors) + } + }) } pub fn one_of( @@ -489,8 +488,7 @@ fn run_decoders( } pub fn failure(zero: a, expected: String) -> Deserializer(a) { - todo - // Deserializer(function: fn(d) { #(zero, glua.new(), deser_error(expected, d)) }) + Deserializer(function: fn(lua, d) { #(zero, lua, deser_error(expected, d)) }) } pub fn recursive(inner: fn() -> Deserializer(a)) -> Deserializer(a) { diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 5203b88..a95bf4a 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -295,3 +295,17 @@ pub fn table_list_err_test() { ) assert deser.run(lua, data, deser.list(deser.string)) == not_table_list } + +pub fn then_test() { + let positive_deser = { + use lua, num <- deser.then(deser.number) + case num >. 0.0 { + False -> deser.failure(0.0, "PositiveNum") + True -> deser.success(num) + } + } + let lua = glua.new() + let assert Ok(#(lua, 4.0)) = deser.run(lua, glua.int(4), positive_deser) + let assert Error([deser.DeserializeError("PositiveNum", "Float", [])]) = + deser.run(lua, glua.int(-4), positive_deser) +} From 8f48603b30e8fc90c0f2fbfae1aba25bfb8c2f26 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 00:26:53 -1000 Subject: [PATCH 25/41] Add functions --- src/deser.gleam | 15 +++++++++++++++ src/glua.gleam | 12 +++++++++--- test/deserialize_test.gleam | 34 ++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index daf942a..80e563e 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -295,6 +295,21 @@ fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { } } +pub const function: Deserializer(glua.Function) = Deserializer(deser_function) + +fn deser_function(lua: Lua, data: ValueRef) -> Return(glua.Function) { + let got = classify(data) + case got == "Function" { + True -> #(coerce_funciton(data), lua, []) + False -> #(coerce_funciton(Nil), lua, [ + 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 diff --git a/src/glua.gleam b/src/glua.gleam index 4ced40b..5a30187 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -51,6 +51,9 @@ pub type Chunk /// that will return references to the values instead of decoding them. pub type ValueRef +/// A `ValueRef` that is a function +pub type Function + @external(erlang, "glua_ffi", "coerce_nil") pub fn nil() -> ValueRef @@ -410,13 +413,13 @@ fn do_eval_file( @external(erlang, "glua_ffi", "call_function") fn do_call_function( lua: Lua, - fun: ValueRef, + fun: Function, args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) pub fn call_function( state lua: Lua, - ref fun: ValueRef, + ref fun: Function, args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { do_call_function(lua, fun, args) @@ -429,5 +432,8 @@ pub fn call_function_by_name( args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { use fun <- result.try(get(lua, keys)) - call_function(lua, fun, args) + call_function(lua, coerce_funciton(fun), args) } + +@external(erlang, "glua_ffi", "coerce") +fn coerce_funciton(func: ValueRef) -> Function diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index a95bf4a..d19adc5 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -1,4 +1,4 @@ -import deser +import deser.{DeserializeError} import gleam/dict import gleam/dynamic import gleam/list @@ -41,7 +41,7 @@ pub fn field_err_test() { let lua = glua.new() let #(lua, data) = glua.table(lua, [#(glua.string("red herring"), glua.string("nope"))]) - let assert Error([deser.DeserializeError("Field", "Nothing", path)]) = + let assert Error([DeserializeError("Field", "Nothing", path)]) = deser.run(lua, data, { use str <- deser.field(glua.string("name"), deser.string) deser.success(str) @@ -84,7 +84,7 @@ pub fn subfield_err_test() { #(glua.string("name"), glua.string("Hina")), #(glua.string("friends"), inner), ]) - let assert Error([deser.DeserializeError("Field", "Nothing", path)]) = + let assert Error([DeserializeError("Field", "Nothing", path)]) = deser.run(lua, data, { use first <- deser.subfield( [glua.string("friends"), glua.int(1)], @@ -149,7 +149,7 @@ pub fn at_err_test() { ], deser.string, ) - let assert Error([deser.DeserializeError("Field", "Nothing", path)]) = + let assert Error([DeserializeError("Field", "Nothing", path)]) = deser.run(lua, first, third) assert path == ["first", "third"] |> list.map(glua.string) } @@ -268,7 +268,7 @@ pub fn table_list_ok_test() { pub fn table_list_err_test() { let lua = glua.new() let tf = fn(pair: #(Int, String)) { #(glua.int(pair.0), glua.string(pair.1)) } - let not_table_list = Error([deser.DeserializeError("TableList", "Table", [])]) + let not_table_list = Error([DeserializeError("TableList", "Table", [])]) // Missing start let #(lua, data) = @@ -306,6 +306,28 @@ pub fn then_test() { } let lua = glua.new() let assert Ok(#(lua, 4.0)) = deser.run(lua, glua.int(4), positive_deser) - let assert Error([deser.DeserializeError("PositiveNum", "Float", [])]) = + let assert Error([DeserializeError("PositiveNum", "Float", [])]) = deser.run(lua, glua.int(-4), positive_deser) } + +pub fn custom_function_ok_test() { + let assert Ok(#(lua, [func])) = + glua.eval(glua.new(), "return function () return 42 end") + let assert Ok(#(lua, func)) = deser.run(lua, func, deser.function) + let assert Ok(#(lua, [num])) = glua.call_function(lua, func, []) + let assert Ok(#(_lua, 42.0)) = deser.run(lua, num, deser.number) +} + +pub fn builtin_function_ok_test() { + let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return string.upper") + let assert Ok(#(lua, func)) = deser.run(lua, func, deser.function) + let assert Ok(#(lua, [str])) = + glua.call_function(lua, func, [glua.string("hello")]) + let assert Ok(#(_lua, "HELLO")) = deser.run(lua, str, deser.string) +} + +pub fn function_err_test() { + let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return string") + let assert Error([DeserializeError("Function", "Table", [])]) = + deser.run(lua, func, deser.function) +} From 2a8868cdf9271eca745fd13056ef985e44afdac7 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:44:36 -1000 Subject: [PATCH 26/41] Update readme --- README.md | 187 +++++++++++++----------------------- src/glua.gleam | 16 ++- src/glua_ffi.erl | 10 +- test/deserialize_test.gleam | 2 +- test/lua/two_numbers.lua | 1 + test/readme_test.gleam | 162 +++++++++++++++++++++++++++++++ 6 files changed, 245 insertions(+), 133 deletions(-) create mode 100644 test/lua/two_numbers.lua create mode 100644 test/readme_test.gleam diff --git a/README.md b/README.md index 3ef2b2d..da16661 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A library for embedding Lua in Gleam applications! [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/glua/) ```sh -gleam add glua@1 +gleam add glua@2 ``` ## Usage @@ -14,7 +14,8 @@ gleam add glua@1 ### Executing Lua code ```gleam -let code = " + let code = + " function greet() return 'Hello from Lua!' end @@ -22,47 +23,54 @@ end return greet() " -let assert Ok(#(_state, [result])) = glua.eval( - state: glua.new(), - code:, - using: decode.string -) + // Make a fresh instance lua instance + let lua = glua.new() + let assert Ok(#(_state, [result])) = glua.eval(state: lua, code:) -assert result == "Hello from Lua!" + assert result == glua.string("Hello from Lua!") ``` -### Parsing a chunk, then executing it +### Decoding output +The `deser` api is similar to the `decode` +```gleam +pub type Project { + Project(name: String, language: String) +} +let code = "return { name = 'glua', written_in = 'gleam'}" +let assert Ok(#(lua, [table])) = glua.eval(glua.new(), code) + +let deserializer = { + use name <- deser.field(glua.string("name"), deser.string) + use language <- deser.field(glua.string("written_in"), deser.string) + deser.success(Project(name:, language:)) +} +let assert Ok(#(_lua, project)) = deser.run(lua, table, deserializer) +assert project == Project(name: "glua", language: "gleam") +``` + +### Parsing a chunk, then executing it ```gleam let code = "return 'this is a chunk of Lua code'" let assert Ok(#(state, chunk)) = glua.load(state: glua.new(), code:) -let assert Ok(#(_state, [result])) = - glua.eval_chunk(state:, chunk:, using: decode.string) +let assert Ok(#(_state, [result])) = glua.eval_chunk(state:, chunk:) -assert result == "this is a chunk of Lua code" +assert result == glua.string("this is a chunk of Lua code") ``` ### Executing Lua files - ```gleam -let assert Ok(#(_state, [n, m])) = glua.eval_file( - state: glua.new(), - path: "./my_lua_files/two_numbers.lua" - using: decode.int -) +let assert Ok(#(_state, [n, m])) = + glua.eval_file(state: glua.new(), path: "./test/lua/two_numbers.lua") -assert n == 1 && m == 2 +assert n == glua.int(1) && m == glua.int(2) ``` ### Sandboxing - ```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", - using: decode.int -) +let assert Error(glua.LuaRuntimeException(exception, _)) = + glua.eval(state: lua, code: "os.execute('rm -f important_file')") // 'important_file' was not deleted assert exception == glua.ErrorCall(["os.execute is sandboxed"]) @@ -71,139 +79,82 @@ assert exception == glua.ErrorCall(["os.execute is sandboxed"]) ### Getting values from Lua ```gleam -let assert Ok(version) = glua.get( - state: glua.new(), - keys: ["_VERSION"], - using: decode.string -) +let assert Ok(version) = glua.get(state: glua.new(), keys: ["_VERSION"]) -assert version == "Lua 5.3" +assert version == glua.string("Lua 5.3") ``` ### Setting values in Lua ```gleam // we need to encode any value we want to pass to Lua -let #(lua, encoded) = glua.string(glua.new(), "my_value") - +let lua = glua.new() // `keys` is the full path to where the value will be set // and any intermediate table will be created if it is not present let keys = ["my_table", "my_value"] -let assert Ok(lua) = glua.set(state: lua, keys:, value: encoded) +let assert Ok(lua) = glua.set(state: lua, keys:, value: glua.string("my_value")) // now we can get the value -let assert Ok(value) = glua.get(state: lua, keys:, using: decode.string) +let assert Ok(value) = glua.get(state: lua, keys:) // or return it from a Lua script let assert Ok(#(_lua, [returned])) = - glua.eval( - state: lua, - code: "return my_table.my_value", - using: decode.string, - ) - -assert value == "my_value" -assert returned == "my_value" -``` + glua.eval(state: lua, code: "return my_table.my_value") -```gleam -// we can also encode a list of tuples as a table to set it in Lua -let my_table = [ - #("my_first_value", 1.2), - #("my_second_value", 2.1) -] - -// the function we use to encode the keys and the function we use to encode the values -let encoders = #(glua.string, glua.float) - -let #(lua, encoded) = glua.new() |> glua.table(encoders, my_table) -let assert Ok(lua) = glua.set(state: lua, keys: ["my_table"], value: encoded) - -// now we can get its values -let assert Ok(#(lua, [result])) = glua.eval( - state: lua, - code: "return my_table.my_second_value", - using: decode.float -) - -assert result == 2.1 - -// or we can get the whole table and decode it back to a list of tuples -assert glua.get( - state: lua, - keys: ["my_table"], - using: glua.table_decoder(decode.string, decode.float) -) == Ok([ - #("my_first_value", 1.2), - #("my_second_value", 2.1) -]) +assert value == glua.string("my_value") +assert returned == glua.string("my_value") ``` ### Calling Lua functions from Gleam ```gleam -// here we use `ref_get` instead of `get` because we need a reference to the function -// and not a decoded value let lua = glua.new() -let assert Ok(fun) = glua.ref_get( - state: lua, - keys: ["math", "max"] -) +let assert Ok(val) = glua.get(state: lua, keys: ["math", "max"]) +let assert Ok(#(lua, fun)) = deser.run(lua, val, deser.function) +let args = [1, 20, 7, 18] |> list.map(glua.int) -// we need to encode each argument we pass to a Lua function -// `glua.list` encodes a list of values using a single encoder function -let #(lua, args) = glua.list(lua, glua.int, [1, 20, 7, 18]) +let assert Ok(#(lua, [result])) = + glua.call_function(state: lua, fun: fun, args:) -let assert Ok(#(lua, [result])) = glua.call_function( - state: lua, - ref: fun, - args:, - using: decode.int -) +let assert Ok(#(lua, result)) = deser.run(lua, result, deser.number) -assert result == 20 +assert result == 20.0 // `glua.call_function_by_name` is a shorthand for `glua.ref_get` followed by `glua.call_function` -let assert Ok(#(_lua, [result])) = glua.call_function_by_name( - state: lua, - keys: ["math", "max"], - args:, - using: decode.int -) - -assert result == 20 +let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) + +let assert Ok(#(_lua, result)) = deser.run(lua, result, deser.number) + +assert result == 20.0 ``` ### Exposing Gleam functions to Lua ```gleam -let #(lua, fun) = { - use lua, args <- glua.function(glua.new()) - - let assert [x, min, max] = args - let assert Ok([x, min, max]) = list.try_map( - [x, min, max], - decode.run(_, decode.float) - ) +let lua = glua.new() +let #(lua, fun) = + glua.function(lua, fn(lua, args) { + // Since Gleam is a statically typed language, each and every argument must be decoded + let assert [x, min, max] = args + let assert Ok(#(lua, x)) = deser.run(lua, x, deser.number) + let assert Ok(#(lua, min)) = deser.run(lua, min, deser.number) + let assert Ok(#(lua, max)) = deser.run(lua, max, deser.number) - let result = float.clamp(x, min, max) + let result = float.clamp(x, min, max) - glua.list(lua, glua.float, [result]) -} + #(lua, [glua.float(result)]) + }) let keys = ["my_functions", "clamp"] let assert Ok(lua) = glua.set(state: lua, keys:, value: fun) -let #(lua, args) = glua.list(lua, glua.float, [2.3, 1.2, 2.1]) -let assert Ok(#(_lua, [result])) = glua.call_function_by_name( - state: lua, - keys:, - args:, - using: decode.float -) +let args = [2.3, 1.2, 2.1] |> list.map(glua.float) +let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys:, args:) -assert result == 2.1 +assert result == glua.float(2.1) ``` Further documentation can be found at . diff --git a/src/glua.gleam b/src/glua.gleam index 5a30187..e1c9dc8 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -63,9 +63,8 @@ pub fn string(v: String) -> ValueRef @external(erlang, "glua_ffi", "coerce") pub fn bool(v: Bool) -> ValueRef -pub fn int(v: Int) -> ValueRef { - float(int.to_float(v)) -} +@external(erlang, "glua_ffi", "coerce") +pub fn int(v: Int) -> ValueRef @external(erlang, "glua_ffi", "coerce") pub fn float(v: Float) -> ValueRef @@ -95,10 +94,6 @@ pub fn table_decoder( decode.list(of: inner) } -pub fn list(values: List(a), encoder: fn(a) -> ValueRef) -> List(ValueRef) { - list.map(values, encoder) -} - @external(erlang, "glua_ffi", "wrap_fun") pub fn function( lua: Lua, @@ -160,11 +155,12 @@ fn list_substraction(a: List(a), b: List(a)) -> List(a) /// ``` pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) { let msg = string.join(keys, with: ".") <> " is sandboxed" - set(lua, ["_G", ..keys], sandbox_fun(msg)) + let #(fun, lua) = sandbox_fun(lua, msg) + set(lua, ["_G", ..keys], fun) } @external(erlang, "glua_ffi", "sandbox_fun") -fn sandbox_fun(msg: String) -> ValueRef +fn sandbox_fun(state: Lua, msg: String) -> #(ValueRef, Lua) /// Gets a private value that is not exposed to the Lua runtime. /// @@ -419,7 +415,7 @@ fn do_call_function( pub fn call_function( state lua: Lua, - ref fun: Function, + fun fun: Function, args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { do_call_function(lua, fun, args) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index c196b3d..ea08a8f 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -3,7 +3,7 @@ -import(luerl_lib, [lua_error/2]). -include_lib("luerl/include/luerl.hrl"). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, +-export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/2, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, get_table_transform/4, get_table_list_transform/4]). @@ -200,9 +200,11 @@ wrap_fun(State, Fun) -> {T, St} = luerl:encode(NewFun, State), {St, T}. -sandbox_fun(Msg) -> - fun(_, State) -> {error, map_error(lua_error({error_call, [Msg]}, State))} end. - +sandbox_fun(St, Msg) -> + Fun = fun(_, State) -> + {error, map_error(lua_error({error_call, [Msg]}, State))} + end, + luerl:encode(Fun, St). get_table_key(Lua, Table, Key) -> case luerl:get_table_key(Table, Key, Lua) of diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index d19adc5..3b69f95 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -306,7 +306,7 @@ pub fn then_test() { } let lua = glua.new() let assert Ok(#(lua, 4.0)) = deser.run(lua, glua.int(4), positive_deser) - let assert Error([DeserializeError("PositiveNum", "Float", [])]) = + let assert Error([DeserializeError("PositiveNum", "Int", [])]) = deser.run(lua, glua.int(-4), positive_deser) } diff --git a/test/lua/two_numbers.lua b/test/lua/two_numbers.lua new file mode 100644 index 0000000..cd12910 --- /dev/null +++ b/test/lua/two_numbers.lua @@ -0,0 +1 @@ +return 1, 2 diff --git a/test/readme_test.gleam b/test/readme_test.gleam new file mode 100644 index 0000000..a4f613b --- /dev/null +++ b/test/readme_test.gleam @@ -0,0 +1,162 @@ +import deser +import gleam/dict +import gleam/float +import gleam/list +import glua + +/// This file contains all the tests in the readme +pub fn eval_test() { + let code = + " +function greet() + return 'Hello from Lua!' +end + +return greet() +" + + // Make a fresh instance lua instance + let lua = glua.new() + let assert Ok(#(_state, [result])) = glua.eval(state: lua, code:) + + assert result == glua.string("Hello from Lua!") +} + +pub type Project { + Project(name: String, language: String) +} + +pub fn deser_test() { + let code = "return { name = 'glua', written_in = 'gleam'}" + let assert Ok(#(lua, [table])) = glua.eval(glua.new(), code) + + let deserializer = { + use name <- deser.field(glua.string("name"), deser.string) + use language <- deser.field(glua.string("written_in"), deser.string) + deser.success(Project(name:, language:)) + } + + let assert Ok(#(_lua, project)) = deser.run(lua, table, deserializer) + assert project == Project(name: "glua", language: "gleam") +} + +pub fn parse_execute_chunk() { + let code = "return 'this is a chunk of Lua code'" + let assert Ok(#(state, chunk)) = glua.load(state: glua.new(), code:) + let assert Ok(#(_state, [result])) = glua.eval_chunk(state:, chunk:) + + assert result == glua.string("this is a chunk of Lua code") +} + +pub fn exec_lua_files_test() { + let assert Ok(#(_state, [n, m])) = + glua.eval_file(state: glua.new(), path: "./test/lua/two_numbers.lua") + + assert n == glua.int(1) && m == glua.int(2) +} + +pub fn sandboxing_test() { + 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')") + + // 'important_file' was not deleted + assert exception == glua.ErrorCall(["os.execute is sandboxed"]) +} + +pub fn get_global_values() { + let assert Ok(version) = glua.get(state: glua.new(), keys: ["_VERSION"]) + + assert version == glua.string("Lua 5.3") +} + +pub fn set_global_values() { + let lua = glua.new() + // `keys` is the full path to where the value will be set + // and any intermediate table will be created if it is not present + let keys = ["my_table", "my_value"] + let assert Ok(lua) = + glua.set(state: lua, keys:, value: glua.string("my_value")) + + // now we can get the value + let assert Ok(value) = glua.get(state: lua, keys:) + + // or return it from a Lua script + let assert Ok(#(_lua, [returned])) = + glua.eval(state: lua, code: "return my_table.my_value") + + assert value == glua.string("my_value") + assert returned == glua.string("my_value") +} + +pub fn table_deocding_test() { + // we can also encode a list of tuples as a table to set it in Lua + let my_table = [ + #(glua.string("my_first_value"), glua.float(1.2)), + #(glua.string("my_second_value"), glua.float(2.1)), + ] + + let #(lua, encoded) = glua.table(glua.new(), my_table) + let assert Ok(lua) = glua.set(state: lua, keys: ["my_table"], value: encoded) + + // now we can get its values + let assert Ok(#(lua, [result])) = + glua.eval(state: lua, code: "return my_table.my_second_value") + + let assert Ok(#(lua, 2.1)) = deser.run(lua, result, deser.number) + // or we can get the whole table and decode it back to a list of tuples + let assert Ok(table) = glua.get(state: lua, keys: ["my_table"]) + let assert Ok(#(_lua, table)) = + deser.run(lua, table, deser.dict(deser.string, deser.number)) + + assert table + == dict.from_list([#("my_first_value", 1.2), #("my_second_value", 2.1)]) +} + +pub fn call_functions_test() { + let lua = glua.new() + let assert Ok(val) = glua.get(state: lua, keys: ["math", "max"]) + let assert Ok(#(lua, fun)) = deser.run(lua, val, deser.function) + let args = [1, 20, 7, 18] |> list.map(glua.int) + + let assert Ok(#(lua, [result])) = + glua.call_function(state: lua, fun: fun, args:) + + let assert Ok(#(lua, result)) = deser.run(lua, result, deser.number) + + assert result == 20.0 + + // `glua.call_function_by_name` is a shorthand for `glua.ref_get` followed by `glua.call_function` + let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) + + let assert Ok(#(_lua, result)) = deser.run(lua, result, deser.number) + + assert result == 20.0 +} + +pub fn expose_functions_test() { + let lua = glua.new() + let #(lua, fun) = + glua.function(lua, fn(lua, args) { + // Since Gleam is a statically typed language, each and every argument must be decoded + let assert [x, min, max] = args + let assert Ok(#(lua, x)) = deser.run(lua, x, deser.number) + let assert Ok(#(lua, min)) = deser.run(lua, min, deser.number) + let assert Ok(#(lua, max)) = deser.run(lua, max, deser.number) + + let result = float.clamp(x, min, max) + + #(lua, [glua.float(result)]) + }) + + let keys = ["my_functions", "clamp"] + + let assert Ok(lua) = glua.set(state: lua, keys:, value: fun) + + let args = [2.3, 1.2, 2.1] |> list.map(glua.float) + let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys:, args:) + + assert result == glua.float(2.1) +} From 600dafd61a4e68369272b11e6c0c6f94b21028d8 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:47:57 -1000 Subject: [PATCH 27/41] ref_get does NOT exist --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da16661..47f6faf 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ let assert Ok(#(lua, result)) = deser.run(lua, result, deser.number) assert result == 20.0 -// `glua.call_function_by_name` is a shorthand for `glua.ref_get` followed by `glua.call_function` +// `glua.call_function_by_name` is a shorthand for `glua.get` followed by `glua.call_function` let assert Ok(#(_lua, [result])) = glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) From 62a402d2ec644d5c70b1e80fe5ee91f3a1e02460 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:59:29 -1000 Subject: [PATCH 28/41] Add other decode functions without tests Functions map_errors, collapse_errors, then, and run_decoders have been added --- src/deser.gleam | 67 +++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 80e563e..3242d38 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -433,25 +433,23 @@ pub fn map_errors( decoder: Deserializer(a), transformer: fn(List(DeserializeError)) -> List(DeserializeError), ) -> Deserializer(a) { - todo - // Deserializer(function: fn(d) { - // let #(data, errors) = decoder.function(d) - // #(data, transformer(errors)) - // }) + Deserializer(function: fn(lua, d) { + let #(data, lua, errors) = decoder.function(lua, d) + #(data, lua, transformer(errors)) + }) } pub fn collapse_errors( decoder: Deserializer(a), name: String, ) -> Deserializer(a) { - todo - // Deserializer(function: fn(dynamic_data) { - // let #(data, errors) as layer = decoder.function(dynamic_data) - // case errors { - // [] -> layer - // [_, ..] -> #(data, decode_error(name, dynamic_data)) - // } - // }) + Deserializer(function: fn(lua, dynamic_data) { + let #(data, lua, errors) as layer = decoder.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, lua, deser_error(name, dynamic_data)) + } + }) } pub fn then( @@ -473,33 +471,32 @@ pub fn one_of( first: Deserializer(a), or alternatives: List(Deserializer(a)), ) -> Deserializer(a) { - todo - // Deserializer(function: fn(dynamic_data) { - // let #(_, errors) as layer = first.function(dynamic_data) - // case errors { - // [] -> layer - // [_, ..] -> run_decoders(dynamic_data, layer, alternatives) - // } - // }) + Deserializer(function: fn(lua, dynamic_data) { + let #(_, lua, errors) as layer = first.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> run_decoders(dynamic_data, lua, layer, alternatives) + } + }) } fn run_decoders( data: ValueRef, - failure: #(a, List(DeserializeError)), + lua: Lua, + failure: Return(a), decoders: List(Deserializer(a)), -) -> #(a, List(DeserializeError)) { - todo - // case decoders { - // [] -> failure - // - // [decoder, ..decoders] -> { - // let #(_, errors) as layer = decoder.function(data) - // case errors { - // [] -> layer - // [_, ..] -> run_decoders(data, failure, decoders) - // } - // } - // } +) -> Return(a) { + case decoders { + [] -> failure + + [decoder, ..decoders] -> { + let #(_, lua, 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) { From 9cc2eefb35f791d46c8ea06747edd56bf46847e2 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:01:52 -1000 Subject: [PATCH 29/41] Clean up warnings --- src/deser.gleam | 33 +++------------------------------ src/glua.gleam | 1 - test/deserialize_test.gleam | 2 +- test/glua_test.gleam | 1 - 4 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 3242d38..3890a50 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -5,13 +5,9 @@ import gleam/dict.{type Dict} import gleam/dynamic -import gleam/dynamic/decode import gleam/int import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/pair -import gleam/result -import gleam/string +import gleam/option.{type Option} import glua.{type Lua, type ValueRef} pub opaque type Deserializer(t) { @@ -85,7 +81,7 @@ fn index_into( )) -> { handle_miss(lua, data, [key, ..position]) } - Error(err) -> { + Error(_err) -> { let #(default, lua, _) = inner(lua, data) #(default, lua, [DeserializeError("Table", classify(data), [])]) |> push_path(list.reverse(position)) @@ -124,30 +120,6 @@ fn get_table_key( key: ValueRef, ) -> Result(#(Lua, ValueRef), glua.LuaError) -fn to_string(lua: Lua, val: ValueRef) { - case classify(val) { - "Null" -> "" - "Int" -> decode(val, lua) - "Float" -> decode(val, lua) - "String" -> decode(val, lua) - "Unknown" -> "" - "UserDef" -> "userdefined: " <> string.inspect(val) - _ -> { - { - case glua.call_function_by_name(lua, ["tostring"], [val]) { - Ok(#(lua, [value])) -> { - run(lua, value, string) - |> result.map(pair.second) - |> result.unwrap("") - } - Error(err) -> " string.inspect(err) <> ")>" - _ -> "" - } - } - } - } -} - fn push_path(layer: Return(t), path: List(ValueRef)) -> Return(t) { let errors = list.map(layer.2, fn(error) { @@ -156,6 +128,7 @@ fn push_path(layer: Return(t), path: List(ValueRef)) -> Return(t) { #(layer.0, layer.1, errors) } +// TOOD: Make it take lua pub fn success(data: t) -> Deserializer(t) { Deserializer(function: fn(lua, _) { #(data, lua, []) }) } diff --git a/src/glua.gleam b/src/glua.gleam index e1c9dc8..82e575e 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -4,7 +4,6 @@ import gleam/dynamic import gleam/dynamic/decode -import gleam/int import gleam/list import gleam/pair import gleam/result diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 3b69f95..aae5a1b 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -298,7 +298,7 @@ pub fn table_list_err_test() { pub fn then_test() { let positive_deser = { - use lua, num <- deser.then(deser.number) + use _lua, num <- deser.then(deser.number) case num >. 0.0 { False -> deser.failure(0.0, "PositiveNum") True -> deser.success(num) diff --git a/test/glua_test.gleam b/test/glua_test.gleam index eb52093..f0579d6 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -5,7 +5,6 @@ // import gleam/list // import gleam/option // import gleam/pair -import deserialize_test import gleeunit // import glua From 71988a1f3a55e758c653a041b3b1759d0eca60a7 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:03:26 -1000 Subject: [PATCH 30/41] Make success take lua No idea why it was like that to begin with since not taking `lua` defeats the purpose of decode.run returning `lua` --- README.md | 2 +- src/deser.gleam | 4 ++-- test/deserialize_test.gleam | 18 +++++++++--------- test/readme_test.gleam | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 47f6faf..7f281a6 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ let assert Ok(#(lua, [table])) = glua.eval(glua.new(), code) let deserializer = { use name <- deser.field(glua.string("name"), deser.string) use language <- deser.field(glua.string("written_in"), deser.string) - deser.success(Project(name:, language:)) + deser.success(lua, Project(name:, language:)) } let assert Ok(#(_lua, project)) = deser.run(lua, table, deserializer) diff --git a/src/deser.gleam b/src/deser.gleam index 3890a50..e07f581 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -129,8 +129,8 @@ fn push_path(layer: Return(t), path: List(ValueRef)) -> Return(t) { } // TOOD: Make it take lua -pub fn success(data: t) -> Deserializer(t) { - Deserializer(function: fn(lua, _) { #(data, lua, []) }) +pub fn success(lua: Lua, data: t) -> Deserializer(t) { + Deserializer(function: fn(_, _) { #(data, lua, []) }) } pub fn deser_error( diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index aae5a1b..0b361f8 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -32,7 +32,7 @@ pub fn field_ok_test() { let assert Ok(#(_lua, val)) = deser.run(lua, data, { use str <- deser.field(glua.string("name"), deser.string) - deser.success(str) + deser.success(lua, str) }) assert val == "Hina" } @@ -44,7 +44,7 @@ pub fn field_err_test() { let assert Error([DeserializeError("Field", "Nothing", path)]) = deser.run(lua, data, { use str <- deser.field(glua.string("name"), deser.string) - deser.success(str) + deser.success(lua, str) }) assert path == [glua.string("name")] } @@ -67,7 +67,7 @@ pub fn subfield_ok_test() { [glua.string("friends"), glua.int(1)], deser.string, ) - deser.success(first) + deser.success(lua, first) }) assert val == "Puffy" } @@ -90,7 +90,7 @@ pub fn subfield_err_test() { [glua.string("friends"), glua.int(1)], deser.string, ) - deser.success(first) + deser.success(lua, first) }) assert path == [glua.string("friends"), glua.int(1)] } @@ -112,7 +112,7 @@ pub fn field_metatable_test() { glua.string("aasdlkjghasddlkjghasddklgjh;ksjdh"), deser.string, ) - deser.success(pong) + deser.success(lua, pong) }) assert val == "pong" } @@ -194,7 +194,7 @@ pub fn optional_field_ok_test() { "doh i missed", deser.string, ) - deser.success(hit) + deser.success(lua, hit) }) assert hit == "hit" let assert Ok(#(_lua, miss)) = @@ -204,7 +204,7 @@ pub fn optional_field_ok_test() { "doh i missed", deser.string, ) - deser.success(hit) + deser.success(lua, hit) }) assert miss == "doh i missed" @@ -298,10 +298,10 @@ pub fn table_list_err_test() { pub fn then_test() { let positive_deser = { - use _lua, num <- deser.then(deser.number) + use lua, num <- deser.then(deser.number) case num >. 0.0 { False -> deser.failure(0.0, "PositiveNum") - True -> deser.success(num) + True -> deser.success(lua, num) } } let lua = glua.new() diff --git a/test/readme_test.gleam b/test/readme_test.gleam index a4f613b..45bf65f 100644 --- a/test/readme_test.gleam +++ b/test/readme_test.gleam @@ -33,7 +33,7 @@ pub fn deser_test() { let deserializer = { use name <- deser.field(glua.string("name"), deser.string) use language <- deser.field(glua.string("written_in"), deser.string) - deser.success(Project(name:, language:)) + deser.success(lua, Project(name:, language:)) } let assert Ok(#(_lua, project)) = deser.run(lua, table, deserializer) From 8f09682d1731e52cab134526a4e1fdcc92fb71af Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 03:18:46 -1000 Subject: [PATCH 31/41] Add back glua_test --- src/deser.gleam | 26 +- src/glua_ffi.erl | 13 +- test/deserialize_test.gleam | 4 +- test/glua_test.gleam | 1010 +++++++++++++++-------------------- 4 files changed, 470 insertions(+), 583 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index e07f581..1eb5107 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -5,6 +5,7 @@ import gleam/dict.{type Dict} import gleam/dynamic +import gleam/float import gleam/int import gleam/list import gleam/option.{type Option} @@ -234,10 +235,27 @@ fn deser_num(lua, data: ValueRef) -> Return(Float) { } } -pub const index: Deserializer(Int) = Deserializer(deser_index) +pub const int: Deserializer(Int) = Deserializer(deser_int) -fn deser_index(lua, data: ValueRef) -> Return(Int) { - run_dynamic_function(lua, data, "Int", 0) +fn deser_int(lua, data: ValueRef) -> Return(Int) { + let got = classify(data) + let error = #(0, lua, [ + 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, lua, []) + False -> error + } + } + "Int" -> { + #(decode(data, lua), lua, []) + } + _ -> error + } } pub const raw: Deserializer(ValueRef) = Deserializer(decode_raw) @@ -246,7 +264,7 @@ fn decode_raw(lua, data: ValueRef) -> Return(ValueRef) { #(data, lua, []) } -pub const user_defined: Deserializer(dynamic.Dynamic) = Deserializer( +pub const userdata: Deserializer(dynamic.Dynamic) = Deserializer( deser_user_defined, ) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index ea08a8f..1f21862 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -8,23 +8,14 @@ eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, get_table_transform/4, get_table_list_transform/4]). -%% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam -maybe_process_userdata(Lst) when is_list(Lst) -> - lists:map(fun maybe_process_userdata/1, Lst); -maybe_process_userdata({userdata, Data}) -> - Data; -maybe_process_userdata(Other) -> - Other. - %% helper to convert luerl return values to a format %% that is more suitable for use in Gleam code to_gleam(Value) -> case Value of {ok, Result, LuaState} -> - Values = maybe_process_userdata(Result), - {ok, {LuaState, Values}}; + {ok, {LuaState, Result}}; {ok, _} = Result -> - maybe_process_userdata(Result); + Result; {lua_error, _, _} = Error -> {error, map_error(Error)}; {error, _, _} = Error -> diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 0b361f8..3ae414a 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -18,7 +18,7 @@ pub fn deserializer_test() { let data = ["this", "is", "some", "random", "data"] let #(lua, ref) = data |> glua.userdata(lua, _) - let assert Ok(#(_lua, userdefined)) = deser.run(lua, ref, deser.user_defined) + let assert Ok(#(_lua, userdefined)) = deser.run(lua, ref, deser.userdata) assert userdefined == coerce_dynamic(data) } @@ -238,7 +238,7 @@ pub fn table_list_decode_test() { |> list.map(fn(pair) { #(glua.int(pair.0), glua.string(pair.1)) }), ) let assert Ok(#(_lua, dict)) = - deser.run(lua, data, deser.dict(deser.index, deser.string)) + deser.run(lua, data, deser.dict(deser.int, deser.string)) assert dict == dict.from_list(meanings) } diff --git a/test/glua_test.gleam b/test/glua_test.gleam index f0579d6..6d77709 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,572 +1,450 @@ -// import gleam/dict -// import gleam/dynamic -// import gleam/dynamic/decode -// import gleam/int -// import gleam/list -// import gleam/option -// import gleam/pair +import deser +import gleam/dict +import gleam/dynamic/decode +import gleam/int +import gleam/list +import gleam/option import gleeunit -// import glua +import glua pub fn main() -> Nil { gleeunit.main() } -// 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 table = -// glua.table( -// my_table -// |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), -// ) -// #(lua, [table]) -// }) -// -// let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) -// let assert Ok(#(_lua, [table])) = -// glua.call_function_by_name( -// lua, -// ["cool_numbers"], -// [], -// using: glua.table_decoder(decode.string, decode.int), -// ) -// -// assert dict.from_list(table) == dict.from_list(my_table) -// } -// -// pub fn sandbox_test() { -// let assert Ok(lua) = glua.sandbox(glua.new(), ["math", "max"]) -// 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:, -// using: decode.int, -// ) -// -// assert exception == glua.ErrorCall(["math.max is sandboxed"]) -// -// let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) -// -// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = -// glua.eval( -// state: lua, -// code: "return string.upper('my_string')", -// using: decode.string, -// ) -// -// assert name == "upper" -// -// let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) -// -// let assert Error(glua.LuaRuntimeException(exception, _)) = -// glua.ref_eval( -// state: lua, -// code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", -// ) -// -// assert exception == glua.ErrorCall(["os.execute is sandboxed"]) -// -// 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], -// using: decode.string, -// ) -// -// assert exception == glua.ErrorCall(["print is sandboxed"]) -// } -// -// pub fn new_sandboxed_test() { -// let assert Ok(lua) = glua.new_sandboxed([]) -// -// let assert Error(glua.LuaRuntimeException(exception, _)) = -// glua.ref_eval(state: lua, code: "return load(\"return 1\")") -// -// assert exception == glua.ErrorCall(["load is sandboxed"]) -// -// let arg = glua.int(1) -// let assert Error(glua.LuaRuntimeException(exception, _)) = -// glua.ref_call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) -// -// assert exception == glua.ErrorCall(["os.exit is sandboxed"]) -// -// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = -// glua.ref_eval(state: lua, code: "io.write('some_message')") -// -// assert name == "write" -// -// let assert Ok(lua) = glua.new_sandboxed([["package"], ["require"]]) -// let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) -// -// let code = "local s = require 'example'; return s" -// let assert Ok(#(_, [result])) = -// glua.eval(state: lua, code:, using: decode.string) -// -// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -// } -// -// pub fn encoding_and_decoding_nested_tables_test() { -// let nested_table = [ -// #( -// glua.string("key"), -// glua.table([ -// #( -// glua.int(1), -// glua.table([#(glua.string("deeper_key"), glua.string("deeper_value"))]), -// ), -// ]), -// ), -// ] -// -// let keys = ["my_nested_table"] -// -// let nested_table_decoder = -// glua.table_decoder( -// decode.string, -// glua.table_decoder( -// decode.int, -// glua.table_decoder(decode.string, decode.string), -// ), -// ) -// let tbl = glua.table(nested_table) -// -// let assert Ok(lua) = glua.set(state: glua.new(), keys:, value: tbl) -// -// let assert Ok(result) = -// glua.get(state: lua, keys:, using: nested_table_decoder) -// -// assert result == [#("key", [#(1, [#("deeper_key", "deeper_value")])])] -// } -// -// pub type Userdata { -// Userdata(foo: String, bar: Int) -// } -// -// pub fn userdata_test() { -// let lua = glua.new() -// let 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:)) -// } -// -// let assert Ok(lua) = glua.set(lua, ["my_userdata"], glua.userdata(userdata)) -// let assert Ok(#(lua, [result])) = -// glua.eval(lua, "return my_userdata", userdata_decoder) -// -// assert result == userdata -// -// let userdata = Userdata("other_userdata", 2) -// let assert Ok(lua) = -// glua.set(lua, ["my_other_userdata"], glua.userdata(userdata)) -// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(value, index), _)) = -// glua.eval(lua, "return my_other_userdata.foo", decode.string) -// -// assert value == "{usdref,1}" -// assert index == "foo" -// } -// -// pub fn get_test() { -// let state = glua.new() -// -// let assert Ok(pi) = -// glua.get(state: state, keys: ["math", "pi"], using: decode.float) -// -// 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(ret) = glua.get(state:, keys:, using: decode.bool) -// -// assert ret == True -// -// let code = -// " -// my_value = 10 -// return 'ignored' -// " -// let assert Ok(#(state, _)) = -// glua.new() |> glua.eval(code:, using: decode.string) -// let assert Ok(ret) = glua.get(state:, keys: ["my_value"], using: decode.int) -// -// assert ret == 10 -// } -// -// pub fn get_returns_proper_errors_test() { -// let state = glua.new() -// -// assert glua.get(state:, keys: ["non_existent_global"], using: decode.string) -// == Error(glua.KeyNotFound) -// -// let encoded = glua.int(10) -// let assert Ok(state) = -// glua.set(state:, keys: ["my_table", "some_value"], value: encoded) -// -// assert glua.get(state:, keys: ["my_table", "my_val"], using: decode.int) -// == Error(glua.KeyNotFound) -// } -// -// pub fn set_test() { -// let encoded = glua.string("custom version") -// -// let assert Ok(lua) = -// glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) -// let assert Ok(result) = -// glua.get(state: lua, keys: ["_VERSION"], using: decode.string) -// -// assert result == "custom version" -// -// let numbers = -// [2, 4, 7, 12] -// |> list.index_map(fn(n, i) { #(i + 1, n * n) }) -// -// let keys = ["math", "squares"] -// -// let encoded = -// glua.table( -// numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), -// ) -// let assert Ok(lua) = glua.set(lua, keys, encoded) -// -// assert glua.get(lua, keys, glua.table_decoder(decode.int, decode.int)) -// == Ok([#(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, glua.table_decoder(decode.int, decode.int)) -// -// let count = -// list.map(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 arg = -// glua.table( -// list.index_map(list.range(1, 10), fn(i, n) { -// #(glua.int(i + 1), glua.int(n)) -// }), -// ) -// -// let assert Ok(#(lua, [result])) = -// glua.call_function_by_name( -// state: lua, -// keys: ["count_odd"], -// args: [arg], -// using: decode.int, -// ) -// -// assert result == 5 -// -// let tbl = -// glua.table([ -// #( -// 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)) -// }), -// ), -// #( -// 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) -// -// let assert Ok(lua) = glua.set(state: lua, keys: ["my_functions"], value: tbl) -// -// let assert Ok(#(lua, [result])) = -// glua.call_function_by_name( -// state: lua, -// keys: ["my_functions", "is_even"], -// args: [arg], -// using: decode.bool, -// ) -// -// assert result == True -// -// let assert Ok(#(_, [result])) = -// glua.eval( -// state: lua, -// code: "return my_functions.is_odd(4)", -// using: decode.bool, -// ) -// -// assert result == False -// } -// -// pub fn set_lua_paths_test() { -// let assert Ok(state) = -// glua.set_lua_paths(state: glua.new(), paths: ["./test/lua/?.lua"]) -// -// let code = "local s = require 'example'; return s" -// -// let assert Ok(#(_, [result])) = glua.eval(state:, code:, using: decode.string) -// -// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -// } -// -// pub fn get_private_test() { -// assert glua.new() -// |> glua.set_private("test", [1, 2, 3]) -// |> glua.get_private("test", using: decode.list(decode.int)) -// == Ok([1, 2, 3]) -// -// assert glua.new() -// |> glua.get_private("non_existent", using: decode.string) -// == Error(glua.KeyNotFound) -// } -// -// pub fn delete_private_test() { -// let lua = glua.set_private(glua.new(), "the_value", "that_will_be_deleted") -// -// assert glua.get_private(lua, "the_value", using: decode.string) -// == Ok("that_will_be_deleted") -// -// assert glua.delete_private(lua, "the_value") -// |> glua.get_private(key: "the_value", using: decode.string) -// == Error(glua.KeyNotFound) -// } -// -// pub fn load_test() { -// let assert Ok(#(lua, chunk)) = -// glua.load(state: glua.new(), code: "return 5 * 5") -// let assert Ok(#(_, [result])) = -// glua.eval_chunk(state: lua, chunk:, using: decode.int) -// -// assert result == 25 -// } -// -// 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(#(_, [result])) = -// glua.eval_chunk(state: lua, chunk:, using: decode.string) -// -// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -// } -// -// pub fn eval_test() { -// let assert Ok(#(lua, [result])) = -// glua.eval( -// state: glua.new(), -// code: "return 'hello, ' .. 'world!'", -// using: decode.string, -// ) -// -// assert result == "hello, world!" -// -// let assert Ok(#(_, results)) = -// glua.eval(state: lua, code: "return 2 + 2, 3 - 1", using: decode.int) -// -// assert results == [4, 2] -// } -// -// pub fn eval_returns_proper_errors_test() { -// let state = glua.new() -// -// assert glua.eval(state:, code: "if true then 1 + ", using: decode.int) -// == Error( -// glua.LuaCompilerException(messages: ["syntax error before: ", "1"]), -// ) -// -// assert glua.eval(state:, code: "return 'Hello from Lua!'", using: decode.int) -// == Error( -// glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), -// ) -// -// let assert Error(glua.LuaRuntimeException( -// exception: glua.IllegalIndex(value:, index:), -// state: _, -// )) = glua.eval(state:, code: "return a.b", using: decode.int) -// -// assert value == "nil" -// assert index == "b" -// -// let assert Error(glua.LuaRuntimeException( -// exception: glua.ErrorCall(messages:), -// state: _, -// )) = glua.eval(state:, code: "error('error message')", using: decode.int) -// -// assert messages == ["error message"] -// -// let assert Error(glua.LuaRuntimeException( -// exception: glua.UndefinedFunction(value:), -// state: _, -// )) = glua.eval(state:, code: "local a = 5; a()", using: decode.int) -// -// assert value == "5" -// let assert Error(glua.LuaRuntimeException( -// exception: glua.BadArith(operator:, args:), -// state: _, -// )) = glua.eval(state:, code: "return 10 / 0", using: decode.int) -// -// assert operator == "/" -// assert args == ["10", "0"] -// -// let assert Error(glua.LuaRuntimeException( -// exception: glua.AssertError(message:), -// state: _, -// )) = -// glua.eval( -// state:, -// code: "assert(1 == 2, 'assertion failed')", -// using: decode.int, -// ) -// -// assert message == "assertion failed" -// } -// -// pub fn eval_file_test() { -// let assert Ok(#(_, [result])) = -// glua.eval_file( -// state: glua.new(), -// path: "./test/lua/example.lua", -// using: decode.string, -// ) -// -// assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -// } -// -// pub fn call_function_test() { -// let assert Ok(#(lua, [fun])) = -// glua.ref_eval(state: glua.new(), code: "return string.reverse") -// -// let encoded = glua.string("auL") -// -// let assert Ok(#(lua, [result])) = -// glua.call_function( -// state: lua, -// ref: fun, -// args: [encoded], -// using: decode.string, -// ) -// -// assert result == "Lua" -// -// let assert Ok(#(lua, [fun])) = -// glua.ref_eval(state: lua, code: "return function(a, b) return a .. b end") -// -// let args = list.map(["Lua in ", "Gleam"], glua.string) -// -// let assert Ok(#(_, [result])) = -// glua.call_function(state: lua, ref: fun, args:, using: decode.string) -// -// assert result == "Lua in Gleam" -// } -// -// pub fn call_function_returns_proper_errors_test() { -// let state = glua.new() -// -// let assert Ok(#(state, [ref])) = -// glua.ref_eval(state:, code: "return string.upper") -// -// let arg = glua.string("Hello from Gleam!") -// -// assert glua.call_function(state:, ref:, args: [arg], using: decode.int) -// == Error( -// glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), -// ) -// -// let assert Ok(#(lua, [ref])) = glua.ref_eval(state:, code: "return 1") -// -// let assert Error(glua.LuaRuntimeException( -// exception: glua.UndefinedFunction(value:), -// state: _, -// )) = glua.call_function(state: lua, ref:, args: [], using: decode.string) -// -// assert value == "1" -// } -// -// pub fn call_function_by_name_test() { -// let args = list.map([20, 10], glua.int) -// let assert Ok(#(lua, [result])) = -// glua.call_function_by_name( -// state: glua.new(), -// keys: ["math", "max"], -// args:, -// using: decode.int, -// ) -// -// assert result == 20 -// -// let assert Ok(#(lua, [result])) = -// glua.call_function_by_name( -// state: lua, -// keys: ["math", "min"], -// args:, -// using: decode.int, -// ) -// -// assert result == 10 -// -// let arg = glua.float(10.2) -// let assert Ok(#(_, [result])) = -// glua.call_function_by_name( -// state: lua, -// keys: ["math", "type"], -// args: [arg], -// using: decode.optional(decode.string), -// ) -// -// assert result == option.Some("float") -// } -// -// pub fn nested_function_references_test() { -// let code = "return function() return math.sqrt end" -// -// let assert Ok(#(lua, [ref])) = glua.ref_eval(state: glua.new(), code:) -// let assert Ok(#(lua, [ref])) = -// glua.ref_call_function(state: lua, ref:, args: []) -// -// let arg = glua.int(400) -// let assert Ok(#(_, [result])) = -// glua.call_function(state: lua, ref:, args: [arg], using: decode.float) -// assert result == 20.0 -// } -// -// pub fn alloc_test() { -// let #(lua, table) = glua.alloc_table(glua.new(), []) -// let proxy = -// glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) -// let metatable = glua.table([#(glua.string("__index"), proxy)]) -// let assert Ok(#(lua, _)) = -// glua.ref_call_function_by_name(lua, ["setmetatable"], [table, metatable]) -// let assert Ok(lua) = glua.set(lua, ["test_table"], table) -// -// let assert Ok(#(_lua, [ret1])) = -// glua.eval(lua, "return test_table.any_key", decode.string) -// -// let assert Ok(#(_lua, [ret2])) = -// glua.eval(lua, "return test_table.other_key", decode.string) -// -// assert ret1 == "constant" -// assert ret2 == "constant" -// } + +pub fn get_table_test() { + let lua = glua.new() + let my_table = [ + #("meaning of life", 42.0), + #("pi", 3.0), + #("euler's number", 3.0), + ] + + let #(lua, cool_numbers) = + glua.function(lua, fn(lua, _params) { + let #(lua, table) = + glua.table( + lua, + my_table + |> list.map(fn(pair) { #(glua.string(pair.0), glua.float(pair.1)) }), + ) + #(lua, [table]) + }) + + let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) + let assert Ok(#(lua, [table])) = + glua.call_function_by_name(lua, ["cool_numbers"], []) + + let assert Ok(#(_lua, table)) = + deser.run(lua, table, deser.dict(deser.string, deser.number)) + + assert table == dict.from_list(my_table) +} + +pub fn sandbox_test() { + let assert Ok(lua) = glua.sandbox(glua.new(), ["math", "max"]) + 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:) + + assert exception == glua.ErrorCall(["math.max is sandboxed"]) + + let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) + + let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = + glua.eval(state: lua, code: "return string.upper('my_string')") + + assert name == "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)", + ) + + assert exception == glua.ErrorCall(["os.execute is sandboxed"]) + + 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]) + + assert exception == glua.ErrorCall(["print is sandboxed"]) +} + +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\")") + + assert exception == glua.ErrorCall(["load is sandboxed"]) + + let arg = glua.int(1) + let assert Error(glua.LuaRuntimeException(exception, _)) = + glua.call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) + + assert exception == glua.ErrorCall(["os.exit is sandboxed"]) + + let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = + glua.eval(state: lua, code: "io.write('some_message')") + + assert name == "write" + + let assert Ok(lua) = glua.new_sandboxed([["package"], ["require"]]) + let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) + + let code = "local s = require 'example'; return s" + let assert Ok(#(_, [result])) = glua.eval(state: lua, code:) + + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") +} + +pub type Userdata { + Userdata(foo: String, bar: Int) +} + +pub fn userdata_test() { + let lua = glua.new() + let 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:)) + } + + let #(lua, data) = glua.userdata(lua, userdata) + let assert Ok(lua) = glua.set(lua, ["my_userdata"], data) + let assert Ok(#(lua, [result])) = glua.eval(lua, "return my_userdata") + let assert Ok(#(lua, result)) = deser.run(lua, result, deser.userdata) + let assert Ok(result) = decode.run(result, userdata_decoder) + + assert result == userdata + + let userdata = Userdata("other_userdata", 2) + let #(lua, data) = glua.userdata(lua, userdata) + let assert Ok(lua) = glua.set(lua, ["my_other_userdata"], data) + let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(value, index), _)) = + glua.eval(lua, "return my_other_userdata.foo") + + assert value == "{usdref,1}" + assert index == "foo" +} + +pub fn get_test() { + let lua = glua.new() + + let assert Ok(pi) = glua.get(state: lua, keys: ["math", "pi"]) + let assert Ok(#(lua, pi)) = deser.run(lua, pi, deser.number) + + assert pi >. 3.14 && pi <. 3.15 + + let keys = ["my_table", "my_value"] + let encoded = glua.bool(True) + let assert Ok(lua) = glua.set(state: lua, keys:, value: encoded) + let assert Ok(ret) = glua.get(state: lua, keys:) + + assert ret == glua.bool(True) + + let code = + " + my_value = 10 + return 'ignored' +" + let assert Ok(#(lua, _)) = glua.new() |> glua.eval(code:) + let assert Ok(ret) = glua.get(state: lua, keys: ["my_value"]) + + assert ret == glua.int(10) +} + +pub fn get_returns_proper_errors_test() { + let state = glua.new() + + assert glua.get(state:, keys: ["non_existent_global"]) + == Error(glua.KeyNotFound) + + 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) +} + +pub fn set_test() { + let encoded = glua.string("custom version") + + let assert Ok(lua) = + glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) + let assert Ok(result) = glua.get(state: lua, keys: ["_VERSION"]) + let assert Ok(#(lua, result)) = deser.run(lua, result, deser.string) + + assert result == "custom version" + + let numbers = + [2, 4, 7, 12] + |> list.index_map(fn(n, i) { #(i + 1, n * n) }) + + let keys = ["math", "squares"] + + let #(lua, encoded) = + glua.table( + lua, + numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), + ) + let assert Ok(lua) = glua.set(lua, keys, encoded) + + let assert Ok(val) = glua.get(lua, keys) + let assert Ok(#(lua, val)) = deser.run(lua, val, deser.list(deser.int)) + assert val == [4, 16, 49, 144] + + let count_odd = fn(lua: glua.Lua, args: List(glua.ValueRef)) { + let assert [list] = args + let assert Ok(#(lua, list)) = deser.run(lua, list, deser.list(deser.int)) + + let count = list.count(list, int.is_odd) + #(lua, list.map([count], glua.int)) + } + + let #(lua, encoded) = glua.function(lua, count_odd) + let assert Ok(lua) = glua.set(lua, ["count_odd"], encoded) + + let #(lua, arg) = + glua.table( + lua, + list.index_map(list.range(1, 10), fn(i, n) { #(glua.int(i), glua.int(n)) }), + ) + + let assert Ok(#(lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["count_odd"], args: [arg]) + + assert result == glua.int(5) + + let #(lua, is_even) = + glua.function(lua, fn(lua, args) { + let assert [arg] = args + let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) + #(lua, list.map([int.is_even(arg)], glua.bool)) + }) + let #(lua, is_odd) = + glua.function(lua, fn(lua, args) { + let assert [arg] = args + let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) + #(lua, list.map([int.is_odd(arg)], glua.bool)) + }) + let #(lua, tbl) = + glua.table(lua, [ + #(glua.string("is_even"), is_even), + #(glua.string("is_odd"), is_odd), + ]) + + let arg = glua.int(4) + + let assert Ok(lua) = glua.set(state: lua, keys: ["my_functions"], value: tbl) + + let assert Ok(#(lua, [result])) = + glua.call_function_by_name( + state: lua, + keys: ["my_functions", "is_even"], + args: [arg], + ) + + assert result == glua.bool(True) + + let assert Ok(#(_, [result])) = + glua.eval(state: lua, code: "return my_functions.is_odd(4)") + + assert result == glua.bool(False) +} + +pub fn set_lua_paths_test() { + let assert Ok(state) = + glua.set_lua_paths(state: glua.new(), paths: ["./test/lua/?.lua"]) + + let code = "local s = require 'example'; return s" + + let assert Ok(#(_, [result])) = glua.eval(state:, code:) + + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") +} + +pub fn get_private_test() { + let lua = glua.new() |> glua.set_private("test", [1, 2, 3]) + let assert Ok(priv) = glua.get_private(lua, "test") + assert decode.run(priv, decode.list(decode.int)) == Ok([1, 2, 3]) + + assert glua.new() + |> glua.get_private("non_existent") + == Error(glua.KeyNotFound) +} + +pub fn delete_private_test() { + let lua = glua.set_private(glua.new(), "the_value", "that_will_be_deleted") + + let assert Ok(priv) = glua.get_private(lua, "the_value") + assert decode.run(priv, decode.string) == Ok("that_will_be_deleted") + + let lua = glua.delete_private(lua, "the_value") + let assert Error(glua.KeyNotFound) = + glua.get_private(state: lua, key: "the_value") +} + +pub fn load_test() { + let assert Ok(#(lua, chunk)) = + glua.load(state: glua.new(), code: "return 5 * 5") + let assert Ok(#(_, [result])) = glua.eval_chunk(state: lua, chunk:) + + assert result == glua.int(25) +} + +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(#(_, [result])) = glua.eval_chunk(state: lua, chunk:) + + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") +} + +pub fn eval_test() { + let assert Ok(#(lua, [result])) = + glua.eval(state: glua.new(), code: "return 'hello, ' .. 'world!'") + + assert result == glua.string("hello, world!") + + let assert Ok(#(_, [a, b])) = + glua.eval(state: lua, code: "return 2 + 2, 3 - 1") + + assert a == glua.int(4) + assert b == glua.int(2) +} + +pub fn eval_returns_proper_errors_test() { + let state = glua.new() + + assert glua.eval(state:, code: "if true then 1 + ") + == Error( + glua.LuaCompilerException(messages: ["syntax error before: ", "1"]), + ) + + let assert Error(glua.LuaRuntimeException( + exception: glua.IllegalIndex(value:, index:), + state: _, + )) = glua.eval(state:, code: "return a.b") + + assert value == "nil" + assert index == "b" + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(messages:), + state: _, + )) = glua.eval(state:, code: "error('error message')") + + assert messages == ["error message"] + + let assert Error(glua.LuaRuntimeException( + exception: glua.UndefinedFunction(value:), + state: _, + )) = glua.eval(state:, code: "local a = 5; a()") + + assert value == "5" + let assert Error(glua.LuaRuntimeException( + exception: glua.BadArith(operator:, args:), + state: _, + )) = glua.eval(state:, code: "return 10 / 0") + + assert operator == "/" + assert args == ["10", "0"] + + let assert Error(glua.LuaRuntimeException( + exception: glua.AssertError(message:), + state: _, + )) = glua.eval(state:, code: "assert(1 == 2, 'assertion failed')") + + assert message == "assertion failed" +} + +pub fn eval_file_test() { + let assert Ok(#(_, [result])) = + glua.eval_file(state: glua.new(), path: "./test/lua/example.lua") + + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") +} + +pub fn call_function_test() { + let assert Ok(#(lua, [fun])) = + glua.eval(state: glua.new(), code: "return string.reverse") + + let encoded = glua.string("auL") + let assert Ok(#(lua, fun)) = deser.run(lua, fun, deser.function) + + let assert Ok(#(lua, [result])) = + glua.call_function(state: lua, fun: fun, args: [encoded]) + + assert result == glua.string("Lua") + + let assert Ok(#(lua, [fun])) = + glua.eval(state: lua, code: "return function(a, b) return a .. b end") + let assert Ok(#(lua, fun)) = deser.run(lua, fun, deser.function) + + let args = list.map(["Lua in ", "Gleam"], glua.string) + + let assert Ok(#(_, [result])) = + glua.call_function(state: lua, fun: fun, args:) + + assert result == glua.string("Lua in Gleam") +} + +pub fn call_function_by_name_test() { + let args = list.map([20, 10], glua.int) + let assert Ok(#(lua, [result])) = + glua.call_function_by_name(state: glua.new(), keys: ["math", "max"], args:) + + assert result == glua.int(20) + + let assert Ok(#(lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "min"], args:) + + assert result == glua.int(10) + + let arg = glua.float(10.2) + let assert Ok(#(lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "type"], args: [arg]) + let assert Ok(#(_lua, result)) = + deser.run(lua, result, deser.optional(deser.string)) + + assert result == option.Some("float") +} + +pub fn nested_function_references_test() { + let code = "return function() return math.sqrt end" + + let assert Ok(#(lua, [ref])) = glua.eval(state: glua.new(), code:) + let assert Ok(#(lua, fun)) = deser.run(lua, ref, deser.function) + let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, fun:, args: []) + let assert Ok(#(lua, fun)) = deser.run(lua, ref, deser.function) + + let arg = glua.int(400) + let assert Ok(#(_, [result])) = + glua.call_function(state: lua, fun:, args: [arg]) + assert result == glua.float(20.0) +} + +pub fn alloc_test() { + let #(lua, table) = glua.table(glua.new(), []) + let #(lua, proxy) = + glua.function(lua, fn(lua, _args) { #(lua, [glua.string("constant")]) }) + let #(lua, metatable) = glua.table(lua, [#(glua.string("__index"), proxy)]) + let assert Ok(#(lua, _)) = + glua.call_function_by_name(lua, ["setmetatable"], [table, metatable]) + let assert Ok(lua) = glua.set(lua, ["test_table"], table) + + let assert Ok(#(_lua, [ret1])) = glua.eval(lua, "return test_table.any_key") + + let assert Ok(#(_lua, [ret2])) = glua.eval(lua, "return test_table.other_key") + + assert ret1 == glua.string("constant") + assert ret2 == glua.string("constant") +} From e834ef71cc86ec253e252f29faef127ea673b234 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:26:21 -1000 Subject: [PATCH 32/41] Fix edge case for nonexistent tables and userdata --- src/deser.gleam | 40 +++++++++++++++++++++++++++---------- src/glua.gleam | 2 +- src/glua_ffi.erl | 16 +++++++++++++-- test/deserialize_test.gleam | 13 ++++++++++++ 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 1eb5107..4dd93bf 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -3,6 +3,7 @@ //// 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/bool import gleam/dict.{type Dict} import gleam/dynamic import gleam/float @@ -272,17 +273,18 @@ pub const userdata: Deserializer(dynamic.Dynamic) = Deserializer( fn unwrap_userdata(a: userdata) -> Result(dynamic.Dynamic, Nil) fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { - let ret = run_dynamic_function(lua, data, "UserDef", dynamic.nil()) - let #(dyn, lua, errs) = ret - case errs { - [] -> - case unwrap_userdata(dyn) { - Ok(dyn) -> #(dyn, lua, []) - Error(Nil) -> #(dynamic.nil(), lua, [ - DeserializeError("UserDef", classify(data), []), - ]) - } - _ -> ret + let got = classify(data) + let error = #(dynamic.nil(), lua, [DeserializeError("UserData", got, [])]) + use <- bool.guard(got != "UserData", error) + use <- bool.guard( + !userdata_exists(lua, data), + #(dynamic.nil(), lua, [ + DeserializeError("UserData", "NonexistentUserData", []), + ]), + ) + case unwrap_userdata(decode(data, lua)) { + Ok(dyn) -> #(dyn, lua, []) + Error(Nil) -> error } } @@ -311,6 +313,12 @@ pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { let not_table_list = #([], lua, [DeserializeError("TableList", class, [])]) case class { "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #([], lua, [ + DeserializeError("TableList", "NonexistentTable", []), + ]), + ) let res = get_table_list_transform(lua, data, #([], lua, []), fn(it, acc) { case acc.2 { @@ -352,6 +360,10 @@ pub fn dict( let class = classify(data) case class { "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #(dict.new(), lua, [DeserializeError("Table", "NonexistentTable", [])]), + ) get_table_transform(lua, data, #(dict.new(), lua, []), 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. @@ -366,6 +378,12 @@ pub fn dict( }) } +@external(erlang, "glua_ffi", "table_exists") +fn table_exists(state: Lua, table: ValueRef) -> Bool + +@external(erlang, "glua_ffi", "userdata_exists") +fn userdata_exists(state: Lua, userdata: ValueRef) -> Bool + /// Preconditions: `table` is a lua table @external(erlang, "glua_ffi", "get_table_transform") fn get_table_transform( diff --git a/src/glua.gleam b/src/glua.gleam index 82e575e..b1d2cf0 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -36,7 +36,7 @@ pub type LuaRuntimeExceptionKind { BadArith(operator: String, args: List(String)) /// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument. AssertError(message: String) - /// An exception that could not be identified + /// An exception that could not be identified. UnknownException(dynamic.Dynamic) } diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1f21862..13972f0 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -6,7 +6,7 @@ -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/2, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, - get_table_transform/4, get_table_list_transform/4]). + get_table_transform/4, get_table_list_transform/4, userdata_exists/2, table_exists/2]). %% helper to convert luerl return values to a format %% that is more suitable for use in Gleam code @@ -37,7 +37,7 @@ classify(N) when is_float(N) -> classify({tref,_}) -> <<"Table">>; classify({usdref,_}) -> - <<"UserDef">>; + <<"UserData">>; classify({eref,_}) -> <<"Unknown">>; classify({funref,_,_}) -> @@ -197,6 +197,18 @@ sandbox_fun(St, Msg) -> end, luerl:encode(Fun, St). +table_exists(State, Table) -> + case luerl_heap:chk_table(Table, State) of + ok -> true; + error -> false + end. + +userdata_exists(State, Table) -> + case luerl_heap:chk_userdata(Table, State) of + ok -> true; + error -> false + end. + get_table_key(Lua, Table, Key) -> case luerl:get_table_key(Table, Key, Lua) of {ok, nil, _} -> diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 3ae414a..90f56de 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -331,3 +331,16 @@ pub fn function_err_test() { let assert Error([DeserializeError("Function", "Table", [])]) = deser.run(lua, func, deser.function) } + +pub fn nonexistent_table_test() { + let #(_lua, ref) = + glua.table(glua.new(), [#(glua.string("key"), glua.string("value"))]) + let assert Error([DeserializeError("Table", "NonexistentTable", [])]) = + deser.run(glua.new(), ref, deser.dict(deser.string, deser.string)) +} + +pub fn nonexistent_userdata_test() { + let #(_lua, ref) = glua.userdata(glua.new(), 3) + let assert Error([DeserializeError("UserData", "NonexistentUserData", [])]) = + deser.run(glua.new(), ref, deser.userdata) +} From f31b9ef0d07c2d0c770568e5419dd8f2f1e5bc84 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:41:48 -1000 Subject: [PATCH 33/41] Remove lua paramater from function --- src/glua.gleam | 11 ++++++---- src/glua_ffi.erl | 11 +++++----- test/deserialize_test.gleam | 5 +++-- test/glua_test.gleam | 44 +++++++++++++++++++++---------------- test/readme_test.gleam | 5 +++-- 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index b1d2cf0..f417fe7 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -95,9 +95,12 @@ pub fn table_decoder( @external(erlang, "glua_ffi", "wrap_fun") pub fn function( - lua: Lua, fun: fn(Lua, List(ValueRef)) -> #(Lua, List(ValueRef)), -) -> #(Lua, ValueRef) +) -> Function + +/// Downgrade a `Function` to a `ValueRef` +@external(erlang, "glua_ffi", "coerce") +pub fn func_to_ref(func: Function) -> ValueRef /// Creates a new Lua VM instance @external(erlang, "luerl", "init") @@ -427,8 +430,8 @@ pub fn call_function_by_name( args args: List(ValueRef), ) -> Result(#(Lua, List(ValueRef)), LuaError) { use fun <- result.try(get(lua, keys)) - call_function(lua, coerce_funciton(fun), args) + call_function(lua, coerce_function(fun), args) } @external(erlang, "glua_ffi", "coerce") -fn coerce_funciton(func: ValueRef) -> Function +fn coerce_function(func: ValueRef) -> Function diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 13972f0..a706354 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -3,7 +3,7 @@ -import(luerl_lib, [lua_error/2]). -include_lib("luerl/include/luerl.hrl"). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/2, sandbox_fun/2, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, +-export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/2, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, get_table_transform/4, get_table_list_transform/4, userdata_exists/2, table_exists/2]). @@ -183,13 +183,12 @@ encode_userdata(State, Values) -> {Data, St} = luerl_heap:alloc_userdata(Values, State), {St, Data}. -wrap_fun(State, Fun) -> - NewFun = fun(Args, St0) -> - {St1, Return} = Fun(St0, Args), +wrap_fun(Fun) -> + NewFun = fun(Args, St) -> + {St1, Return} = Fun(St, Args), {Return, St1} end, - {T, St} = luerl:encode(NewFun, State), - {St, T}. + #erl_func{code=NewFun}. sandbox_fun(St, Msg) -> Fun = fun(_, State) -> diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 90f56de..4a4d3ed 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -98,8 +98,9 @@ pub fn subfield_err_test() { pub fn field_metatable_test() { let lua = glua.new() let #(lua, data) = glua.table(lua, []) - let #(lua, func) = - glua.function(lua, fn(lua, _args) { #(lua, [glua.string("pong")]) }) + let func = + glua.function(fn(lua, _args) { #(lua, [glua.string("pong")]) }) + |> glua.func_to_ref let #(lua, metatable) = glua.table(lua, [ #(glua.string("__index"), func), diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 6d77709..793e80f 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -20,8 +20,8 @@ pub fn get_table_test() { #("euler's number", 3.0), ] - let #(lua, cool_numbers) = - glua.function(lua, fn(lua, _params) { + let cool_numbers = + glua.function(fn(lua, _params) { let #(lua, table) = glua.table( lua, @@ -30,6 +30,7 @@ pub fn get_table_test() { ) #(lua, [table]) }) + |> glua.func_to_ref let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) let assert Ok(#(lua, [table])) = @@ -209,7 +210,7 @@ pub fn set_test() { #(lua, list.map([count], glua.int)) } - let #(lua, encoded) = glua.function(lua, count_odd) + let encoded = glua.function(count_odd) |> glua.func_to_ref let assert Ok(lua) = glua.set(lua, ["count_odd"], encoded) let #(lua, arg) = @@ -223,22 +224,26 @@ pub fn set_test() { assert result == glua.int(5) - let #(lua, is_even) = - glua.function(lua, fn(lua, args) { - let assert [arg] = args - let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) - #(lua, list.map([int.is_even(arg)], glua.bool)) - }) - let #(lua, is_odd) = - glua.function(lua, fn(lua, args) { - let assert [arg] = args - let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) - #(lua, list.map([int.is_odd(arg)], glua.bool)) - }) let #(lua, tbl) = glua.table(lua, [ - #(glua.string("is_even"), is_even), - #(glua.string("is_odd"), is_odd), + #( + glua.string("is_even"), + glua.function(fn(lua, args) { + let assert [arg] = args + let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) + #(lua, list.map([int.is_even(arg)], glua.bool)) + }) + |> glua.func_to_ref, + ), + #( + glua.string("is_odd"), + glua.function(fn(lua, args) { + let assert [arg] = args + let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) + #(lua, list.map([int.is_odd(arg)], glua.bool)) + }) + |> glua.func_to_ref, + ), ]) let arg = glua.int(4) @@ -434,8 +439,9 @@ pub fn nested_function_references_test() { pub fn alloc_test() { let #(lua, table) = glua.table(glua.new(), []) - let #(lua, proxy) = - glua.function(lua, fn(lua, _args) { #(lua, [glua.string("constant")]) }) + let proxy = + glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) + |> glua.func_to_ref let #(lua, metatable) = glua.table(lua, [#(glua.string("__index"), proxy)]) let assert Ok(#(lua, _)) = glua.call_function_by_name(lua, ["setmetatable"], [table, metatable]) diff --git a/test/readme_test.gleam b/test/readme_test.gleam index 45bf65f..22a9b3e 100644 --- a/test/readme_test.gleam +++ b/test/readme_test.gleam @@ -137,8 +137,8 @@ pub fn call_functions_test() { pub fn expose_functions_test() { let lua = glua.new() - let #(lua, fun) = - glua.function(lua, fn(lua, args) { + let fun = + glua.function(fn(lua, args) { // Since Gleam is a statically typed language, each and every argument must be decoded let assert [x, min, max] = args let assert Ok(#(lua, x)) = deser.run(lua, x, deser.number) @@ -149,6 +149,7 @@ pub fn expose_functions_test() { #(lua, [glua.float(result)]) }) + |> glua.func_to_ref let keys = ["my_functions", "clamp"] From 5af5071be712495833542c0256cc9dc2c716213c Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:45:12 -1000 Subject: [PATCH 34/41] Rename `ValueRef` to just `Value` --- README.md | 2 +- src/deser.gleam | 80 ++++++++++++++++++------------------- src/glua.gleam | 80 ++++++++++++++++--------------------- test/deserialize_test.gleam | 2 +- test/glua_test.gleam | 12 +++--- test/readme_test.gleam | 2 +- 6 files changed, 83 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 7f281a6..db2effb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Glua -A library for embedding Lua in Gleam applications! +A library for embedding Lua in Gleam applications powered by [Luerl](https://github.com/rvirding/luerl)! [![Package Version](https://img.shields.io/hexpm/v/glua)](https://hex.pm/packages/glua) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/glua/) diff --git a/src/deser.gleam b/src/deser.gleam index 4dd93bf..4b8e7ad 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -10,21 +10,21 @@ import gleam/float import gleam/int import gleam/list import gleam/option.{type Option} -import glua.{type Lua, type ValueRef} +import glua.{type Lua, type Value} pub opaque type Deserializer(t) { - Deserializer(function: fn(Lua, ValueRef) -> Return(t)) + Deserializer(function: fn(Lua, Value) -> Return(t)) } pub type DeserializeError { - DeserializeError(expected: String, found: String, path: List(ValueRef)) + DeserializeError(expected: String, found: String, path: List(Value)) } type Return(t) = #(t, Lua, List(DeserializeError)) pub fn field( - field_path: ValueRef, + field_path: Value, field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { @@ -32,7 +32,7 @@ pub fn field( } pub fn subfield( - field_path: List(ValueRef), + field_path: List(Value), field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { @@ -57,11 +57,11 @@ pub fn subfield( fn index_into( lua: Lua, - path: List(ValueRef), - position: List(ValueRef), - inner: fn(Lua, ValueRef) -> Return(b), - data: ValueRef, - handle_miss: fn(Lua, ValueRef, List(ValueRef)) -> Return(b), + 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 { [] -> { @@ -95,7 +95,7 @@ fn index_into( pub fn run( lua: Lua, - data: ValueRef, + data: Value, deser: Deserializer(t), ) -> Result(#(Lua, t), List(DeserializeError)) { let #(maybe_invalid_data, lua, errors) = deser.function(lua, data) @@ -105,7 +105,7 @@ pub fn run( } } -pub fn at(path: List(ValueRef), inner: Deserializer(a)) -> Deserializer(a) { +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, lua, _) = inner.function(lua, data) @@ -118,11 +118,11 @@ pub fn at(path: List(ValueRef), inner: Deserializer(a)) -> Deserializer(a) { @external(erlang, "glua_ffi", "get_table_key") fn get_table_key( lua: Lua, - table: ValueRef, - key: ValueRef, -) -> Result(#(Lua, ValueRef), glua.LuaError) + table: Value, + key: Value, +) -> Result(#(Lua, Value), glua.LuaError) -fn push_path(layer: Return(t), path: List(ValueRef)) -> Return(t) { +fn push_path(layer: Return(t), path: List(Value)) -> Return(t) { let errors = list.map(layer.2, fn(error) { DeserializeError(..error, path: list.append(path, error.path)) @@ -137,7 +137,7 @@ pub fn success(lua: Lua, data: t) -> Deserializer(t) { pub fn deser_error( expected expected: String, - found found: ValueRef, + found found: Value, ) -> List(DeserializeError) { [DeserializeError(expected: expected, found: classify(found), path: [])] } @@ -146,7 +146,7 @@ pub fn deser_error( pub fn classify(a: anything) -> String pub fn optional_field( - key: ValueRef, + key: Value, default: t, field_decoder: Deserializer(t), next: fn(t) -> Deserializer(final), @@ -178,7 +178,7 @@ pub fn optional_field( } pub fn optionally_at( - path: List(ValueRef), + path: List(Value), default: a, inner: Deserializer(a), ) -> Deserializer(a) { @@ -191,7 +191,7 @@ pub fn optionally_at( fn run_dynamic_function( lua: Lua, - data: ValueRef, + data: Value, expected: String, zero: t, ) -> Return(t) { @@ -206,23 +206,23 @@ fn run_dynamic_function( /// Warning: Can panic @external(erlang, "luerl", "decode") -fn decode(a: ValueRef, lua: Lua) -> a +fn decode(a: Value, lua: Lua) -> a pub const string: Deserializer(String) = Deserializer(deser_string) -fn deser_string(lua, data: ValueRef) -> Return(String) { +fn deser_string(lua, data: Value) -> Return(String) { run_dynamic_function(lua, data, "String", "") } pub const bool: Deserializer(Bool) = Deserializer(deser_bool) -fn deser_bool(lua, data: ValueRef) -> Return(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: ValueRef) -> Return(Float) { +fn deser_num(lua, data: Value) -> Return(Float) { let got = classify(data) case got { "Float" -> #(decode(data, lua), lua, []) @@ -238,7 +238,7 @@ fn deser_num(lua, data: ValueRef) -> Return(Float) { pub const int: Deserializer(Int) = Deserializer(deser_int) -fn deser_int(lua, data: ValueRef) -> Return(Int) { +fn deser_int(lua, data: Value) -> Return(Int) { let got = classify(data) let error = #(0, lua, [ DeserializeError("Int", got, []), @@ -259,9 +259,9 @@ fn deser_int(lua, data: ValueRef) -> Return(Int) { } } -pub const raw: Deserializer(ValueRef) = Deserializer(decode_raw) +pub const raw: Deserializer(Value) = Deserializer(decode_raw) -fn decode_raw(lua, data: ValueRef) -> Return(ValueRef) { +fn decode_raw(lua, data: Value) -> Return(Value) { #(data, lua, []) } @@ -272,7 +272,7 @@ pub const userdata: Deserializer(dynamic.Dynamic) = Deserializer( @external(erlang, "glua_ffi", "unwrap_userdata") fn unwrap_userdata(a: userdata) -> Result(dynamic.Dynamic, Nil) -fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { +fn deser_user_defined(lua, data: Value) -> Return(dynamic.Dynamic) { let got = classify(data) let error = #(dynamic.nil(), lua, [DeserializeError("UserData", got, [])]) use <- bool.guard(got != "UserData", error) @@ -290,7 +290,7 @@ fn deser_user_defined(lua, data: ValueRef) -> Return(dynamic.Dynamic) { pub const function: Deserializer(glua.Function) = Deserializer(deser_function) -fn deser_function(lua: Lua, data: ValueRef) -> Return(glua.Function) { +fn deser_function(lua: Lua, data: Value) -> Return(glua.Function) { let got = classify(data) case got == "Function" { True -> #(coerce_funciton(data), lua, []) @@ -347,9 +347,9 @@ pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { @external(erlang, "glua_ffi", "get_table_list_transform") fn get_table_list_transform( lua: Lua, - table: ValueRef, + table: Value, accumulator: acc, - func: fn(ValueRef, acc) -> acc, + func: fn(Value, acc) -> acc, ) -> Result(acc, Nil) pub fn dict( @@ -379,27 +379,27 @@ pub fn dict( } @external(erlang, "glua_ffi", "table_exists") -fn table_exists(state: Lua, table: ValueRef) -> Bool +fn table_exists(state: Lua, table: Value) -> Bool @external(erlang, "glua_ffi", "userdata_exists") -fn userdata_exists(state: Lua, userdata: ValueRef) -> Bool +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: ValueRef, + table: Value, accumulator: acc, - func: fn(ValueRef, ValueRef, acc) -> acc, + func: fn(Value, Value, acc) -> acc, ) -> acc fn fold_dict( lua: Lua, acc: #(Dict(k, v), Lua, List(DeserializeError)), - key: ValueRef, - value: ValueRef, - key_decoder: fn(Lua, ValueRef) -> Return(k), - value_decoder: fn(Lua, ValueRef) -> Return(v), + 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) { @@ -490,7 +490,7 @@ pub fn one_of( } fn run_decoders( - data: ValueRef, + data: Value, lua: Lua, failure: Return(a), decoders: List(Deserializer(a)), diff --git a/src/glua.gleam b/src/glua.gleam index f417fe7..a92ff63 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -40,45 +40,41 @@ pub type LuaRuntimeExceptionKind { UnknownException(dynamic.Dynamic) } -/// The exception that happens when a functi /// Represents a chunk of Lua code that is already loaded into the Lua VM pub type Chunk -/// Represents a reference to a value inside the Lua environment. -/// -/// Each one of the functions that returns values from the Lua environment has a `ref_` counterpart -/// that will return references to the values instead of decoding them. -pub type ValueRef +/// Represents an encoded value inside the Lua environment. +pub type Value -/// A `ValueRef` that is a function +/// A `Value` that is a function pub type Function @external(erlang, "glua_ffi", "coerce_nil") -pub fn nil() -> ValueRef +pub fn nil() -> Value @external(erlang, "glua_ffi", "coerce") -pub fn string(v: String) -> ValueRef +pub fn string(v: String) -> Value @external(erlang, "glua_ffi", "coerce") -pub fn bool(v: Bool) -> ValueRef +pub fn bool(v: Bool) -> Value @external(erlang, "glua_ffi", "coerce") -pub fn int(v: Int) -> ValueRef +pub fn int(v: Int) -> Value @external(erlang, "glua_ffi", "coerce") -pub fn float(v: Float) -> ValueRef +pub fn float(v: Float) -> Value @external(erlang, "glua_ffi", "encode_table") -pub fn table(lua: Lua, values: List(#(ValueRef, ValueRef))) -> #(Lua, ValueRef) +pub fn table(lua: Lua, values: List(#(Value, Value))) -> #(Lua, Value) -pub fn table_list(lua: Lua, values: List(ValueRef)) -> #(Lua, ValueRef) { - list.map_fold(values, 1, fn(acc, ref) { #(acc + 1, #(int(acc), ref)) }) +pub fn table_list(lua: Lua, values: List(Value)) -> #(Lua, Value) { + list.map_fold(values, 1, fn(acc, val) { #(acc + 1, #(int(acc), val)) }) |> pair.second() |> table(lua, _) } @external(erlang, "glua_ffi", "encode_userdata") -pub fn userdata(lua: Lua, val: anything) -> #(Lua, ValueRef) +pub fn userdata(lua: Lua, val: anything) -> #(Lua, Value) pub fn table_decoder( keys_decoder: decode.Decoder(a), @@ -94,13 +90,11 @@ pub fn table_decoder( } @external(erlang, "glua_ffi", "wrap_fun") -pub fn function( - fun: fn(Lua, List(ValueRef)) -> #(Lua, List(ValueRef)), -) -> Function +pub fn function(fun: fn(Lua, List(Value)) -> #(Lua, List(Value))) -> Function -/// Downgrade a `Function` to a `ValueRef` +/// Downgrade a `Function` to a `Value` @external(erlang, "glua_ffi", "coerce") -pub fn func_to_ref(func: Function) -> ValueRef +pub fn func_to_val(func: Function) -> Value /// Creates a new Lua VM instance @external(erlang, "luerl", "init") @@ -162,7 +156,7 @@ pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) } @external(erlang, "glua_ffi", "sandbox_fun") -fn sandbox_fun(state: Lua, msg: String) -> #(ValueRef, Lua) +fn sandbox_fun(state: Lua, msg: String) -> #(Value, Lua) /// Gets a private value that is not exposed to the Lua runtime. /// @@ -183,10 +177,7 @@ pub fn get_private( } /// Same as `glua.get`, but returns a reference to the value instead of decoding it -pub fn get( - state lua: Lua, - keys keys: List(String), -) -> Result(ValueRef, LuaError) { +pub fn get(state lua: Lua, keys keys: List(String)) -> Result(Value, LuaError) { do_get(lua, keys) } @@ -231,7 +222,7 @@ pub fn get( pub fn set( state lua: Lua, keys keys: List(String), - value val: ValueRef, + value val: Value, ) -> Result(Lua, LuaError) { let state = { use acc, key <- list.try_fold(keys, #([], lua)) @@ -271,7 +262,7 @@ pub fn set_private(state lua: Lua, key key: String, value value: a) -> Lua { pub fn set_api( lua: Lua, keys: List(String), - values: List(#(String, ValueRef)), + values: List(#(String, Value)), ) -> Result(Lua, LuaError) { use state, #(key, val) <- list.try_fold(values, lua) set(state, list.append(keys, [key]), val) @@ -310,13 +301,13 @@ pub fn set_lua_paths( // TODO: Fix @external(erlang, "luerl_heap", "alloc_table") -fn do_alloc_table(content: List(a), lua: Lua) -> #(ValueRef, Lua) +fn do_alloc_table(content: List(a), lua: Lua) -> #(Value, Lua) @external(erlang, "glua_ffi", "get_private") fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) @external(erlang, "glua_ffi", "get_table_keys") -fn do_get(lua: Lua, keys: List(String)) -> Result(ValueRef, LuaError) +fn do_get(lua: Lua, keys: List(String)) -> Result(Value, LuaError) @external(erlang, "glua_ffi", "set_table_keys") fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) @@ -373,18 +364,18 @@ fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError) pub fn eval( state lua: Lua, code code: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) { +) -> Result(#(Lua, List(Value)), LuaError) { do_eval(lua, code) } @external(erlang, "glua_ffi", "eval") -fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(ValueRef)), LuaError) +fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError) /// Same as `glua.eval_chunk`, but returns references to the values instead of decode them pub fn eval_chunk( state lua: Lua, chunk chunk: Chunk, -) -> Result(#(Lua, List(ValueRef)), LuaError) { +) -> Result(#(Lua, List(Value)), LuaError) { do_eval_chunk(lua, chunk) } @@ -392,34 +383,31 @@ pub fn eval_chunk( fn do_eval_chunk( lua: Lua, chunk: Chunk, -) -> Result(#(Lua, List(ValueRef)), LuaError) +) -> Result(#(Lua, List(Value)), LuaError) /// Same as `glua.eval_file`, but returns references to the values instead of decode them. pub fn eval_file( state lua: Lua, path path: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) { +) -> Result(#(Lua, List(Value)), LuaError) { do_eval_file(lua, path) } @external(erlang, "glua_ffi", "eval_file") -fn do_eval_file( - lua: Lua, - path: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) +fn do_eval_file(lua: Lua, path: String) -> Result(#(Lua, List(Value)), LuaError) @external(erlang, "glua_ffi", "call_function") fn do_call_function( lua: Lua, fun: Function, - args: List(ValueRef), -) -> Result(#(Lua, List(ValueRef)), LuaError) + args: List(Value), +) -> Result(#(Lua, List(Value)), LuaError) pub fn call_function( state lua: Lua, fun fun: Function, - args args: List(ValueRef), -) -> Result(#(Lua, List(ValueRef)), LuaError) { + args args: List(Value), +) -> Result(#(Lua, List(Value)), LuaError) { do_call_function(lua, fun, args) } @@ -427,11 +415,11 @@ pub fn call_function( pub fn call_function_by_name( state lua: Lua, keys keys: List(String), - args args: List(ValueRef), -) -> Result(#(Lua, List(ValueRef)), LuaError) { + args args: List(Value), +) -> Result(#(Lua, List(Value)), LuaError) { use fun <- result.try(get(lua, keys)) call_function(lua, coerce_function(fun), args) } @external(erlang, "glua_ffi", "coerce") -fn coerce_function(func: ValueRef) -> Function +fn coerce_function(func: Value) -> Function diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 4a4d3ed..1625f7d 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -100,7 +100,7 @@ pub fn field_metatable_test() { let #(lua, data) = glua.table(lua, []) let func = glua.function(fn(lua, _args) { #(lua, [glua.string("pong")]) }) - |> glua.func_to_ref + |> glua.func_to_val let #(lua, metatable) = glua.table(lua, [ #(glua.string("__index"), func), diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 793e80f..6a41d98 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -30,7 +30,7 @@ pub fn get_table_test() { ) #(lua, [table]) }) - |> glua.func_to_ref + |> glua.func_to_val let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) let assert Ok(#(lua, [table])) = @@ -202,7 +202,7 @@ pub fn set_test() { let assert Ok(#(lua, val)) = deser.run(lua, val, deser.list(deser.int)) assert val == [4, 16, 49, 144] - let count_odd = fn(lua: glua.Lua, args: List(glua.ValueRef)) { + let count_odd = fn(lua: glua.Lua, args: List(glua.Value)) { let assert [list] = args let assert Ok(#(lua, list)) = deser.run(lua, list, deser.list(deser.int)) @@ -210,7 +210,7 @@ pub fn set_test() { #(lua, list.map([count], glua.int)) } - let encoded = glua.function(count_odd) |> glua.func_to_ref + let encoded = glua.function(count_odd) |> glua.func_to_val let assert Ok(lua) = glua.set(lua, ["count_odd"], encoded) let #(lua, arg) = @@ -233,7 +233,7 @@ pub fn set_test() { let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) #(lua, list.map([int.is_even(arg)], glua.bool)) }) - |> glua.func_to_ref, + |> glua.func_to_val, ), #( glua.string("is_odd"), @@ -242,7 +242,7 @@ pub fn set_test() { let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) #(lua, list.map([int.is_odd(arg)], glua.bool)) }) - |> glua.func_to_ref, + |> glua.func_to_val, ), ]) @@ -441,7 +441,7 @@ pub fn alloc_test() { let #(lua, table) = glua.table(glua.new(), []) let proxy = glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) - |> glua.func_to_ref + |> glua.func_to_val let #(lua, metatable) = glua.table(lua, [#(glua.string("__index"), proxy)]) let assert Ok(#(lua, _)) = glua.call_function_by_name(lua, ["setmetatable"], [table, metatable]) diff --git a/test/readme_test.gleam b/test/readme_test.gleam index 22a9b3e..7f6f43e 100644 --- a/test/readme_test.gleam +++ b/test/readme_test.gleam @@ -149,7 +149,7 @@ pub fn expose_functions_test() { #(lua, [glua.float(result)]) }) - |> glua.func_to_ref + |> glua.func_to_val let keys = ["my_functions", "clamp"] From 2f615e64459b61a8eae70fdbd3fbd5d4fcdef641 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:07:01 -1000 Subject: [PATCH 35/41] Remove unused functions --- src/glua.gleam | 13 -------- src/glua_ffi.erl | 85 +++--------------------------------------------- 2 files changed, 4 insertions(+), 94 deletions(-) diff --git a/src/glua.gleam b/src/glua.gleam index a92ff63..c9f1134 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -76,19 +76,6 @@ pub fn table_list(lua: Lua, values: List(Value)) -> #(Lua, Value) { @external(erlang, "glua_ffi", "encode_userdata") pub fn userdata(lua: Lua, val: anything) -> #(Lua, Value) -pub fn table_decoder( - keys_decoder: decode.Decoder(a), - values_decoder: decode.Decoder(b), -) -> decode.Decoder(List(#(a, b))) { - let inner = { - use key <- decode.field(0, keys_decoder) - use val <- decode.field(1, values_decoder) - decode.success(#(key, val)) - } - - decode.list(of: inner) -} - @external(erlang, "glua_ffi", "wrap_fun") pub fn function(fun: fn(Lua, List(Value)) -> #(Lua, List(Value))) -> Function diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index a706354..8677f0a 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -3,10 +3,10 @@ -import(luerl_lib, [lua_error/2]). -include_lib("luerl/include/luerl.hrl"). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/2, get_table_keys/2, get_table_keys_dec/2, get_table_key/3, - get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, encode_table/2, encode_userdata/2, - eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3, classify/1, unwrap_userdata/1, - get_table_transform/4, get_table_list_transform/4, userdata_exists/2, table_exists/2]). +-export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/2, get_table_keys/2, get_table_key/3, + get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_file/2, encode_table/2, encode_userdata/2, + eval_chunk/2, call_function/3, classify/1, unwrap_userdata/1, get_table_transform/4, + get_table_list_transform/4, userdata_exists/2, table_exists/2]). %% helper to convert luerl return values to a format %% that is more suitable for use in Gleam code @@ -84,52 +84,6 @@ do_list_transform(Arr, Acc, Func) -> throw:hole -> {error, nil} end. - -%% helper to determine if a value is encoded or not -%% borrowed from https://github.com/tv-labs/lua/blob/5bf2069c2bd0b8f19ae8f3ea1e6947a44c3754d8/lib/lua/util.ex#L19-L35 -%% Also see (luerl 1.5.1): https://hexdocs.pm/luerl/luerl.html#t:luerldata/0 -is_encoded(nil) -> - true; -is_encoded(true) -> - true; -is_encoded(false) -> - true; -is_encoded(Binary) when is_binary(Binary) -> - true; -is_encoded(N) when is_number(N) -> - true; -is_encoded({tref,_}) -> - true; -is_encoded({usrdef,_}) -> - true; -% Rationale: https://github.com/rvirding/luerl/blob/8756287ed2083795e7456855edf56a065a49b5aa/src/luerl.erl#L924-L939 -% has no mentions of eref -is_encoded({eref,_}) -> - false; -is_encoded({funref,_,_}) -> - true; -is_encoded({erl_func,_}) -> - true; -is_encoded({erl_mfa,_,_,_}) -> - true; -is_encoded(_) -> - false. - -encode(X, St0) -> - case is_encoded(X) of - true -> {X, St0}; - false -> luerl:encode(X, St0) - end. - -encode_list(L, St0) when is_list(L) -> - Enc = fun(X, {L1, St}) -> - {Enc, St1} = encode(X, St), - {[Enc | L1], St1} - end, - {L1, St1} = lists:foldl(Enc, {[], St0}, L), - {lists:reverse(L1), St1}. - - %% TODO: Improve compiler errors handling and try to detect more errors map_error({error, [{_, luerl_parse, Errors} | _], _}) -> FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors), @@ -228,16 +182,6 @@ get_table_keys(Lua, Keys) -> to_gleam(Other) end. -get_table_keys_dec(Lua, Keys) -> - case luerl:get_table_keys_dec(Keys, Lua) of - {ok, nil, _} -> - {error, key_not_found}; - {ok, Value, _} -> - {ok, Value}; - Other -> - to_gleam(Other) - end. - set_table_keys(Lua, Keys, Value) -> to_gleam(luerl:set_table_keys(Keys, Value, Lua)). @@ -253,37 +197,16 @@ eval(Lua, Code) -> to_gleam(luerl:do( unicode:characters_to_list(Code), Lua)). -eval_dec(Lua, Code) -> - to_gleam(luerl:do_dec( - unicode:characters_to_list(Code), Lua)). - eval_chunk(Lua, Chunk) -> to_gleam(luerl:call_chunk(Chunk, Lua)). -eval_chunk_dec(Lua, Chunk) -> - call_function_dec(Lua, Chunk, []). - eval_file(Lua, Path) -> to_gleam(luerl:dofile( unicode:characters_to_list(Path), Lua)). -eval_file_dec(Lua, Path) -> - to_gleam(luerl:dofile_dec( - unicode:characters_to_list(Path), Lua)). - call_function(Lua, Fun, Args) -> to_gleam(luerl:call(Fun, Args, Lua)). -call_function_dec(Lua, Fun, Args) -> - {EncodedArgs, St1} = encode_list(Args, Lua), - case luerl:call(Fun, EncodedArgs, St1) of - {ok, Ret, St2} -> - Values = luerl:decode_list(Ret, St2), - {ok, {St2, Values}}; - Other -> - to_gleam(Other) - end. - get_private(Lua, Key) -> try {ok, luerl:get_private(Key, Lua)} From 7e02f6fd965bfe19068fb016522261cccc545fe9 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:52:33 -1000 Subject: [PATCH 36/41] Make success inherit lua, make deser.run not return lua I should do more tests on how lua state actually gets passed through decoders since using vibes isn't the best idea for a library. Also deserializing something with deser.run shouldn't mutate state. --- src/deser.gleam | 15 +++++---- src/glua.gleam | 1 - test/deserialize_test.gleam | 61 ++++++++++++++++++------------------- test/glua_test.gleam | 27 ++++++++-------- test/readme_test.gleam | 20 ++++++------ 5 files changed, 60 insertions(+), 64 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 4b8e7ad..ddbef80 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -97,10 +97,10 @@ pub fn run( lua: Lua, data: Value, deser: Deserializer(t), -) -> Result(#(Lua, t), List(DeserializeError)) { - let #(maybe_invalid_data, lua, errors) = deser.function(lua, data) +) -> Result(t, List(DeserializeError)) { + let #(maybe_invalid_data, _lua, errors) = deser.function(lua, data) case errors { - [] -> Ok(#(lua, maybe_invalid_data)) + [] -> Ok(maybe_invalid_data) [_, ..] -> Error(errors) } } @@ -130,9 +130,8 @@ fn push_path(layer: Return(t), path: List(Value)) -> Return(t) { #(layer.0, layer.1, errors) } -// TOOD: Make it take lua -pub fn success(lua: Lua, data: t) -> Deserializer(t) { - Deserializer(function: fn(_, _) { #(data, lua, []) }) +pub fn success(data: t) -> Deserializer(t) { + Deserializer(function: fn(lua, _) { #(data, lua, []) }) } pub fn deser_error( @@ -463,11 +462,11 @@ pub fn collapse_errors( pub fn then( decoder: Deserializer(a), - next: fn(Lua, a) -> Deserializer(b), + next: fn(a) -> Deserializer(b), ) -> Deserializer(b) { Deserializer(function: fn(lua, dynamic_data) { let #(data, lua, errors) = decoder.function(lua, dynamic_data) - let decoder = next(lua, data) + let decoder = next(data) let #(data, lua, _) as layer = decoder.function(lua, dynamic_data) case errors { [] -> layer diff --git a/src/glua.gleam b/src/glua.gleam index c9f1134..2e389aa 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -3,7 +3,6 @@ //// Gleam wrapper around [Luerl](https://github.com/rvirding/luerl). import gleam/dynamic -import gleam/dynamic/decode import gleam/list import gleam/pair import gleam/result diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 1625f7d..e5eaadc 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -9,16 +9,15 @@ fn coerce_dynamic(a: anything) -> dynamic.Dynamic pub fn deserializer_test() { let lua = glua.new() - let assert Ok(#(_lua, "Hello")) = - deser.run(lua, glua.string("Hello"), deser.string) + let assert Ok("Hello") = deser.run(lua, glua.string("Hello"), deser.string) - let assert Ok(#(_lua, 42.0)) = deser.run(lua, glua.int(42), deser.number) + let assert Ok(42.0) = deser.run(lua, glua.int(42), deser.number) - let assert Ok(#(_lua, False)) = deser.run(lua, glua.bool(False), deser.bool) + let assert Ok(False) = deser.run(lua, glua.bool(False), deser.bool) let data = ["this", "is", "some", "random", "data"] let #(lua, ref) = data |> glua.userdata(lua, _) - let assert Ok(#(_lua, userdefined)) = deser.run(lua, ref, deser.userdata) + let assert Ok(userdefined) = deser.run(lua, ref, deser.userdata) assert userdefined == coerce_dynamic(data) } @@ -29,10 +28,10 @@ pub fn field_ok_test() { #(glua.string("red herring"), glua.string("not here!")), #(glua.string("name"), glua.string("Hina")), ]) - let assert Ok(#(_lua, val)) = + let assert Ok(val) = deser.run(lua, data, { use str <- deser.field(glua.string("name"), deser.string) - deser.success(lua, str) + deser.success(str) }) assert val == "Hina" } @@ -44,7 +43,7 @@ pub fn field_err_test() { let assert Error([DeserializeError("Field", "Nothing", path)]) = deser.run(lua, data, { use str <- deser.field(glua.string("name"), deser.string) - deser.success(lua, str) + deser.success(str) }) assert path == [glua.string("name")] } @@ -61,13 +60,13 @@ pub fn subfield_ok_test() { #(glua.string("name"), glua.string("Hina")), #(glua.string("friends"), inner), ]) - let assert Ok(#(_lua, val)) = + let assert Ok(val) = deser.run(lua, data, { use first <- deser.subfield( [glua.string("friends"), glua.int(1)], deser.string, ) - deser.success(lua, first) + deser.success(first) }) assert val == "Puffy" } @@ -90,7 +89,7 @@ pub fn subfield_err_test() { [glua.string("friends"), glua.int(1)], deser.string, ) - deser.success(lua, first) + deser.success(first) }) assert path == [glua.string("friends"), glua.int(1)] } @@ -107,13 +106,13 @@ pub fn field_metatable_test() { ]) let assert Ok(#(lua, [table])) = glua.call_function_by_name(lua, ["setmetatable"], [data, metatable]) - let assert Ok(#(_lua, val)) = + let assert Ok(val) = deser.run(lua, table, { use pong <- deser.field( glua.string("aasdlkjghasddlkjghasddklgjh;ksjdh"), deser.string, ) - deser.success(lua, pong) + deser.success(pong) }) assert val == "pong" } @@ -130,7 +129,7 @@ pub fn at_ok_test() { [glua.string("first"), glua.string("second"), glua.string("third")], deser.string, ) - let assert Ok(#(_lua, hi)) = deser.run(lua, first, third) + let assert Ok(hi) = deser.run(lua, first, third) assert hi == "hi" } @@ -160,7 +159,7 @@ pub fn optionally_at_ok_test() { let #(lua, nest) = glua.table(lua, [#(glua.string("ping"), glua.string("pong"))]) let #(lua, table) = glua.table(lua, [#(glua.string("nested"), nest)]) - let assert Ok(#(_lua, pong)) = + let assert Ok(pong) = deser.run( lua, table, @@ -171,7 +170,7 @@ pub fn optionally_at_ok_test() { ), ) assert "pong" == pong - let assert Ok(#(_lua, miss)) = + let assert Ok(miss) = deser.run( lua, table, @@ -188,24 +187,24 @@ pub fn optional_field_ok_test() { let lua = glua.new() let #(lua, table) = glua.table(lua, [#(glua.string("bullseye"), glua.string("hit"))]) - let assert Ok(#(_lua, hit)) = + let assert Ok(hit) = deser.run(lua, table, { use hit <- deser.optional_field( glua.string("bullseye"), "doh i missed", deser.string, ) - deser.success(lua, hit) + deser.success(hit) }) assert hit == "hit" - let assert Ok(#(_lua, miss)) = + let assert Ok(miss) = deser.run(lua, table, { use hit <- deser.optional_field( glua.string("bull'seye"), "doh i missed", deser.string, ) - deser.success(lua, hit) + deser.success(hit) }) assert miss == "doh i missed" @@ -220,7 +219,7 @@ pub fn table_decode_test() { points |> list.map(fn(pair) { #(glua.string(pair.0), glua.float(pair.1)) }), ) - let assert Ok(#(_lua, dict)) = + let assert Ok(dict) = deser.run(lua, data, deser.dict(deser.string, deser.number)) assert dict == dict.from_list(points) } @@ -238,7 +237,7 @@ pub fn table_list_decode_test() { meanings |> list.map(fn(pair) { #(glua.int(pair.0), glua.string(pair.1)) }), ) - let assert Ok(#(_lua, dict)) = + let assert Ok(dict) = deser.run(lua, data, deser.dict(deser.int, deser.string)) assert dict == dict.from_list(meanings) } @@ -259,11 +258,11 @@ pub fn table_list_ok_test() { greetings |> list.map(glua.string), ) - let assert Ok(#(lua, list)) = deser.run(lua, data, deser.list(deser.string)) + let assert Ok(list) = deser.run(lua, data, deser.list(deser.string)) assert list == greetings let #(lua, data) = glua.table(lua, []) - let assert Ok(#(_lua, [])) = deser.run(lua, data, deser.list(deser.string)) + let assert Ok([]) = deser.run(lua, data, deser.list(deser.string)) } pub fn table_list_err_test() { @@ -299,14 +298,14 @@ pub fn table_list_err_test() { pub fn then_test() { let positive_deser = { - use lua, num <- deser.then(deser.number) + use num <- deser.then(deser.number) case num >. 0.0 { False -> deser.failure(0.0, "PositiveNum") - True -> deser.success(lua, num) + True -> deser.success(num) } } let lua = glua.new() - let assert Ok(#(lua, 4.0)) = deser.run(lua, glua.int(4), positive_deser) + let assert Ok(4.0) = deser.run(lua, glua.int(4), positive_deser) let assert Error([DeserializeError("PositiveNum", "Int", [])]) = deser.run(lua, glua.int(-4), positive_deser) } @@ -314,17 +313,17 @@ pub fn then_test() { pub fn custom_function_ok_test() { let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return function () return 42 end") - let assert Ok(#(lua, func)) = deser.run(lua, func, deser.function) + let assert Ok(func) = deser.run(lua, func, deser.function) let assert Ok(#(lua, [num])) = glua.call_function(lua, func, []) - let assert Ok(#(_lua, 42.0)) = deser.run(lua, num, deser.number) + let assert Ok(42.0) = deser.run(lua, num, deser.number) } pub fn builtin_function_ok_test() { let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return string.upper") - let assert Ok(#(lua, func)) = deser.run(lua, func, deser.function) + let assert Ok(func) = deser.run(lua, func, deser.function) let assert Ok(#(lua, [str])) = glua.call_function(lua, func, [glua.string("hello")]) - let assert Ok(#(_lua, "HELLO")) = deser.run(lua, str, deser.string) + let assert Ok("HELLO") = deser.run(lua, str, deser.string) } pub fn function_err_test() { diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 6a41d98..2144231 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -36,7 +36,7 @@ pub fn get_table_test() { let assert Ok(#(lua, [table])) = glua.call_function_by_name(lua, ["cool_numbers"], []) - let assert Ok(#(_lua, table)) = + let assert Ok(table) = deser.run(lua, table, deser.dict(deser.string, deser.number)) assert table == dict.from_list(my_table) @@ -120,7 +120,7 @@ pub fn userdata_test() { let #(lua, data) = glua.userdata(lua, userdata) let assert Ok(lua) = glua.set(lua, ["my_userdata"], data) let assert Ok(#(lua, [result])) = glua.eval(lua, "return my_userdata") - let assert Ok(#(lua, result)) = deser.run(lua, result, deser.userdata) + let assert Ok(result) = deser.run(lua, result, deser.userdata) let assert Ok(result) = decode.run(result, userdata_decoder) assert result == userdata @@ -139,7 +139,7 @@ pub fn get_test() { let lua = glua.new() let assert Ok(pi) = glua.get(state: lua, keys: ["math", "pi"]) - let assert Ok(#(lua, pi)) = deser.run(lua, pi, deser.number) + let assert Ok(pi) = deser.run(lua, pi, deser.number) assert pi >. 3.14 && pi <. 3.15 @@ -181,7 +181,7 @@ pub fn set_test() { let assert Ok(lua) = glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) let assert Ok(result) = glua.get(state: lua, keys: ["_VERSION"]) - let assert Ok(#(lua, result)) = deser.run(lua, result, deser.string) + let assert Ok(result) = deser.run(lua, result, deser.string) assert result == "custom version" @@ -199,12 +199,12 @@ pub fn set_test() { let assert Ok(lua) = glua.set(lua, keys, encoded) let assert Ok(val) = glua.get(lua, keys) - let assert Ok(#(lua, val)) = deser.run(lua, val, deser.list(deser.int)) + let assert Ok(val) = deser.run(lua, val, deser.list(deser.int)) assert val == [4, 16, 49, 144] let count_odd = fn(lua: glua.Lua, args: List(glua.Value)) { let assert [list] = args - let assert Ok(#(lua, list)) = deser.run(lua, list, deser.list(deser.int)) + let assert Ok(list) = deser.run(lua, list, deser.list(deser.int)) let count = list.count(list, int.is_odd) #(lua, list.map([count], glua.int)) @@ -230,7 +230,7 @@ pub fn set_test() { glua.string("is_even"), glua.function(fn(lua, args) { let assert [arg] = args - let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) + let assert Ok(arg) = deser.run(lua, arg, deser.int) #(lua, list.map([int.is_even(arg)], glua.bool)) }) |> glua.func_to_val, @@ -239,7 +239,7 @@ pub fn set_test() { glua.string("is_odd"), glua.function(fn(lua, args) { let assert [arg] = args - let assert Ok(#(lua, arg)) = deser.run(lua, arg, deser.int) + let assert Ok(arg) = deser.run(lua, arg, deser.int) #(lua, list.map([int.is_odd(arg)], glua.bool)) }) |> glua.func_to_val, @@ -383,7 +383,7 @@ pub fn call_function_test() { glua.eval(state: glua.new(), code: "return string.reverse") let encoded = glua.string("auL") - let assert Ok(#(lua, fun)) = deser.run(lua, fun, deser.function) + let assert Ok(fun) = deser.run(lua, fun, deser.function) let assert Ok(#(lua, [result])) = glua.call_function(state: lua, fun: fun, args: [encoded]) @@ -392,7 +392,7 @@ pub fn call_function_test() { let assert Ok(#(lua, [fun])) = glua.eval(state: lua, code: "return function(a, b) return a .. b end") - let assert Ok(#(lua, fun)) = deser.run(lua, fun, deser.function) + let assert Ok(fun) = deser.run(lua, fun, deser.function) let args = list.map(["Lua in ", "Gleam"], glua.string) @@ -417,8 +417,7 @@ pub fn call_function_by_name_test() { let arg = glua.float(10.2) let assert Ok(#(lua, [result])) = glua.call_function_by_name(state: lua, keys: ["math", "type"], args: [arg]) - let assert Ok(#(_lua, result)) = - deser.run(lua, result, deser.optional(deser.string)) + let assert Ok(result) = deser.run(lua, result, deser.optional(deser.string)) assert result == option.Some("float") } @@ -427,9 +426,9 @@ pub fn nested_function_references_test() { let code = "return function() return math.sqrt end" let assert Ok(#(lua, [ref])) = glua.eval(state: glua.new(), code:) - let assert Ok(#(lua, fun)) = deser.run(lua, ref, deser.function) + let assert Ok(fun) = deser.run(lua, ref, deser.function) let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, fun:, args: []) - let assert Ok(#(lua, fun)) = deser.run(lua, ref, deser.function) + let assert Ok(fun) = deser.run(lua, ref, deser.function) let arg = glua.int(400) let assert Ok(#(_, [result])) = diff --git a/test/readme_test.gleam b/test/readme_test.gleam index 7f6f43e..51e2b21 100644 --- a/test/readme_test.gleam +++ b/test/readme_test.gleam @@ -33,10 +33,10 @@ pub fn deser_test() { let deserializer = { use name <- deser.field(glua.string("name"), deser.string) use language <- deser.field(glua.string("written_in"), deser.string) - deser.success(lua, Project(name:, language:)) + deser.success(Project(name:, language:)) } - let assert Ok(#(_lua, project)) = deser.run(lua, table, deserializer) + let assert Ok(project) = deser.run(lua, table, deserializer) assert project == Project(name: "glua", language: "gleam") } @@ -103,10 +103,10 @@ pub fn table_deocding_test() { let assert Ok(#(lua, [result])) = glua.eval(state: lua, code: "return my_table.my_second_value") - let assert Ok(#(lua, 2.1)) = deser.run(lua, result, deser.number) + let assert Ok(2.1) = deser.run(lua, result, deser.number) // or we can get the whole table and decode it back to a list of tuples let assert Ok(table) = glua.get(state: lua, keys: ["my_table"]) - let assert Ok(#(_lua, table)) = + let assert Ok(table) = deser.run(lua, table, deser.dict(deser.string, deser.number)) assert table @@ -116,13 +116,13 @@ pub fn table_deocding_test() { pub fn call_functions_test() { let lua = glua.new() let assert Ok(val) = glua.get(state: lua, keys: ["math", "max"]) - let assert Ok(#(lua, fun)) = deser.run(lua, val, deser.function) + let assert Ok(fun) = deser.run(lua, val, deser.function) let args = [1, 20, 7, 18] |> list.map(glua.int) let assert Ok(#(lua, [result])) = glua.call_function(state: lua, fun: fun, args:) - let assert Ok(#(lua, result)) = deser.run(lua, result, deser.number) + let assert Ok(result) = deser.run(lua, result, deser.number) assert result == 20.0 @@ -130,7 +130,7 @@ pub fn call_functions_test() { let assert Ok(#(_lua, [result])) = glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) - let assert Ok(#(_lua, result)) = deser.run(lua, result, deser.number) + let assert Ok(result) = deser.run(lua, result, deser.number) assert result == 20.0 } @@ -141,9 +141,9 @@ pub fn expose_functions_test() { glua.function(fn(lua, args) { // Since Gleam is a statically typed language, each and every argument must be decoded let assert [x, min, max] = args - let assert Ok(#(lua, x)) = deser.run(lua, x, deser.number) - let assert Ok(#(lua, min)) = deser.run(lua, min, deser.number) - let assert Ok(#(lua, max)) = deser.run(lua, max, deser.number) + let assert Ok(x) = deser.run(lua, x, deser.number) + let assert Ok(min) = deser.run(lua, min, deser.number) + let assert Ok(max) = deser.run(lua, max, deser.number) let result = float.clamp(x, min, max) From 6c4d8ff01b1f256653553809302f7248a369ad41 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:45:11 -1000 Subject: [PATCH 37/41] Make deser.then provide lua --- src/deser.gleam | 4 ++-- test/deserialize_test.gleam | 39 ++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index ddbef80..4fb1413 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -462,11 +462,11 @@ pub fn collapse_errors( pub fn then( decoder: Deserializer(a), - next: fn(a) -> Deserializer(b), + next: fn(Lua, a) -> Deserializer(b), ) -> Deserializer(b) { Deserializer(function: fn(lua, dynamic_data) { let #(data, lua, errors) = decoder.function(lua, dynamic_data) - let decoder = next(data) + let decoder = next(lua, data) let #(data, lua, _) as layer = decoder.function(lua, dynamic_data) case errors { [] -> layer diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index e5eaadc..7b6e1d2 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -1,7 +1,9 @@ import deser.{DeserializeError} +import gleam/bool import gleam/dict import gleam/dynamic import gleam/list +import gleam/pair import glua @external(erlang, "glua_ffi", "coerce") @@ -298,7 +300,7 @@ pub fn table_list_err_test() { pub fn then_test() { let positive_deser = { - use num <- deser.then(deser.number) + use _lua, num <- deser.then(deser.number) case num >. 0.0 { False -> deser.failure(0.0, "PositiveNum") True -> deser.success(num) @@ -310,6 +312,41 @@ pub fn then_test() { deser.run(lua, glua.int(-4), positive_deser) } +type Person { + Person(name: String, age: Int) +} + +pub fn then_state_test() { + let person_deser = { + use lua, table <- deser.then(deser.raw) + let assert Ok(#(lua, [meta])) = + glua.call_function_by_name(lua, ["getmetatable"], [table]) + let is_person = case + deser.run(lua, meta, deser.at([glua.string("type")], deser.string)) + { + Ok(kind) -> kind == "person" + Error(_) -> False + } + use <- bool.guard(!is_person, deser.failure(Person("", 0), "Person")) + use name <- deser.field(glua.string("name"), deser.string) + use age <- deser.field(glua.string("age"), deser.int) + deser.success(Person(name, age)) + } + + let lua = glua.new() + let #(lua, data) = + glua.table(lua, [ + #(glua.string("name"), glua.string("Captain Falcon")), + #(glua.string("age"), glua.int(36)), + ]) + let #(lua, metatable) = + glua.table(lua, [#(glua.string("type"), glua.string("person"))]) + let assert Ok(#(lua, _)) = + glua.call_function_by_name(lua, ["setmetatable"], [data, metatable]) + let assert Ok(person) = deser.run(lua, data, person_deser) + assert Person("Captain Falcon", 36) == person +} + pub fn custom_function_ok_test() { let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return function () return 42 end") From 71fae554905fed153c8fb46f81d3ccbc326dc7bf Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:55:59 -1000 Subject: [PATCH 38/41] Remove deserializer state mutation 1. Deserializers should not mutate state 2. It is difficult to implement consistently 3. Custom decoders are harder to write 4. #(lua, data) --- src/deser.gleam | 141 ++++++++++++++++---------------- test/deserialize_test.gleam | 1 - test/mut_deserialize_test.gleam | 0 3 files changed, 70 insertions(+), 72 deletions(-) create mode 100644 test/mut_deserialize_test.gleam diff --git a/src/deser.gleam b/src/deser.gleam index 4fb1413..7f4542e 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -21,7 +21,7 @@ pub type DeserializeError { } type Return(t) = - #(t, Lua, List(DeserializeError)) + #(t, List(DeserializeError)) pub fn field( field_path: Value, @@ -37,7 +37,7 @@ pub fn subfield( next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { Deserializer(function: fn(lua, data) { - let #(out, lua, errors1) = + let #(out, errors1) = index_into( lua, field_path, @@ -45,13 +45,13 @@ pub fn subfield( field_decoder.function, data, fn(lua, data, position) { - let #(default, lua, _) = field_decoder.function(lua, data) - #(default, lua, [DeserializeError("Field", "Nothing", [])]) + let #(default, _) = field_decoder.function(lua, data) + #(default, [DeserializeError("Field", "Nothing", [])]) |> push_path(list.reverse(position)) }, ) - let #(out, lua, errors2) = next(out).function(lua, data) - #(out, lua, list.append(errors1, errors2)) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) }) } @@ -84,8 +84,8 @@ fn index_into( handle_miss(lua, data, [key, ..position]) } Error(_err) -> { - let #(default, lua, _) = inner(lua, data) - #(default, lua, [DeserializeError("Table", classify(data), [])]) + let #(default, _) = inner(lua, data) + #(default, [DeserializeError("Table", classify(data), [])]) |> push_path(list.reverse(position)) } } @@ -98,7 +98,7 @@ pub fn run( data: Value, deser: Deserializer(t), ) -> Result(t, List(DeserializeError)) { - let #(maybe_invalid_data, _lua, errors) = deser.function(lua, data) + let #(maybe_invalid_data, errors) = deser.function(lua, data) case errors { [] -> Ok(maybe_invalid_data) [_, ..] -> Error(errors) @@ -108,8 +108,8 @@ pub fn run( 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, lua, _) = inner.function(lua, data) - #(default, lua, [DeserializeError("Field", "Nothing", [])]) + let #(default, _) = inner.function(lua, data) + #(default, [DeserializeError("Field", "Nothing", [])]) |> push_path(list.reverse(position)) }) }) @@ -124,14 +124,14 @@ fn get_table_key( fn push_path(layer: Return(t), path: List(Value)) -> Return(t) { let errors = - list.map(layer.2, fn(error) { + list.map(layer.1, fn(error) { DeserializeError(..error, path: list.append(path, error.path)) }) - #(layer.0, layer.1, errors) + #(layer.0, errors) } pub fn success(data: t) -> Deserializer(t) { - Deserializer(function: fn(lua, _) { #(data, lua, []) }) + Deserializer(function: fn(_lua, _) { #(data, []) }) } pub fn deser_error( @@ -151,7 +151,7 @@ pub fn optional_field( next: fn(t) -> Deserializer(final), ) -> Deserializer(final) { Deserializer(function: fn(lua, data) { - let #(out, lua, errors1) = + let #(out, errors1) = case get_table_key(lua, data, key) { Ok(#(lua, data)) -> { field_decoder.function(lua, data) @@ -162,17 +162,17 @@ pub fn optional_field( exception: glua.IllegalIndex(_, _), state: _, )) -> { - #(default, lua, []) + #(default, []) } Error(_err) -> { - #(default, lua, [ + #(default, [ DeserializeError("Table", classify(data), []), ]) } } |> push_path([key]) - let #(out, lua, errors2) = next(out).function(lua, data) - #(out, lua, list.append(errors1, errors2)) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) }) } @@ -183,7 +183,7 @@ pub fn optionally_at( ) -> Deserializer(a) { Deserializer(function: fn(lua, data) { index_into(lua, path, [], inner.function, data, fn(_, _, _) { - #(default, lua, []) + #(default, []) }) }) } @@ -196,8 +196,8 @@ fn run_dynamic_function( ) -> Return(t) { let got = classify(data) case got == expected { - True -> #(decode(data, lua), lua, []) - False -> #(zero, lua, [ + True -> #(decode(data, lua), []) + False -> #(zero, [ DeserializeError(expected, got, []), ]) } @@ -224,12 +224,12 @@ 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), lua, []) + "Float" -> #(decode(data, lua), []) "Int" -> { let int: Int = decode(data, lua) - #(int.to_float(int), lua, []) + #(int.to_float(int), []) } - _ -> #(0.0, lua, [ + _ -> #(0.0, [ DeserializeError("Number", got, []), ]) } @@ -239,7 +239,7 @@ pub const int: Deserializer(Int) = Deserializer(deser_int) fn deser_int(lua, data: Value) -> Return(Int) { let got = classify(data) - let error = #(0, lua, [ + let error = #(0, [ DeserializeError("Int", got, []), ]) case got { @@ -247,12 +247,12 @@ fn deser_int(lua, data: Value) -> Return(Int) { let float: Float = decode(data, lua) let int = float.truncate(float) case int.to_float(int) == float { - True -> #(int, lua, []) + True -> #(int, []) False -> error } } "Int" -> { - #(decode(data, lua), lua, []) + #(decode(data, lua), []) } _ -> error } @@ -260,8 +260,8 @@ fn deser_int(lua, data: Value) -> Return(Int) { pub const raw: Deserializer(Value) = Deserializer(decode_raw) -fn decode_raw(lua, data: Value) -> Return(Value) { - #(data, lua, []) +fn decode_raw(_lua, data: Value) -> Return(Value) { + #(data, []) } pub const userdata: Deserializer(dynamic.Dynamic) = Deserializer( @@ -273,27 +273,27 @@ 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(), lua, [DeserializeError("UserData", got, [])]) + let error = #(dynamic.nil(), [DeserializeError("UserData", got, [])]) use <- bool.guard(got != "UserData", error) use <- bool.guard( !userdata_exists(lua, data), - #(dynamic.nil(), lua, [ + #(dynamic.nil(), [ DeserializeError("UserData", "NonexistentUserData", []), ]), ) case unwrap_userdata(decode(data, lua)) { - Ok(dyn) -> #(dyn, lua, []) + Ok(dyn) -> #(dyn, []) Error(Nil) -> error } } pub const function: Deserializer(glua.Function) = Deserializer(deser_function) -fn deser_function(lua: Lua, data: Value) -> Return(glua.Function) { +fn deser_function(_lua: Lua, data: Value) -> Return(glua.Function) { let got = classify(data) case got == "Function" { - True -> #(coerce_funciton(data), lua, []) - False -> #(coerce_funciton(Nil), lua, [ + True -> #(coerce_funciton(data), []) + False -> #(coerce_funciton(Nil), [ DeserializeError("Function", got, []), ]) } @@ -309,25 +309,25 @@ fn coerce_funciton(func: anything) -> glua.Function pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { Deserializer(fn(lua, data) { let class = classify(data) - let not_table_list = #([], lua, [DeserializeError("TableList", class, [])]) + let not_table_list = #([], [DeserializeError("TableList", class, [])]) case class { "Table" -> { use <- bool.guard( !table_exists(lua, data), - #([], lua, [ + #([], [ DeserializeError("TableList", "NonexistentTable", []), ]), ) let res = - get_table_list_transform(lua, data, #([], lua, []), fn(it, acc) { - case acc.2 { + get_table_list_transform(lua, data, #([], []), fn(it, acc) { + case acc.1 { [] -> { case inner.function(lua, it) { - #(value, lua, []) -> { - #(list.prepend(acc.0, value), lua, acc.2) + #(value, []) -> { + #(list.prepend(acc.0, value), acc.1) } - #(_, lua, errors) -> - push_path(#([], lua, errors), [glua.string("items")]) + #(_, errors) -> + push_path(#([], errors), [glua.string("items")]) } } [_, ..] -> acc @@ -361,18 +361,18 @@ pub fn dict( "Table" -> { use <- bool.guard( !table_exists(lua, data), - #(dict.new(), lua, [DeserializeError("Table", "NonexistentTable", [])]), + #(dict.new(), [DeserializeError("Table", "NonexistentTable", [])]), ) - get_table_transform(lua, data, #(dict.new(), lua, []), fn(k, v, a) { + 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.2 { + case a.1 { [] -> fold_dict(lua, a, k, v, key.function, value.function) [_, ..] -> a } }) } - _ -> #(dict.new(), lua, [DeserializeError("Table", class, [])]) + _ -> #(dict.new(), [DeserializeError("Table", class, [])]) } }) } @@ -394,7 +394,7 @@ fn get_table_transform( fn fold_dict( lua: Lua, - acc: #(Dict(k, v), Lua, List(DeserializeError)), + acc: #(Dict(k, v), List(DeserializeError)), key: Value, value: Value, key_decoder: fn(Lua, Value) -> Return(k), @@ -402,29 +402,28 @@ fn fold_dict( ) -> Return(Dict(k, v)) { // First we decode the key. case key_decoder(lua, key) { - #(key, lua, []) -> + #(key, []) -> // Then we decode the value. case value_decoder(lua, value) { - #(value, lua, []) -> { + #(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, lua, acc.2) + #(dict, acc.1) } - #(_, lua, errors) -> - push_path(#(dict.new(), lua, errors), [glua.string("values")]) + #(_, errors) -> + push_path(#(dict.new(), errors), [glua.string("values")]) } - #(_, lua, errors) -> - push_path(#(dict.new(), lua, errors), [glua.string("keys")]) + #(_, 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, lua, []) + "Nil" -> #(option.None, []) _ -> { - let #(data, lua, errors) = inner.function(lua, data) - #(option.Some(data), lua, errors) + let #(data, errors) = inner.function(lua, data) + #(option.Some(data), errors) } } }) @@ -432,8 +431,8 @@ pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) { Deserializer(function: fn(lua, d) { - let #(data, lua, errors) = decoder.function(lua, d) - #(transformer(data), lua, errors) + let #(data, errors) = decoder.function(lua, d) + #(transformer(data), errors) }) } @@ -442,8 +441,8 @@ pub fn map_errors( transformer: fn(List(DeserializeError)) -> List(DeserializeError), ) -> Deserializer(a) { Deserializer(function: fn(lua, d) { - let #(data, lua, errors) = decoder.function(lua, d) - #(data, lua, transformer(errors)) + let #(data, errors) = decoder.function(lua, d) + #(data, transformer(errors)) }) } @@ -452,10 +451,10 @@ pub fn collapse_errors( name: String, ) -> Deserializer(a) { Deserializer(function: fn(lua, dynamic_data) { - let #(data, lua, errors) as layer = decoder.function(lua, dynamic_data) + let #(data, errors) as layer = decoder.function(lua, dynamic_data) case errors { [] -> layer - [_, ..] -> #(data, lua, deser_error(name, dynamic_data)) + [_, ..] -> #(data, deser_error(name, dynamic_data)) } }) } @@ -465,12 +464,12 @@ pub fn then( next: fn(Lua, a) -> Deserializer(b), ) -> Deserializer(b) { Deserializer(function: fn(lua, dynamic_data) { - let #(data, lua, errors) = decoder.function(lua, dynamic_data) + let #(data, errors) = decoder.function(lua, dynamic_data) let decoder = next(lua, data) - let #(data, lua, _) as layer = decoder.function(lua, dynamic_data) + let #(data, _) as layer = decoder.function(lua, dynamic_data) case errors { [] -> layer - [_, ..] -> #(data, lua, errors) + [_, ..] -> #(data, errors) } }) } @@ -480,7 +479,7 @@ pub fn one_of( or alternatives: List(Deserializer(a)), ) -> Deserializer(a) { Deserializer(function: fn(lua, dynamic_data) { - let #(_, lua, errors) as layer = first.function(lua, dynamic_data) + let #(_, errors) as layer = first.function(lua, dynamic_data) case errors { [] -> layer [_, ..] -> run_decoders(dynamic_data, lua, layer, alternatives) @@ -498,7 +497,7 @@ fn run_decoders( [] -> failure [decoder, ..decoders] -> { - let #(_, lua, errors) as layer = decoder.function(lua, data) + let #(_, errors) as layer = decoder.function(lua, data) case errors { [] -> layer [_, ..] -> run_decoders(data, lua, failure, decoders) @@ -508,7 +507,7 @@ fn run_decoders( } pub fn failure(zero: a, expected: String) -> Deserializer(a) { - Deserializer(function: fn(lua, d) { #(zero, lua, deser_error(expected, d)) }) + Deserializer(function: fn(_lua, d) { #(zero, deser_error(expected, d)) }) } pub fn recursive(inner: fn() -> Deserializer(a)) -> Deserializer(a) { diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 7b6e1d2..0bf75b1 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -3,7 +3,6 @@ import gleam/bool import gleam/dict import gleam/dynamic import gleam/list -import gleam/pair import glua @external(erlang, "glua_ffi", "coerce") diff --git a/test/mut_deserialize_test.gleam b/test/mut_deserialize_test.gleam new file mode 100644 index 0000000..e69de29 From 3be357c7c6fb378b6189ecd3753209a814310c51 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:12:08 -1000 Subject: [PATCH 39/41] Allow functions to return errors --- src/deser.gleam | 16 +++++++++++++++ src/glua.gleam | 7 ++++++- src/glua_ffi.erl | 16 ++++++++++----- test.lua | 1 + test/deserialize_test.gleam | 2 +- test/glua_test.gleam | 41 ++++++++++++++++++++++++++++++++----- test/readme_test.gleam | 2 +- 7 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 test.lua diff --git a/src/deser.gleam b/src/deser.gleam index 7f4542e..5a6d74d 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -10,6 +10,7 @@ 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) { @@ -105,6 +106,21 @@ pub fn run( } } +pub fn run_list(lua: Lua, data: List(Value), deser: Deserializer(t)) { + let #(lua, list) = glua.table_list(lua, data) + run(lua, list, deser) +} + +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) { diff --git a/src/glua.gleam b/src/glua.gleam index 2e389aa..73e31f3 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -76,7 +76,9 @@ pub fn table_list(lua: Lua, values: List(Value)) -> #(Lua, Value) { pub fn userdata(lua: Lua, val: anything) -> #(Lua, Value) @external(erlang, "glua_ffi", "wrap_fun") -pub fn function(fun: fn(Lua, List(Value)) -> #(Lua, List(Value))) -> Function +pub fn function( + fun: fn(Lua, List(Value)) -> Result(#(Lua, List(Value)), #(Lua, List(String))), +) -> Function /// Downgrade a `Function` to a `Value` @external(erlang, "glua_ffi", "coerce") @@ -86,6 +88,9 @@ pub fn func_to_val(func: Function) -> Value @external(erlang, "luerl", "init") pub fn new() -> Lua +@external(erlang, "glua_ffi", "classify_type") +pub fn typeof(val: Value) -> String + /// List of Lua modules and functions that will be sandboxed by default pub const default_sandbox = [ ["io"], diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 8677f0a..78c9563 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -5,7 +5,7 @@ -export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/2, get_table_keys/2, get_table_key/3, get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_file/2, encode_table/2, encode_userdata/2, - eval_chunk/2, call_function/3, classify/1, unwrap_userdata/1, get_table_transform/4, + eval_chunk/2, call_function/3, classify/1, unwrap_userdata/1, get_table_transform/4, get_table_list_transform/4, userdata_exists/2, table_exists/2]). %% helper to convert luerl return values to a format @@ -129,7 +129,6 @@ unwrap_userdata(_) -> {error, nil}. encode_table(State, Values) -> - % io:format("THING ~p~n, ~p~n", [State, Values]), {Data, St} = luerl_heap:alloc_table(Values, State), {St, Data}. @@ -138,9 +137,16 @@ encode_userdata(State, Values) -> {St, Data}. wrap_fun(Fun) -> - NewFun = fun(Args, St) -> - {St1, Return} = Fun(St, Args), - {Return, St1} + NewFun = fun(Args, StateIn) -> + {Status, Result} = Fun(StateIn, Args), + case Status of + ok -> + {StateOut, Return} = Result, + {Return, StateOut}; + error -> + {StateOut, Msgs} = Result, + {error, map_error(lua_error({error_call, Msgs}, StateOut))} + end end, #erl_func{code=NewFun}. diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..b098e50 --- /dev/null +++ b/test.lua @@ -0,0 +1 @@ +print(type({})) diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 0bf75b1..6dbd2b6 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -99,7 +99,7 @@ pub fn field_metatable_test() { let lua = glua.new() let #(lua, data) = glua.table(lua, []) let func = - glua.function(fn(lua, _args) { #(lua, [glua.string("pong")]) }) + glua.function(fn(lua, _args) { Ok(#(lua, [glua.string("pong")])) }) |> glua.func_to_val let #(lua, metatable) = glua.table(lua, [ diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 2144231..8de5d5d 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -4,6 +4,7 @@ import gleam/dynamic/decode import gleam/int import gleam/list import gleam/option +import gleam/result import gleeunit import glua @@ -28,7 +29,7 @@ pub fn get_table_test() { my_table |> list.map(fn(pair) { #(glua.string(pair.0), glua.float(pair.1)) }), ) - #(lua, [table]) + Ok(#(lua, [table])) }) |> glua.func_to_val @@ -207,7 +208,7 @@ pub fn set_test() { let assert Ok(list) = deser.run(lua, list, deser.list(deser.int)) let count = list.count(list, int.is_odd) - #(lua, list.map([count], glua.int)) + Ok(#(lua, list.map([count], glua.int))) } let encoded = glua.function(count_odd) |> glua.func_to_val @@ -231,7 +232,7 @@ pub fn set_test() { glua.function(fn(lua, args) { let assert [arg] = args let assert Ok(arg) = deser.run(lua, arg, deser.int) - #(lua, list.map([int.is_even(arg)], glua.bool)) + Ok(#(lua, list.map([int.is_even(arg)], glua.bool))) }) |> glua.func_to_val, ), @@ -240,7 +241,7 @@ pub fn set_test() { glua.function(fn(lua, args) { let assert [arg] = args let assert Ok(arg) = deser.run(lua, arg, deser.int) - #(lua, list.map([int.is_odd(arg)], glua.bool)) + Ok(#(lua, list.map([int.is_odd(arg)], glua.bool))) }) |> glua.func_to_val, ), @@ -439,7 +440,7 @@ pub fn nested_function_references_test() { pub fn alloc_test() { let #(lua, table) = glua.table(glua.new(), []) let proxy = - glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) }) + glua.function(fn(lua, _args) { Ok(#(lua, [glua.string("constant")])) }) |> glua.func_to_val let #(lua, metatable) = glua.table(lua, [#(glua.string("__index"), proxy)]) let assert Ok(#(lua, _)) = @@ -453,3 +454,33 @@ pub fn alloc_test() { assert ret1 == glua.string("constant") assert ret2 == glua.string("constant") } + +fn bad_arg_error(lua: glua.Lua, errors: List(deser.DeserializeError)) { + #(lua, errors |> list.map(deser.error_to_string)) +} + +pub fn throw_error_test() { + let add_func = + glua.function(fn(lua, params) { + let result = + deser.run_list(lua, params, { + use augend <- deser.field(glua.int(1), deser.number) + use addend <- deser.field(glua.int(2), deser.number) + deser.success(#(augend, addend)) + }) + |> result.map_error(bad_arg_error(lua, _)) + use #(augend, addend) <- result.try(result) + Ok(#(lua, [glua.float(augend +. addend)])) + }) + let lua = glua.new() + let assert Ok(#(lua, [result])) = + glua.call_function(lua, add_func, [glua.int(1), glua.int(4)]) + let assert Ok(5.0) = deser.run(lua, result, deser.number) + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(["Expected Field got Nothing at [2]"]), + state: _lua, + )) = + glua.call_function(lua, add_func, [ + glua.int(1), + ]) +} diff --git a/test/readme_test.gleam b/test/readme_test.gleam index 51e2b21..4dd14b9 100644 --- a/test/readme_test.gleam +++ b/test/readme_test.gleam @@ -147,7 +147,7 @@ pub fn expose_functions_test() { let result = float.clamp(x, min, max) - #(lua, [glua.float(result)]) + Ok(#(lua, [glua.float(result)])) }) |> glua.func_to_val From 295c28353ee9012e100ae5e43ef8d920a1b10410 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:42:19 -1000 Subject: [PATCH 40/41] Add deser.item and rename deser.run_list to deser.run_multi --- src/deser.gleam | 94 ++++++++++++++------- src/glua_ffi.erl | 158 +++++++++++++++++++++++++----------- test/deserialize_test.gleam | 41 ++++++++++ test/glua_test.gleam | 28 ++++--- 4 files changed, 234 insertions(+), 87 deletions(-) diff --git a/src/deser.gleam b/src/deser.gleam index 5a6d74d..08cfa49 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -72,21 +72,19 @@ fn index_into( } [key, ..path] -> { - case get_table_key(lua, data, key) { - Ok(#(lua, data)) -> { - index_into(lua, path, [key, ..position], inner, data, handle_miss) - } - // NOTE: I don't feel comfortable matching on this - Error(glua.KeyNotFound) - | Error(glua.LuaRuntimeException( - exception: glua.IllegalIndex(_, _), - state: _, - )) -> { - handle_miss(lua, data, [key, ..position]) - } - Error(_err) -> { + 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", classify(data), [])]) + #(default, [DeserializeError("Table", class, [])]) |> push_path(list.reverse(position)) } } @@ -106,11 +104,47 @@ pub fn run( } } -pub fn run_list(lua: Lua, data: List(Value), deser: Deserializer(t)) { - let #(lua, list) = glua.table_list(lua, data) +/// 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 @@ -136,7 +170,7 @@ fn get_table_key( lua: Lua, table: Value, key: Value, -) -> Result(#(Lua, Value), glua.LuaError) +) -> Result(#(Lua, Value), Nil) fn push_path(layer: Return(t), path: List(Value)) -> Return(t) { let errors = @@ -167,24 +201,16 @@ pub fn optional_field( 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) } - // NOTE: I don't feel comfortable matching on this - Error(glua.KeyNotFound) - | Error(glua.LuaRuntimeException( - exception: glua.IllegalIndex(_, _), - state: _, - )) -> { - #(default, []) - } - Error(_err) -> { - #(default, [ - DeserializeError("Table", classify(data), []), - ]) - } + Error(Nil) -> #(default, []) } |> push_path([key]) let #(out, errors2) = next(out).function(lua, data) @@ -277,7 +303,13 @@ fn deser_int(lua, data: Value) -> Return(Int) { pub const raw: Deserializer(Value) = Deserializer(decode_raw) fn decode_raw(_lua, data: Value) -> Return(Value) { - #(data, []) + 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( diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 78c9563..427c8f5 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -3,10 +3,32 @@ -import(luerl_lib, [lua_error/2]). -include_lib("luerl/include/luerl.hrl"). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/2, get_table_keys/2, get_table_key/3, - get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_file/2, encode_table/2, encode_userdata/2, - eval_chunk/2, call_function/3, classify/1, unwrap_userdata/1, get_table_transform/4, - get_table_list_transform/4, userdata_exists/2, table_exists/2]). +-export([coerce/1, + coerce_nil/0, + coerce_userdata/1, + wrap_fun/1, + sandbox_fun/2, + get_table_keys/2, + get_table_key/3, + get_private/2, + set_table_keys/3, + load/2, + load_file/2, + eval/2, + eval_file/2, + encode_table/2, + encode_userdata/2, + eval_chunk/2, + call_function/3, + classify/1, + unwrap_userdata/1, + get_table_transform/4, + new_mval/1, + get_entry/2, + get_table_list_transform/4, + userdata_exists/2, + table_exists/2]). + %% helper to convert luerl return values to a format %% that is more suitable for use in Gleam code @@ -24,6 +46,7 @@ to_gleam(Value) -> {error, unknown_error} end. + classify(nil) -> <<"Nil">>; classify(Bool) when is_boolean(Bool) -> @@ -34,45 +57,63 @@ classify(N) when is_integer(N) -> <<"Int">>; classify(N) when is_float(N) -> <<"Float">>; -classify({tref,_}) -> +classify({tref, _}) -> <<"Table">>; -classify({usdref,_}) -> +classify({usdref, _}) -> <<"UserData">>; -classify({eref,_}) -> +classify({eref, _}) -> <<"Unknown">>; -classify({funref,_,_}) -> +classify({funref, _, _}) -> <<"Function">>; -classify({erl_func,_}) -> +classify({erl_func, _}) -> <<"Function">>; -classify({erl_mfa,_,_,_}) -> +classify({erl_mfa, _, _, _}) -> <<"Function">>; +classify(MVal) when is_tuple(MVal), + tuple_size(MVal) >= 1, + element(1, MVal) =:= multi_value -> + <<"MultiValue">>; classify(_) -> <<"Unknown">>. -get_table_transform(St, #tref{}=T, Acc, Func) +new_mval(MVal) when is_list(MVal) -> + NewMVal = [multi_value | MVal], + list_to_tuple(NewMVal). + +get_entry(MVal, Idx) when is_tuple(MVal), + element(1, MVal) =:= multi_value -> + Pos = Idx + 1, + case tuple_size(MVal) >= Pos andalso Pos >= 1 of + true -> {ok, element(Pos, MVal)}; + false -> {error, nil} + end. + +get_table_transform(St, #tref{} = T, Acc, Func) when is_function(Func, 3) -> - #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), + #table{a = Arr, d = Dict} = luerl_heap:get_table(T, St), Acc1 = ttdict:fold(Func, Acc, Dict), array:sparse_foldl(Func, Acc1, Arr). -get_table_list_transform(St, #tref{}=T, Acc, Func) - when is_function(Func, 2) -> - #table{a=Arr, d=Dict} = luerl_heap:get_table(T, St), + +get_table_list_transform(St, #tref{} = T, Acc, Func) + when is_function(Func, 2) -> + #table{a = Arr, d = Dict} = luerl_heap:get_table(T, St), case Dict of empty -> do_list_transform(Arr, Acc, Func); _ -> {error, nil} end. + do_list_transform(Arr, Acc, Func) -> N = array:size(Arr), try Wrapped = fun(Idx, Val, {Expected, UserAcc}) -> - case Idx of - Expected -> - {Expected - 1, Func(Val, UserAcc)}; - _ -> throw(hole) - end - end, + case Idx of + Expected -> + {Expected - 1, Func(Val, UserAcc)}; + _ -> throw(hole) + end + end, {End, Final} = array:sparse_foldr(Wrapped, {N - 1, Acc}, Arr), case {N, End} of @@ -84,6 +125,7 @@ do_list_transform(Arr, Acc, Func) -> throw:hole -> {error, nil} end. + %% TODO: Improve compiler errors handling and try to detect more errors map_error({error, [{_, luerl_parse, Errors} | _], _}) -> FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors), @@ -102,8 +144,8 @@ map_error({lua_error, {badarith, Operator, Args}, State}) -> FormattedOperator = unicode:characters_to_binary(atom_to_list(Operator)), FormattedArgs = lists:map(fun(V) -> - unicode:characters_to_binary( - io_lib:format("~p", [V])) + unicode:characters_to_binary( + io_lib:format("~p", [V])) end, Args), {lua_runtime_exception, {bad_arith, FormattedOperator, FormattedArgs}, State}; @@ -114,70 +156,86 @@ map_error({lua_error, Dynamic, State}) -> map_error(_) -> unknown_error. + coerce(X) -> X. + coerce_nil() -> nil. + coerce_userdata(X) -> {userdata, X}. + unwrap_userdata({userdata, Data}) -> {ok, Data}; unwrap_userdata(_) -> {error, nil}. + encode_table(State, Values) -> {Data, St} = luerl_heap:alloc_table(Values, State), {St, Data}. + encode_userdata(State, Values) -> {Data, St} = luerl_heap:alloc_userdata(Values, State), {St, Data}. + wrap_fun(Fun) -> NewFun = fun(Args, StateIn) -> - {Status, Result} = Fun(StateIn, Args), - case Status of - ok -> - {StateOut, Return} = Result, - {Return, StateOut}; - error -> - {StateOut, Msgs} = Result, - {error, map_error(lua_error({error_call, Msgs}, StateOut))} - end - end, - #erl_func{code=NewFun}. + {Status, Result} = Fun(StateIn, Args), + case Status of + ok -> + {StateOut, Return} = Result, + {Return, StateOut}; + error -> + {StateOut, Msgs} = Result, + {error, map_error(lua_error({error_call, Msgs}, StateOut))} + end + end, + #erl_func{code = NewFun}. + sandbox_fun(St, Msg) -> - Fun = fun(_, State) -> - {error, map_error(lua_error({error_call, [Msg]}, State))} - end, + Fun = fun(_, State) -> + {error, map_error(lua_error({error_call, [Msg]}, State))} + end, luerl:encode(Fun, St). -table_exists(State, Table) -> + +table_exists(State, Table) -> case luerl_heap:chk_table(Table, State) of ok -> true; error -> false end. -userdata_exists(State, Table) -> + +userdata_exists(State, Table) -> case luerl_heap:chk_userdata(Table, State) of ok -> true; error -> false end. + +get_table_key(_Lua, Args, _Key) when is_tuple(Args), + element(1, Args) =:= func_args -> + {error, nil}; + get_table_key(Lua, Table, Key) -> case luerl:get_table_key(Table, Key, Lua) of {ok, nil, _} -> - {error, key_not_found}; + {error, nil}; {ok, Value, Lua} -> {ok, {Lua, Value}}; - Other -> - to_gleam(Other) + _Other -> + {error, nil} end. + get_table_keys(Lua, Keys) -> case luerl:get_table_keys(Keys, Lua) of {ok, nil, _} -> @@ -188,31 +246,39 @@ get_table_keys(Lua, Keys) -> to_gleam(Other) end. + set_table_keys(Lua, Keys, Value) -> to_gleam(luerl:set_table_keys(Keys, Value, Lua)). + load(Lua, Code) -> to_gleam(luerl:load( - unicode:characters_to_list(Code), Lua)). + unicode:characters_to_list(Code), Lua)). + load_file(Lua, Path) -> to_gleam(luerl:loadfile( - unicode:characters_to_list(Path), Lua)). + unicode:characters_to_list(Path), Lua)). + eval(Lua, Code) -> to_gleam(luerl:do( - unicode:characters_to_list(Code), Lua)). + unicode:characters_to_list(Code), Lua)). + eval_chunk(Lua, Chunk) -> to_gleam(luerl:call_chunk(Chunk, Lua)). + eval_file(Lua, Path) -> to_gleam(luerl:dofile( - unicode:characters_to_list(Path), Lua)). + unicode:characters_to_list(Path), Lua)). + call_function(Lua, Fun, Args) -> to_gleam(luerl:call(Fun, Args, Lua)). + get_private(Lua, Key) -> try {ok, luerl:get_private(Key, Lua)} diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam index 6dbd2b6..7b7c0cc 100644 --- a/test/deserialize_test.gleam +++ b/test/deserialize_test.gleam @@ -380,3 +380,44 @@ pub fn nonexistent_userdata_test() { let assert Error([DeserializeError("UserData", "NonexistentUserData", [])]) = deser.run(glua.new(), ref, deser.userdata) } + +pub fn arg_ok_test() { + let args = [glua.string("Hello"), glua.int(42)] + let lua = glua.new() + let assert Ok(#("Hello", 42)) = + deser.run_multi(lua, args, { + use hello <- deser.item(1, deser.string) + use num <- deser.item(2, deser.int) + deser.success(#(hello, num)) + }) + Nil +} + +pub fn arg_error_test() { + let lua = glua.new() + let #(lua, table) = glua.table(lua, []) + let args = [table, glua.int(42)] + + let assert Error([err]) = + deser.run_multi(lua, args, { + use hello <- deser.item(1, { + use it <- deser.field(glua.string("nest"), { + use it <- deser.field(glua.string("another nest"), deser.int) + deser.success(it) + }) + deser.success(it) + }) + use num <- deser.item(2, deser.int) + deser.success(#(hello, num)) + }) + assert err + == DeserializeError( + "Field", + "Nothing", + [ + "", + "nest", + ] + |> list.map(glua.string), + ) +} diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 8de5d5d..f06b7f9 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -463,9 +463,9 @@ pub fn throw_error_test() { let add_func = glua.function(fn(lua, params) { let result = - deser.run_list(lua, params, { - use augend <- deser.field(glua.int(1), deser.number) - use addend <- deser.field(glua.int(2), deser.number) + deser.run_multi(lua, params, { + use augend <- deser.item(1, deser.number) + use addend <- deser.item(2, deser.number) deser.success(#(augend, addend)) }) |> result.map_error(bad_arg_error(lua, _)) @@ -474,13 +474,21 @@ pub fn throw_error_test() { }) let lua = glua.new() let assert Ok(#(lua, [result])) = - glua.call_function(lua, add_func, [glua.int(1), glua.int(4)]) - let assert Ok(5.0) = deser.run(lua, result, deser.number) + glua.call_function(lua, add_func, [glua.int(2), glua.int(4)]) + let assert Ok(6.0) = deser.run(lua, result, deser.number) let assert Error(glua.LuaRuntimeException( - exception: glua.ErrorCall(["Expected Field got Nothing at [2]"]), + exception: glua.ErrorCall([ + "Expected Field got Nothing at [\"\"]", + "Expected Field got Nothing at [\"\"]", + ]), state: _lua, - )) = - glua.call_function(lua, add_func, [ - glua.int(1), - ]) + )) = glua.call_function(lua, add_func, []) + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall([ + "Expected Number got String at [\"\"]", + "Expected Field got Nothing at [\"\"]", + ]), + state: _lua, + )) = glua.call_function(lua, add_func, [glua.string("1")]) } From ca2f2d4d9719056860870e478d7297c2b3627531 Mon Sep 17 00:00:00 2001 From: DynamicCake <128557765+DynamicCake@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:19:33 -1000 Subject: [PATCH 41/41] Fix utf8 encoding problem --- src/deser.gleam | 7 +++++++ src/glua_ffi.erl | 5 ++++- test/glua_test.gleam | 11 +++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/deser.gleam b/src/deser.gleam index 08cfa49..8aaf88a 100644 --- a/src/deser.gleam +++ b/src/deser.gleam @@ -3,6 +3,7 @@ //// 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 @@ -255,6 +256,12 @@ 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) { diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 427c8f5..1bd2d6a 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -52,7 +52,10 @@ classify(nil) -> classify(Bool) when is_boolean(Bool) -> <<"Bool">>; classify(Binary) when is_binary(Binary) -> - <<"String">>; + case gleam@bit_array:is_utf8(Binary) of + true -> <<"String">>; + false -> <<"ByteString">> + end; classify(N) when is_integer(N) -> <<"Int">>; classify(N) when is_float(N) -> diff --git a/test/glua_test.gleam b/test/glua_test.gleam index f06b7f9..8af4e50 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,4 +1,5 @@ import deser +import gleam/bit_array import gleam/dict import gleam/dynamic/decode import gleam/int @@ -492,3 +493,13 @@ pub fn throw_error_test() { state: _lua, )) = glua.call_function(lua, add_func, [glua.string("1")]) } + +pub fn non_utf8_test() { + let lua = glua.new() + let assert Ok(chars) = glua.get(lua, ["_G", "utf8", "charpattern"]) + let assert Error([deser.DeserializeError("String", "ByteString", [])]) = + deser.run(lua, chars, deser.string) + + let assert Ok(str) = deser.run(lua, chars, deser.byte_string) + assert !bit_array.is_utf8(str) +}