From 70147fbefc3f54e6a012eb44c3b37f207778e1b1 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sun, 23 Aug 2026 00:08:15 -0700 Subject: [PATCH 1/2] fix(vm): raise a clear error when encoding a decoded table Signed-off-by: Sai Asish Y --- lib/lua/vm/value.ex | 60 ++++++++++++++++++++++++++------------ test/lua/vm/value_test.exs | 10 +++++++ 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/lib/lua/vm/value.ex b/lib/lua/vm/value.ex index cd55225d..c6726164 100644 --- a/lib/lua/vm/value.ex +++ b/lib/lua/vm/value.ex @@ -277,25 +277,39 @@ defmodule Lua.VM.Value do end def encode(list, state, fun_wrapper) when is_list(list) do - if keyword_list?(list) do - {data, state} = - Enum.reduce(list, {%{}, state}, fn {k, v}, {data, state} -> - key = Atom.to_string(k) - {encoded_v, state} = encode(v, state, fun_wrapper) - {Map.put(data, key, encoded_v), state} - end) - - State.alloc_table(state, data) - else - {data, state} = - list - |> Enum.with_index(1) - |> Enum.reduce({%{}, state}, fn {v, idx}, {data, state} -> - {encoded_v, state} = encode(v, state, fun_wrapper) - {Map.put(data, idx, encoded_v), state} - end) - - State.alloc_table(state, data) + cond do + keyword_list?(list) -> + {data, state} = + Enum.reduce(list, {%{}, state}, fn {k, v}, {data, state} -> + key = Atom.to_string(k) + {encoded_v, state} = encode(v, state, fun_wrapper) + {Map.put(data, key, encoded_v), state} + end) + + State.alloc_table(state, data) + + # `decode/2` returns a table as a list of `{key, value}` pairs with + # binary/number keys. That shape has no positional encoding (each pair + # is a bare 2-tuple matching no `encode/3` clause), so it would crash + # with a raw `FunctionClauseError`. Refuse it with an actionable message + # instead, mirroring the struct and cyclic-tref clauses above. + decoded_pair_list?(list) -> + raise Lua.RuntimeException, + "cannot encode a list of {key, value} pairs into a Lua value. This is the " <> + "shape `decode/2` produces for a table, and it does not round-trip back " <> + "into the VM. Convert it to a map first (e.g. `Map.new/1`), which encodes " <> + "as a Lua table." + + true -> + {data, state} = + list + |> Enum.with_index(1) + |> Enum.reduce({%{}, state}, fn {v, idx}, {data, state} -> + {encoded_v, state} = encode(v, state, fun_wrapper) + {Map.put(data, idx, encoded_v), state} + end) + + State.alloc_table(state, data) end end @@ -328,6 +342,14 @@ defmodule Lua.VM.Value do defp keyword_list?([]), do: true defp keyword_list?(_), do: false + # A non-empty list where every element is a `{key, value}` pair with a binary + # or number key: exactly what `decode/2` returns for a Lua table. The empty + # list is already handled by `keyword_list?/1`, so this never matches `[]`. + defp decoded_pair_list?([{k, _v} | rest]) when is_binary(k) or is_number(k), do: decoded_pair_list?(rest) + + defp decoded_pair_list?([]), do: true + defp decoded_pair_list?(_), do: false + # --- Decoding (Lua VM → Elixir) --- @doc """ diff --git a/test/lua/vm/value_test.exs b/test/lua/vm/value_test.exs index 35da1881..0d163dcb 100644 --- a/test/lua/vm/value_test.exs +++ b/test/lua/vm/value_test.exs @@ -130,6 +130,16 @@ defmodule Lua.VM.ValueTest do inner_table = Map.fetch!(state.tables, inner_id) assert inner_table.data["x"] == 1 end + + test "refuses a decoded table (list of {key, value} pairs) with a clear error" do + assert_raise Lua.RuntimeException, ~r/list of \{key, value\} pairs/, fn -> + Value.encode([{"x", 1}, {"y", 2}], new_state()) + end + + assert_raise Lua.RuntimeException, ~r/list of \{key, value\} pairs/, fn -> + Value.encode([{1, "a"}, {2, "b"}], new_state()) + end + end end describe "encode_list/2" do From 508cd5c191b4040c25d48c329b7598d4d5c094f4 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Fri, 25 Sep 2026 09:53:24 -0700 Subject: [PATCH 2/2] use a heredoc for the decoded pair list error Signed-off-by: Sai Asish Y --- lib/lua/vm/value.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/lua/vm/value.ex b/lib/lua/vm/value.ex index c6726164..3f3227de 100644 --- a/lib/lua/vm/value.ex +++ b/lib/lua/vm/value.ex @@ -294,11 +294,12 @@ defmodule Lua.VM.Value do # with a raw `FunctionClauseError`. Refuse it with an actionable message # instead, mirroring the struct and cyclic-tref clauses above. decoded_pair_list?(list) -> - raise Lua.RuntimeException, - "cannot encode a list of {key, value} pairs into a Lua value. This is the " <> - "shape `decode/2` produces for a table, and it does not round-trip back " <> - "into the VM. Convert it to a map first (e.g. `Map.new/1`), which encodes " <> - "as a Lua table." + raise Lua.RuntimeException, """ + cannot encode a list of {key, value} pairs into a Lua value. This is the \ + shape `decode/2` produces for a table, and it does not round-trip back \ + into the VM. Convert it to a map first (e.g. `Map.new/1`), which encodes \ + as a Lua table.\ + """ true -> {data, state} =