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
17 changes: 11 additions & 6 deletions lib/lua.ex
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ defmodule Lua do
if Util.encoded?(value) do
{value, lua.state}
else
Value.encode(value, lua.state, &wrap_callback(&1, function_name, scope))
encode_value!(value, lua.state, &wrap_callback(&1, function_name, scope))
end

state = do_set_nested(state, keys, encoded)
Expand Down Expand Up @@ -999,6 +999,10 @@ defmodule Lua do
{nil, lua}
end

def encode!(%__MODULE__{} = lua, :nan) do
{:nan, lua}
end

def encode!(%__MODULE__{} = lua, value) when is_atom(value) and not is_boolean(value) do
{Atom.to_string(value), lua}
end
Expand All @@ -1009,14 +1013,15 @@ defmodule Lua do
if Util.encoded?(value) do
{value, lua}
else
{encoded, state} = Value.encode(value, state, &wrap_callback(&1, :anonymous, []))
{encoded, state} = encode_value!(value, state, &wrap_callback(&1, :anonymous, []))
{encoded, %{lua | state: state}}
end
rescue
_e in [ArgumentError] ->
reraise Lua.RuntimeException, "Failed to encode #{inspect(value)}", __STACKTRACE__
end

_e in [FunctionClauseError] ->
defp encode_value!(value, state, fun_wrapper) do
Value.encode(value, state, fun_wrapper)
rescue
_e in [ArgumentError, FunctionClauseError] ->
reraise Lua.RuntimeException, "Failed to encode #{inspect(value)}", __STACKTRACE__
end

Expand Down
1 change: 1 addition & 0 deletions lib/lua/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Lua.Util do
def encoded?(true), do: true
def encoded?(binary) when is_binary(binary), do: true
def encoded?(number) when is_number(number), do: true
def encoded?(:nan), do: true
def encoded?({:tref, _}), do: true
def encoded?({:lua_closure, _, _}), do: true
def encoded?({:compiled_closure, _, _}), do: true
Expand Down
4 changes: 4 additions & 0 deletions lib/lua/vm/error_formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ defmodule Lua.VM.ErrorFormatter do
"Relational operators (< <= > >=) only compare two numbers or two strings. Convert one operand so both sides share a type."
end

defp build_suggestion(:type_error, :length_non_table, value_type) do
"The length operator (#) only works on strings and tables. Make sure the value is a string or table, not a #{format_type_name(value_type)}."
end

defp build_suggestion(:type_error, :length_not_integer, _value_type) do
"The length operation returned a non-integer value. If you defined a __len metamethod, make sure it returns an integer."
end
Expand Down
22 changes: 15 additions & 7 deletions lib/lua/vm/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,9 @@ defmodule Lua.VM.Executor do
raise_index_type_error(value, nil, proto.source, name_hint, state)
end

# The `_proto` parameter is unused today because `try_unary_metamethod`
# doesn't thread a `source` through; B5d-v2 will route `__len` errors
# back to `proto.source` (matching the other bridges' attribution),
# so the parameter stays in the signature for forward-compat.
@doc false
@spec dispatcher_length(term(), State.t(), term()) :: {term(), State.t()}
def dispatcher_length(value, state, _proto) do
def dispatcher_length(value, state, proto) do
try_unary_metamethod("__len", value, state, fn ->
case value do
{:tref, id} ->
Expand All @@ -487,11 +483,23 @@ defmodule Lua.VM.Executor do
length(v)

_ ->
0
raise_length_type_error(value, nil, proto.source, state)
end
end)
end

# Lua 5.3 §3.4.7: `#` is defined for strings and tables only; any other
# operand without a `__len` metamethod is a type error, never 0.
defp raise_length_type_error(value, line, source, state) do
raise TypeError,
value: "attempt to get length of a #{Value.type_name(value)} value",
line: line,
source: source,
error_kind: :length_non_table,
value_type: value_type(value),
state: state
end

@doc false
@spec dispatcher_coerce_numeric_for_controls(term(), term(), term(), State.t()) ::
{number(), number(), number()}
Expand Down Expand Up @@ -2597,7 +2605,7 @@ defmodule Lua.VM.Executor do
length(v)

_ ->
0
raise_length_type_error(value, line, proto.source, state)
end
end)

