Skip to content

Decoded tables cannot round-trip through set!/encode! (FunctionClauseError on {key, value} pairs) #408

Description

@davydog187

The problem

Lua.eval!/3 with decode: true (the default) returns tables as lists of {key, value} tuples with string/number keys. But that shape cannot be passed back into the VM: Lua.VM.Value.encode/3 has no clause for a {binary, term} (or {number, term}) 2-tuple, so round-tripping any decoded table crashes with a bare FunctionClauseError:

lua = Lua.new()
{[decoded], lua} = Lua.eval!(lua, "return {x = 1}")
# decoded == [{"x", 1}]

Lua.set!(lua, [:t], decoded)
# ** (FunctionClauseError) no function clause matching in Lua.VM.Value.encode/3

The decode boundary and the encode boundary are asymmetric: what one produces, the other refuses — and refuses with an internal crash rather than a clear error.

Why it happens

encode/3's list clause checks keyword_list?/1. A decoded table like [{"x", 1}] has binary keys, so it falls into the positional branch, which Enum.with_index/2s the list and tries to encode each element — and the element {"x", 1} is a 2-tuple that matches no encode/3 clause.

Options

  1. Make decoded tables encodable (round-trip symmetry): add a clause/branch for lists of {k, v} pairs where k is a binary or number, encoding them as table entries. This makes decode → modify → set! work, which is a natural workflow.
  2. Refuse clearly: raise Lua.RuntimeException with an actionable message (mirroring the struct-refusal clause and the cyclic-tref clause from Fix unbounded recursion on cyclic tables at the eval boundary #407), telling the caller to convert to a map first (Map.new(decoded) works today).

Note that option 1 has an ambiguity to resolve: a list of 2-tuples could in principle be a Lua sequence of pair-tables. Today the positional branch already can't represent that (it crashes), so treating pair-lists as table entries is not a behavior regression — but it should be documented.

Related oddity

keyword_list?/1 treats any atom-keyed 2-tuple list as a keyword list, so Lua.set!(lua, [:w], [{:tref, 12}]) silently encodes as the table {tref = 12}. That is arguably correct keyword-list semantics, but combined with #407 (where decode can leave a literal {:tref, id} inside decoded output at a cycle), a decoded-then-re-encoded value can silently change meaning instead of erroring. Worth considering while touching this code.

Context

Found while reviewing #407, which made encode/3 refuse a bare {:tref, _} with a clear error. The pair-tuple gap predates that PR and exists on main independently of cyclic tables.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions