Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/lua.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions test/lua_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down