diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index d0c1227..79d102e 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -735,39 +735,80 @@ defmodule Lua.VM.Executor do # Bitwise operations defp do_execute([{:bitwise_and, dest, a, b} | rest], regs, upvalues, proto, state) do - result = Bitwise.band(trunc(elem(regs, a)), trunc(elem(regs, b))) + val_a = elem(regs, a) + val_b = elem(regs, b) + + {result, new_state} = + try_binary_metamethod("__band", val_a, val_b, state, fn -> + Bitwise.band(to_integer!(val_a), to_integer!(val_b)) + end) + regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, new_state) end defp do_execute([{:bitwise_or, dest, a, b} | rest], regs, upvalues, proto, state) do - result = Bitwise.bor(trunc(elem(regs, a)), trunc(elem(regs, b))) + val_a = elem(regs, a) + val_b = elem(regs, b) + + {result, new_state} = + try_binary_metamethod("__bor", val_a, val_b, state, fn -> + Bitwise.bor(to_integer!(val_a), to_integer!(val_b)) + end) + regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, new_state) end defp do_execute([{:bitwise_xor, dest, a, b} | rest], regs, upvalues, proto, state) do - result = Bitwise.bxor(trunc(elem(regs, a)), trunc(elem(regs, b))) + val_a = elem(regs, a) + val_b = elem(regs, b) + + {result, new_state} = + try_binary_metamethod("__bxor", val_a, val_b, state, fn -> + Bitwise.bxor(to_integer!(val_a), to_integer!(val_b)) + end) + regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, new_state) end defp do_execute([{:shift_left, dest, a, b} | rest], regs, upvalues, proto, state) do - result = Bitwise.bsl(trunc(elem(regs, a)), trunc(elem(regs, b))) + val_a = elem(regs, a) + val_b = elem(regs, b) + + {result, new_state} = + try_binary_metamethod("__shl", val_a, val_b, state, fn -> + lua_shift_left(to_integer!(val_a), to_integer!(val_b)) + end) + regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, new_state) end defp do_execute([{:shift_right, dest, a, b} | rest], regs, upvalues, proto, state) do - result = Bitwise.bsr(trunc(elem(regs, a)), trunc(elem(regs, b))) + val_a = elem(regs, a) + val_b = elem(regs, b) + + {result, new_state} = + try_binary_metamethod("__shr", val_a, val_b, state, fn -> + lua_shift_right(to_integer!(val_a), to_integer!(val_b)) + end) + regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, new_state) end defp do_execute([{:bitwise_not, dest, source} | rest], regs, upvalues, proto, state) do - result = Bitwise.bnot(trunc(elem(regs, source))) + val = elem(regs, source) + + {result, new_state} = + try_unary_metamethod("__bnot", val, state, fn -> + Bitwise.bnot(to_integer!(val)) + end) + regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, new_state) end # Comparison operations @@ -1585,6 +1626,49 @@ defmodule Lua.VM.Executor do defp to_number(v), do: {:error, v} + # Convert value to integer for bitwise operations (with string coercion) + defp to_integer!(v) when is_integer(v), do: v + defp to_integer!(v) when is_float(v), do: trunc(v) + + defp to_integer!(v) when is_binary(v) do + case Value.parse_number(v) do + nil -> + raise TypeError, + value: "attempt to perform bitwise operation on a string value", + error_kind: :bitwise_on_non_integer, + value_type: :string + + n -> + trunc(n) + end + end + + defp to_integer!(v) do + raise TypeError, + value: "attempt to perform bitwise operation on a #{Value.type_name(v)} value", + error_kind: :bitwise_on_non_integer, + value_type: value_type(v) + end + + # Lua 5.3 shift semantics: negative shift reverses direction, shift >= 64 yields 0 + defp lua_shift_left(_val, shift) when shift >= 64, do: 0 + defp lua_shift_left(_val, shift) when shift <= -64, do: 0 + defp lua_shift_left(val, shift) when shift < 0, do: lua_shift_right(val, -shift) + + defp lua_shift_left(val, shift) do + Bitwise.band(Bitwise.bsl(val, shift), 0xFFFFFFFFFFFFFFFF) + end + + defp lua_shift_right(_val, shift) when shift >= 64, do: 0 + defp lua_shift_right(_val, shift) when shift <= -64, do: 0 + defp lua_shift_right(val, shift) when shift < 0, do: lua_shift_left(val, -shift) + + defp lua_shift_right(val, shift) do + # Unsigned right shift - mask to 64-bit first + unsigned_val = Bitwise.band(val, 0xFFFFFFFFFFFFFFFF) + Bitwise.bsr(unsigned_val, shift) + end + # Helper to determine Lua type from Elixir value defp value_type(nil), do: nil defp value_type(v) when is_boolean(v), do: :boolean diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 2aaae74..1597383 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -40,10 +40,14 @@ defmodule Lua.VM.Stdlib do |> State.register_function("select", &lua_select/2) |> State.register_function("load", &lua_load/2) |> State.register_function("require", &lua_require/2) + |> State.register_function("collectgarbage", &lua_collectgarbage/2) + |> State.register_function("dofile", &lua_dofile/2) + |> State.set_global("_VERSION", "Lua 5.3") |> install_package_table() |> Lua.VM.Stdlib.String.install() |> Lua.VM.Stdlib.Math.install() |> Lua.VM.Stdlib.Table.install() + |> install_unpack_alias() |> install_global_g() end @@ -626,4 +630,43 @@ defmodule Lua.VM.Stdlib do {Value.to_string(value), state} end end + + # collectgarbage stub — no-op accepting all standard modes + defp lua_collectgarbage(args, state) do + mode = List.first(args) || "collect" + + case mode do + "count" -> + # Return plausible memory usage in KB and remainder bytes + {[0.0, 0], state} + + "isrunning" -> + {[true], state} + + _ -> + # "collect", "stop", "restart", "step", "setpause", "setstepmul", "generational", "incremental" + {[0], state} + end + end + + # dofile stub — not supported in embedded mode + defp lua_dofile(_args, _state) do + raise RuntimeError, value: "dofile not supported in embedded mode" + end + + # Install global 'unpack' as alias for table.unpack + defp install_unpack_alias(state) do + case Map.get(state.globals, "table") do + {:tref, id} -> + table = Map.fetch!(state.tables, id) + + case Map.get(table.data, "unpack") do + nil -> state + unpack_fn -> State.set_global(state, "unpack", unpack_fn) + end + + _ -> + state + end + end end diff --git a/lib/lua/vm/stdlib/math.ex b/lib/lua/vm/stdlib/math.ex index 856c4fc..91a962a 100644 --- a/lib/lua/vm/stdlib/math.ex +++ b/lib/lua/vm/stdlib/math.ex @@ -150,7 +150,7 @@ defmodule Lua.VM.Stdlib.Math do # math.ceil(x) defp math_ceil([x], state) when is_number(x) do - {[Float.ceil(x / 1)], state} + {[trunc(Float.ceil(x / 1))], state} end defp math_ceil([x | _], _state) do @@ -201,7 +201,7 @@ defmodule Lua.VM.Stdlib.Math do # math.floor(x) defp math_floor([x], state) when is_number(x) do - {[Float.floor(x / 1)], state} + {[trunc(Float.floor(x / 1))], state} end defp math_floor([x | _], _state) do diff --git a/test/lua/vm/stdlib/math_test.exs b/test/lua/vm/stdlib/math_test.exs index 935ce71..2be82bf 100644 --- a/test/lua/vm/stdlib/math_test.exs +++ b/test/lua/vm/stdlib/math_test.exs @@ -23,7 +23,7 @@ defmodule Lua.VM.Stdlib.MathTest do assert {:ok, proto} = Compiler.compile(ast, source: "test.lua") state = Stdlib.install(State.new()) - assert {:ok, [4.0, -3.0, 5.0], _state} = VM.execute(proto, state) + assert {:ok, [4, -3, 5], _state} = VM.execute(proto, state) end test "math.floor rounds down" do @@ -32,7 +32,7 @@ defmodule Lua.VM.Stdlib.MathTest do assert {:ok, proto} = Compiler.compile(ast, source: "test.lua") state = Stdlib.install(State.new()) - assert {:ok, [3.0, -4.0, 5.0], _state} = VM.execute(proto, state) + assert {:ok, [3, -4, 5], _state} = VM.execute(proto, state) end test "math.max returns maximum" do diff --git a/test/lua_test.exs b/test/lua_test.exs index 8c22ef8..d120fcb 100644 --- a/test/lua_test.exs +++ b/test/lua_test.exs @@ -1766,6 +1766,84 @@ defmodule LuaTest do end end + describe "collectgarbage stub" do + setup do + %{lua: Lua.new(sandboxed: [])} + end + + test "collectgarbage returns without error", %{lua: lua} do + code = """ + collectgarbage() + collectgarbage("collect") + collectgarbage("stop") + return true + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "collectgarbage 'count' returns a number", %{lua: lua} do + code = """ + local k = collectgarbage("count") + return type(k) + """ + + assert {["number"], _} = Lua.eval!(lua, code) + end + end + + describe "global stubs and constants" do + setup do + %{lua: Lua.new(sandboxed: [])} + end + + test "_VERSION is Lua 5.3", %{lua: lua} do + assert {["Lua 5.3"], _} = Lua.eval!(lua, "return _VERSION") + end + + test "unpack works as global alias", %{lua: lua} do + code = "return unpack({10, 20, 30})" + assert {[10, 20, 30], _} = Lua.eval!(lua, code) + end + end + + describe "bitwise operation fixes" do + setup do + %{lua: Lua.new(sandboxed: [])} + end + + test "string coercion for bitwise ops", %{lua: lua} do + code = ~S[return "0xff" | 0] + assert {[255], _} = Lua.eval!(lua, code) + end + + test "shift edge cases", %{lua: lua} do + code = "return 1 << 64, 1 >> 64, 1 << -1" + assert {[0, 0, 0], _} = Lua.eval!(lua, code) + end + + test "negative shift reverses direction", %{lua: lua} do + code = "return 8 >> -2" + assert {[32], _} = Lua.eval!(lua, code) + end + end + + describe "math.floor and math.ceil return integers" do + setup do + %{lua: Lua.new(sandboxed: [])} + end + + test "math.floor returns integer", %{lua: lua} do + code = "return math.type(math.floor(3.5))" + assert {["integer"], _} = Lua.eval!(lua, code) + end + + test "math.ceil returns integer", %{lua: lua} do + code = "return math.type(math.ceil(3.5))" + assert {["integer"], _} = Lua.eval!(lua, code) + end + end + defp test_file(name) do Path.join(["test", "fixtures", name]) end