Expand Down
4 changes: 4 additions & 0 deletions lib/lua/vm/stdlib/math.ex
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,10 @@ defmodule Lua.VM.Stdlib.Math do
{["float"], state}
end

defp math_type([:nan], state) do
{["float"], state}
end

defp math_type([_x], state) do
{[nil], state}
end
Expand Down
13 changes: 11 additions & 2 deletions lib/lua/vm/value.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Lua.VM.Value do
def type_name(v) when is_boolean(v), do: "boolean"
def type_name(v) when is_integer(v), do: "number"
def type_name(v) when is_float(v), do: "number"
def type_name(:nan), do: "number"
def type_name(v) when is_binary(v), do: "string"
def type_name({:tref, _}), do: "table"
def type_name({:lua_closure, _, _}), do: "function"
Expand Down Expand Up @@ -54,6 +55,7 @@ defmodule Lua.VM.Value do
end
end

def to_string(:nan), do: "nan"
def to_string(v) when is_binary(v), do: v

def to_string({:tref, id}), do: "table: 0x#{address(id)}"
Expand Down Expand Up @@ -232,6 +234,7 @@ defmodule Lua.VM.Value do
def encode(nil, state, _fun_wrapper), do: {nil, state}
def encode(value, state, _fun_wrapper) when is_boolean(value), do: {value, state}
def encode(value, state, _fun_wrapper) when is_number(value), do: {value, state}
def encode(:nan, state, _fun_wrapper), do: {:nan, state}
def encode(value, state, _fun_wrapper) when is_binary(value), do: {value, state}
def encode(value, state, _fun_wrapper) when is_atom(value), do: {Atom.to_string(value), state}

Expand Down Expand Up @@ -268,7 +271,7 @@ defmodule Lua.VM.Value do
def encode(map, state, fun_wrapper) when is_map(map) do
{data, state} =
Enum.reduce(map, {%{}, state}, fn {k, v}, {data, state} ->
key = if is_atom(k), do: Atom.to_string(k), else: k
key = table_key(k)
{encoded_v, state} = encode(v, state, fun_wrapper)
{Map.put(data, key, encoded_v), state}
end)
Expand All @@ -280,7 +283,7 @@ defmodule Lua.VM.Value do
if keyword_list?(list) do
{data, state} =
Enum.reduce(list, {%{}, state}, fn {k, v}, {data, state} ->
key = Atom.to_string(k)
key = table_key(k)
{encoded_v, state} = encode(v, state, fun_wrapper)
{Map.put(data, key, encoded_v), state}
end)
Expand Down Expand Up @@ -324,6 +327,12 @@ defmodule Lua.VM.Value do
{Enum.reverse(reversed), state}
end

# Atom keys become Lua strings, except `true`/`false`: those are Lua
# booleans, and a boolean key must stay a boolean key.
defp table_key(key) when is_boolean(key), do: key
defp table_key(key) when is_atom(key), do: Atom.to_string(key)
defp table_key(key), do: key

defp keyword_list?([{k, _v} | rest]) when is_atom(k), do: keyword_list?(rest)
defp keyword_list?([]), do: true
defp keyword_list?(_), do: false
Expand Down
1 change: 1 addition & 0 deletions test/lua/util_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Lua.UtilTest do
assert Lua.Util.encoded?(0)
assert Lua.Util.encoded?(Enum.random(1..1000))
assert Lua.Util.encoded?(:rand.uniform())
assert Lua.Util.encoded?(:nan)

# tref
{{:tref, _} = table, _lua} = Lua.encode!(Lua.new(), %{a: 1, b: 2})
Expand Down
14 changes: 14 additions & 0 deletions test/lua/vm/float_div_zero_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ defmodule Lua.VM.FloatDivZeroTest do
test "NaN compared to a number is unequal" do
assert run!("return 0/0 == 1") == [false]
end

test "NaN is a float number" do
assert run!("return type(0/0), math.type(0/0), tostring(0/0)") == ["number", "float", "nan"]
end

test "NaN crosses the host boundary as :nan and back" do
{[:nan], lua} = Lua.eval!(Lua.new(), "return 0/0")

assert {:nan, lua} = Lua.encode!(lua, :nan)
assert Lua.decode!(lua, :nan) == :nan

