diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb2523..84876c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed +- `Lua.encode!/2` now maps Elixir `nil` to Lua `nil` instead of the string + `"nil"`. Previously top-level `nil` fell into the atom-encoding head and + became `"nil"`, which is truthy in Lua — silently inverting `if not value + then ...` checks and breaking `return nil, "reason"` error patterns. The + round trip `decode!(encode!(nil))` is now lossless, matching the existing + behaviour for `nil` inside tables and function result lists (#374). + ## [1.0.0-rc.3] - 2026-06-15 The fourth release candidate for `1.0.0`. It builds on rc.2 with a diff --git a/lib/lua.ex b/lib/lua.ex index eeca80d..97ae3a3 100644 --- a/lib/lua.ex +++ b/lib/lua.ex @@ -917,6 +917,14 @@ defmodule Lua do true """ @spec encode!(t(), term()) :: {term(), t()} + # Elixir `nil` maps to Lua `nil`, mirroring `decode!/2` so the round trip is + # lossless. Without this clause `nil` falls into the atom head below and + # encodes to the string `"nil"`, which is truthy in Lua and silently inverts + # `if not value then ...` checks (e.g. `return nil, "not found"` patterns). + def encode!(%__MODULE__{} = lua, nil) do + {nil, lua} + end + def encode!(%__MODULE__{} = lua, value) when is_atom(value) and not is_boolean(value) do {Atom.to_string(value), lua} end diff --git a/test/lua_test.exs b/test/lua_test.exs index bd097db..03090b1 100644 --- a/test/lua_test.exs +++ b/test/lua_test.exs @@ -722,6 +722,23 @@ defmodule LuaTest do assert [{1, 1}, {2, 2}] = lua |> Lua.decode!(ref) |> Enum.sort() end + test "it encodes nil to Lua nil rather than the string \"nil\"" do + lua = Lua.new() + + # Top-level nil must map to Lua nil, not the truthy string "nil" + assert {encoded, lua} = Lua.encode!(lua, nil) + assert encoded == nil + refute encoded == "nil" + + # Round trip is lossless and symmetric with decode!/2 + assert nil == Lua.decode!(lua, encoded) + + # The encoded value is falsy inside Lua, so `return nil, "..."` style + # error patterns and `if not value then` checks behave correctly. + lua = Lua.set!(lua, [:value], encoded) + assert {[true], _lua} = Lua.eval!(lua, "return not value") + end + test "it raises for values that cannot be encoded" do error = "Lua runtime error: Failed to encode {:foo, :bar}"