Skip to content
Merged
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
108 changes: 96 additions & 12 deletions lib/lua/vm/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions lib/lua/vm/stdlib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions lib/lua/vm/stdlib/math.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/lua/vm/stdlib/math_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
78 changes: 78 additions & 0 deletions test/lua_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down