lua = Lua.set!(lua, [:nan], :nan)
assert {[true, "number"], _lua} = Lua.eval!(lua, "return nan ~= nan, type(nan)")
end
end

describe "non-zero divisors still divide normally" do
Expand Down
34 changes: 34 additions & 0 deletions test/lua/vm/length_operator_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Lua.VM.LengthOperatorTest do
@moduledoc """
Pins Lua 5.3 §3.4.7: `#` applies to strings and tables (or any value with a
`__len` metamethod); every other operand is a type error, never 0.
"""

use ExUnit.Case, async: true

defp run!(code) do
{results, _lua} = Lua.eval!(Lua.new(), code)
results
end

test "strings and tables have a length" do
assert run!(~S(return #"abc", #{1, 2, 3}, #{})) == [3, 3, 0]
end

test "__len is honoured" do
assert run!("return #setmetatable({}, {__len = function() return 7 end})") == [7]
end

for {literal, type} <- [{"nil", "nil"}, {"true", "boolean"}, {"5", "number"}, {"print", "function"}] do
test "# on a #{type} value raises" do
assert_raise Lua.RuntimeException, ~r/attempt to get length of a #{unquote(type)} value/, fn ->
Lua.eval!(Lua.new(), "return #(#{unquote(literal)})")
end
end
end

test "the error is catchable with pcall" do
assert [false, message] = run!("return pcall(function() return #nil end)")
assert message =~ "attempt to get length of a nil value"
end
end
23 changes: 23 additions & 0 deletions test/lua/vm/value_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ defmodule Lua.VM.ValueTest do
assert {"hello", _state} = Value.encode(:hello, new_state())
assert {"Elixir.MyApp", _state} = Value.encode(MyApp, new_state())
end

test "the nan sentinel passes through as a number" do
assert {:nan, _state} = Value.encode(:nan, new_state())
assert Value.decode(:nan, new_state()) == :nan
assert Value.type_name(:nan) == "number"
end
end

describe "encode/2 functions" do
Expand Down Expand Up @@ -80,6 +86,15 @@ defmodule Lua.VM.ValueTest do
assert table.data["status"] == "married"
end

test "keeps boolean keys as booleans" do
{{:tref, id}, state} = Value.encode(%{true => "yes", false => "no"}, new_state())

table = Map.fetch!(state.tables, id)
assert table.data[true] == "yes"
assert table.data[false] == "no"
refute Map.has_key?(table.data, "true")
end

test "encodes empty map" do
{{:tref, id}, state} = Value.encode(%{}, new_state())

Expand Down Expand Up @@ -122,6 +137,14 @@ defmodule Lua.VM.ValueTest do
assert table.data["age"] == 25
end

test "keeps boolean keys of a keyword-shaped list as booleans" do
{{:tref, id}, state} = Value.encode([{true, "yes"}, {false, "no"}], new_state())

table = Map.fetch!(state.tables, id)
assert table.data[true] == "yes"
assert table.data[false] == "no"
end

test "recursively encodes list elements" do
{{:tref, id}, state} = Value.encode([%{x: 1}], new_state())

Expand Down
13 changes: 13 additions & 0 deletions test/lua_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,13 @@ defmodule LuaTest do
assert {[true], _lua} = Lua.eval!(lua, "return not value")
end

test "boolean table keys survive a round trip" do
lua = Lua.set!(Lua.new(), [:flags], %{true => "yes", false => "no"})

assert {["yes", "no"], lua} = Lua.eval!(lua, "return flags[true], flags[false]")
assert Enum.sort(Lua.get!(lua, [:flags])) == [{false, "no"}, {true, "yes"}]
end

test "it raises for values that cannot be encoded" do
error = "Lua runtime error: Failed to encode {:foo, :bar}"

Expand All @@ -718,6 +725,12 @@ defmodule LuaTest do
end
end

test "Lua.set!/3 raises the same error for values that cannot be encoded" do
assert_raise Lua.RuntimeException, ~r/Failed to encode #PID/, fn ->
Lua.set!(Lua.new(), [:pid], self())
end
end

test "it raises for values that cannot be decoded" do
error = "Lua runtime error: Failed to decode {}"

Expand Down