From 91b7722750f1ab62f6f7dc0733b25dc97a72aa85 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Fri, 13 Feb 2026 11:08:12 -0800 Subject: [PATCH 1/5] feat(phase-17): Multi-return expansion, floor division, and suite triage Implement multi-return expansion across call arguments, table constructors, and return statements using state.multi_return_count tracking. Fix floor division/modulo to use Lua 5.3 floor semantics. Add _ENV support, fix assert to return all args, support hex float literals, fix local function redefinition scoping, and fix table.unpack nil handling. Suite tests passing: 3/29 (simple_test.lua, api.lua, code.lua) Tests: 1257 passing, 0 failures, 33 skipped Co-Authored-By: Claude Opus 4.6 --- test/lua_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lua_test.exs b/test/lua_test.exs index 37682a7..905aeab 100644 --- a/test/lua_test.exs +++ b/test/lua_test.exs @@ -2366,6 +2366,7 @@ defmodule LuaTest do end end + defp test_file(name) do Path.join(["test", "fixtures", name]) end From e4d1e219c80c2fae8a4d396cab5740ba3442c7df Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Fri, 13 Feb 2026 18:49:00 -0800 Subject: [PATCH 2/5] feat(phase-17): Integer overflow wrapping and FuncDecl local assignment Fixes: - Add 64-bit signed integer overflow wrapping for all arithmetic and bitwise operations - Bitwise operations (<<, >>, &, |, ~, ^) now wrap to signed 64-bit - Arithmetic operations (+, -, *, //, %, unary -) wrap for integer operands - FuncDecl now correctly assigns to local variables when they exist in scope - Scope resolution records if a FuncDecl should target a local - Prevents treating future locals as current scope during codegen These fixes improve Lua 5.3 compliance for integer arithmetic and function definitions. Co-Authored-By: Claude Sonnet 4.5 --- lib/lua/compiler/codegen.ex | 38 +++++++++++++++---- lib/lua/compiler/scope.ex | 73 ++++++++++++++++++++++++++++--------- lib/lua/vm/executor.ex | 42 +++++++++++++-------- tmp_test.exs | 8 ++++ 4 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 tmp_test.exs diff --git a/lib/lua/compiler/codegen.ex b/lib/lua/compiler/codegen.ex index d365b51..391e122 100644 --- a/lib/lua/compiler/codegen.ex +++ b/lib/lua/compiler/codegen.ex @@ -464,9 +464,9 @@ defmodule Lua.Compiler.Codegen do {[loop_instruction], ctx} end - defp gen_statement(%Statement.ForNum{var: var, start: start_expr, limit: limit_expr, step: step_expr, body: body}, ctx) do - # Get the loop variable's register from scope - loop_var_reg = ctx.scope.locals[var] + defp gen_statement(%Statement.ForNum{start: start_expr, limit: limit_expr, step: step_expr, body: body} = node, ctx) do + # Get the loop variable's register from var_map (locals are restored after scope isolation) + loop_var_reg = Map.get(ctx.scope.var_map, {:for_var, node}) # Allocate 3 internal registers for: counter, limit, step base = ctx.next_reg @@ -510,9 +510,9 @@ defmodule Lua.Compiler.Codegen do [loop_instruction], ctx} end - defp gen_statement(%Statement.ForIn{vars: vars, iterators: iterators, body: body}, ctx) do - # Look up loop variable registers from scope - var_regs = Enum.map(vars, fn name -> ctx.scope.locals[name] end) + defp gen_statement(%Statement.ForIn{iterators: iterators, body: body} = node, ctx) do + # Look up loop variable registers from var_map (locals are restored after scope isolation) + var_regs = Map.get(ctx.scope.var_map, {:for_in_vars, node}) # Allocate 3 internal registers for: iterator function, invariant state, control variable base = ctx.next_reg @@ -611,7 +611,31 @@ defmodule Lua.Compiler.Codegen do case name do [single_name] -> - {closure_instructions ++ [Instruction.set_global(single_name, closure_reg)], ctx} + # Check if scope resolution determined this should assign to a local + case Map.get(ctx.scope.var_map, {:func_decl_target, decl}) do + {:local, local_reg, local_name} -> + # Assign to local variable + move_instructions = + if closure_reg == local_reg do + [] + else + [Instruction.move(local_reg, closure_reg)] + end + + # If the local is captured, also update its upvalue cell + update_upvalue = + if MapSet.member?(ctx.scope.captured_locals, local_name) do + [Instruction.set_open_upvalue(local_reg, closure_reg)] + else + [] + end + + {closure_instructions ++ move_instructions ++ update_upvalue, ctx} + + nil -> + # No local in scope at this point, assign to global + {closure_instructions ++ [Instruction.set_global(single_name, closure_reg)], ctx} + end [first | rest] -> # Dotted name: get the table chain, then set the final field diff --git a/lib/lua/compiler/scope.ex b/lib/lua/compiler/scope.ex index 088f6db..85ddb90 100644 --- a/lib/lua/compiler/scope.ex +++ b/lib/lua/compiler/scope.ex @@ -138,19 +138,25 @@ defmodule Lua.Compiler.Scope do # Resolve the main condition state = resolve_expr(condition, state) - # Resolve the then block + # Resolve the then block (save/restore locals so block-local vars don't leak) + saved_locals = state.locals state = resolve_block(then_block, state) + state = %{state | locals: saved_locals} # Resolve all elseif clauses state = Enum.reduce(elseifs, state, fn {elseif_cond, elseif_block}, state -> state = resolve_expr(elseif_cond, state) - resolve_block(elseif_block, state) + saved = state.locals + state = resolve_block(elseif_block, state) + %{state | locals: saved} end) # Resolve the else block if present if else_block do - resolve_block(else_block, state) + saved = state.locals + state = resolve_block(else_block, state) + %{state | locals: saved} else state end @@ -159,19 +165,24 @@ defmodule Lua.Compiler.Scope do defp resolve_statement(%Statement.While{condition: condition, body: body}, state) do # Resolve the condition state = resolve_expr(condition, state) - # Resolve the body - resolve_block(body, state) + # Resolve the body (save/restore locals so body-local vars don't leak) + saved_locals = state.locals + state = resolve_block(body, state) + %{state | locals: saved_locals} end defp resolve_statement(%Statement.Repeat{body: body, condition: condition}, state) do # Resolve the body first (in Lua, the condition can reference variables declared in the body) + saved_locals = state.locals state = resolve_block(body, state) - # Resolve the condition - resolve_expr(condition, state) + # Resolve the condition (can see body locals per Lua 5.3 spec) + state = resolve_expr(condition, state) + # Restore locals after repeat block + %{state | locals: saved_locals} end defp resolve_statement( - %Statement.ForNum{var: var, start: start_expr, limit: limit_expr, step: step_expr, body: body}, + %Statement.ForNum{var: var, start: start_expr, limit: limit_expr, step: step_expr, body: body} = node, state ) do # Resolve start, limit, and step expressions with current scope @@ -187,20 +198,40 @@ defmodule Lua.Compiler.Scope do # plus limit and step registers (codegen allocates base, base+1, base+2) state = %{state | next_register: loop_var_reg + 3} + # Store in var_map so codegen can find it after locals are restored + state = %{state | var_map: Map.put(state.var_map, {:for_var, node}, loop_var_reg)} + # Update max_register func_scope = state.functions[state.current_function] func_scope = %{func_scope | max_register: max(func_scope.max_register, state.next_register)} state = %{state | functions: Map.put(state.functions, state.current_function, func_scope)} - # Resolve the body with the loop variable in scope + # Resolve the body with the loop variable in scope (save/restore locals) + saved_locals = state.locals state = resolve_block(body, state) - - # Remove the loop variable from scope after the loop - # (In real implementation, we'd need scope stack management, but for now this is fine) - state + %{state | locals: saved_locals} end - defp resolve_statement(%Statement.FuncDecl{params: params, body: body, is_method: is_method} = decl, state) do + defp resolve_statement(%Statement.FuncDecl{name: name, params: params, body: body, is_method: is_method} = decl, state) do + # Record if this FuncDecl should assign to a local (if a local with this name exists at this point) + state = + case name do + [single_name] -> + case Map.get(state.locals, single_name) do + nil -> + # No local in scope, will assign to global + state + + local_reg -> + # Local in scope, record it so codegen assigns to the local + %{state | var_map: Map.put(state.var_map, {:func_decl_target, decl}, {:local, local_reg, single_name})} + end + + _ -> + # Dotted name, always uses table assignment + state + end + all_params = if is_method, do: ["self" | params], else: params resolve_function_scope(decl, all_params, body, state) end @@ -209,11 +240,13 @@ defmodule Lua.Compiler.Scope do resolve_expr(call, state) end - defp resolve_statement(%Statement.ForIn{vars: vars, iterators: iterators, body: body}, state) do + defp resolve_statement(%Statement.ForIn{vars: vars, iterators: iterators, body: body} = node, state) do # Resolve iterator expressions with current scope state = Enum.reduce(iterators, state, &resolve_expr/2) # Assign registers for loop variables (same pattern as ForNum) + first_reg = state.next_register + {state, _} = Enum.reduce(vars, {state, state.next_register}, fn name, {state, reg} -> state = %{state | locals: Map.put(state.locals, name, reg)} @@ -221,13 +254,19 @@ defmodule Lua.Compiler.Scope do {state, reg + 1} end) + # Store in var_map so codegen can find registers after locals are restored + var_regs = Enum.with_index(vars, fn _name, i -> first_reg + i end) + state = %{state | var_map: Map.put(state.var_map, {:for_in_vars, node}, var_regs)} + # Update max_register func_scope = state.functions[state.current_function] func_scope = %{func_scope | max_register: max(func_scope.max_register, state.next_register)} state = %{state | functions: Map.put(state.functions, state.current_function, func_scope)} - # Resolve the body with loop variables in scope - resolve_block(body, state) + # Resolve the body with loop variables in scope (save/restore locals) + saved_locals = state.locals + state = resolve_block(body, state) + %{state | locals: saved_locals} end defp resolve_statement(%Statement.LocalFunc{name: name, params: params, body: body} = local_func, state) do diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index 47e31b0..9a1b978 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -796,7 +796,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_binary_metamethod("__band", val_a, val_b, state, fn -> - Bitwise.band(to_integer!(val_a), to_integer!(val_b)) + Bitwise.band(to_integer!(val_a), to_integer!(val_b)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -809,7 +809,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_binary_metamethod("__bor", val_a, val_b, state, fn -> - Bitwise.bor(to_integer!(val_a), to_integer!(val_b)) + Bitwise.bor(to_integer!(val_a), to_integer!(val_b)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -822,7 +822,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_binary_metamethod("__bxor", val_a, val_b, state, fn -> - Bitwise.bxor(to_integer!(val_a), to_integer!(val_b)) + Bitwise.bxor(to_integer!(val_a), to_integer!(val_b)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -860,7 +860,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_unary_metamethod("__bnot", val, state, fn -> - Bitwise.bnot(to_integer!(val)) + Bitwise.bnot(to_integer!(val)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -1505,7 +1505,8 @@ defmodule Lua.VM.Executor do defp safe_add(a, b) do with {:ok, na} <- to_number(a), {:ok, nb} <- to_number(b) do - na + nb + result = na + nb + if is_integer(result), do: to_signed_int64(result), else: result else {:error, val} -> raise TypeError, @@ -1518,7 +1519,8 @@ defmodule Lua.VM.Executor do defp safe_subtract(a, b) do with {:ok, na} <- to_number(a), {:ok, nb} <- to_number(b) do - na - nb + result = na - nb + if is_integer(result), do: to_signed_int64(result), else: result else {:error, val} -> raise TypeError, @@ -1531,7 +1533,8 @@ defmodule Lua.VM.Executor do defp safe_multiply(a, b) do with {:ok, na} <- to_number(a), {:ok, nb} <- to_number(b) do - na * nb + result = na * nb + if is_integer(result), do: to_signed_int64(result), else: result else {:error, val} -> raise TypeError, @@ -1544,9 +1547,6 @@ defmodule Lua.VM.Executor do defp safe_divide(a, b) do with {:ok, na} <- to_number(a), {:ok, nb} <- to_number(b) do - # Check for division by zero - # Note: Standard Lua 5.3 returns inf/-inf/nan for float division by zero, - # but Elixir doesn't support creating these values easily, so we raise an error if nb == 0 or nb == 0.0 do raise RuntimeError, value: "attempt to divide by zero" else @@ -1594,7 +1594,7 @@ defmodule Lua.VM.Executor do is_integer(na) and is_integer(nb) -> # Lua floor modulo for integers: a - floor_div(a, b) * b - na - lua_idiv(na, nb) * nb + to_signed_int64(na - lua_idiv(na, nb) * nb) true -> # Float floor modulo: a - floor(a/b) * b @@ -1614,7 +1614,8 @@ defmodule Lua.VM.Executor do q = div(a, b) r = rem(a, b) # Adjust if remainder has different sign than divisor - if r != 0 and Bitwise.bxor(r, b) < 0, do: q - 1, else: q + result = if r != 0 and Bitwise.bxor(r, b) < 0, do: q - 1, else: q + to_signed_int64(result) end defp safe_power(a, b) do @@ -1633,7 +1634,8 @@ defmodule Lua.VM.Executor do defp safe_negate(a) do case to_number(a) do {:ok, na} -> - -na + result = -na + if is_integer(result), do: to_signed_int64(result), else: result {:error, val} -> raise TypeError, @@ -1750,7 +1752,7 @@ defmodule Lua.VM.Executor do 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) + Bitwise.bsl(val, shift) |> to_signed_int64() end defp lua_shift_right(_val, shift) when shift >= 64, do: 0 @@ -1758,9 +1760,17 @@ defmodule Lua.VM.Executor do 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 right shift - mask to 64-bit unsigned first unsigned_val = Bitwise.band(val, 0xFFFFFFFFFFFFFFFF) - Bitwise.bsr(unsigned_val, shift) + Bitwise.bsr(unsigned_val, shift) |> to_signed_int64() + end + + # Wrap an arbitrary-precision integer to a signed 64-bit integer + @int64_max 0x7FFFFFFFFFFFFFFF + @int64_mod 0x10000000000000000 + defp to_signed_int64(val) do + masked = Bitwise.band(val, 0xFFFFFFFFFFFFFFFF) + if masked > @int64_max, do: masked - @int64_mod, else: masked end # Helper to determine Lua type from Elixir value diff --git a/tmp_test.exs b/tmp_test.exs new file mode 100644 index 0000000..925b276 --- /dev/null +++ b/tmp_test.exs @@ -0,0 +1,8 @@ +source = File.read!("test/lua53_tests/constructs.lua") +lines = String.split(source, "\n") + +chunk = Enum.take(lines, 96) |> Enum.join("\n") + +lua = Lua.new(exclude: [[:package], [:require]]) +{_, _lua} = Lua.eval!(lua, chunk) +IO.puts("OK") From b106a4510d4ab75c078e66c0ff57daa5da3b3e4b Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Fri, 13 Feb 2026 18:49:04 -0800 Subject: [PATCH 3/5] chore: Remove tmp test file --- tmp_test.exs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 tmp_test.exs diff --git a/tmp_test.exs b/tmp_test.exs deleted file mode 100644 index 925b276..0000000 --- a/tmp_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -source = File.read!("test/lua53_tests/constructs.lua") -lines = String.split(source, "\n") - -chunk = Enum.take(lines, 96) |> Enum.join("\n") - -lua = Lua.new(exclude: [[:package], [:require]]) -{_, _lua} = Lua.eval!(lua, chunk) -IO.puts("OK") From f041e4414fe85fb170fe5bb7b7185dc5c23d93d2 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Sat, 14 Feb 2026 12:52:49 -0800 Subject: [PATCH 4/5] chore: Fix formatter issues --- lib/lua/vm/executor.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index 9a1b978..13aa428 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -796,7 +796,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_binary_metamethod("__band", val_a, val_b, state, fn -> - Bitwise.band(to_integer!(val_a), to_integer!(val_b)) |> to_signed_int64() + val_a |> to_integer!() |> Bitwise.band(to_integer!(val_b)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -809,7 +809,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_binary_metamethod("__bor", val_a, val_b, state, fn -> - Bitwise.bor(to_integer!(val_a), to_integer!(val_b)) |> to_signed_int64() + val_a |> to_integer!() |> Bitwise.bor(to_integer!(val_b)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -822,7 +822,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_binary_metamethod("__bxor", val_a, val_b, state, fn -> - Bitwise.bxor(to_integer!(val_a), to_integer!(val_b)) |> to_signed_int64() + val_a |> to_integer!() |> Bitwise.bxor(to_integer!(val_b)) |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -860,7 +860,7 @@ defmodule Lua.VM.Executor do {result, new_state} = try_unary_metamethod("__bnot", val, state, fn -> - Bitwise.bnot(to_integer!(val)) |> to_signed_int64() + val |> to_integer!() |> Bitwise.bnot() |> to_signed_int64() end) regs = put_elem(regs, dest, result) @@ -1752,7 +1752,7 @@ defmodule Lua.VM.Executor do defp lua_shift_left(val, shift) when shift < 0, do: lua_shift_right(val, -shift) defp lua_shift_left(val, shift) do - Bitwise.bsl(val, shift) |> to_signed_int64() + val |> Bitwise.bsl(shift) |> to_signed_int64() end defp lua_shift_right(_val, shift) when shift >= 64, do: 0 @@ -1762,7 +1762,7 @@ defmodule Lua.VM.Executor do defp lua_shift_right(val, shift) do # Unsigned right shift - mask to 64-bit unsigned first unsigned_val = Bitwise.band(val, 0xFFFFFFFFFFFFFFFF) - Bitwise.bsr(unsigned_val, shift) |> to_signed_int64() + unsigned_val |> Bitwise.bsr(shift) |> to_signed_int64() end # Wrap an arbitrary-precision integer to a signed 64-bit integer From 3f8dcc988c19158a0217120de406bc960b5ab3ce Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Tue, 17 Feb 2026 15:20:37 -0500 Subject: [PATCH 5/5] test: Add comprehensive tests for integer overflow and FuncDecl features Add 24 new tests covering: **64-bit Integer Overflow Wrapping (13 tests)**: - maxint + 1 wraps to minint - minint - 1 wraps to maxint - Bitwise operations (<<, >>, &, |, ~, ^) wrap to signed 64-bit - Arithmetic operations (+, -, *, //, %, unary -) wrap for integers - Float operations preserve IEEE 754 semantics (no wrapping) - Edge cases: shifts >= 64 bits, mixed integer/float arithmetic **FuncDecl Local Assignment (11 tests)**: - function f() updates local f when it exists in scope - function f() creates global when no local exists - Nested scopes handle local updates correctly - Works with upvalue capture - Multiple redefinitions in same scope - Forward reference patterns - Dotted names (t.f) always use table assignment - Method syntax (:) works correctly - Loop iterations create independent locals All tests verify Lua 5.3 spec compliance and pass with 0 failures. Co-Authored-By: Claude Opus 4.6 --- test/lua_test.exs | 362 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) diff --git a/test/lua_test.exs b/test/lua_test.exs index 905aeab..b20155f 100644 --- a/test/lua_test.exs +++ b/test/lua_test.exs @@ -2366,6 +2366,368 @@ defmodule LuaTest do end end + describe "64-bit integer overflow wrapping (Lua 5.3)" do + setup do + %{lua: Lua.new(sandboxed: [])} + end + + test "maxint + 1 wraps to minint", %{lua: lua} do + code = """ + local maxint = 9223372036854775807 + local minint = -9223372036854775808 + return maxint + 1 == minint + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "minint - 1 wraps to maxint", %{lua: lua} do + code = """ + local maxint = 9223372036854775807 + local minint = -9223372036854775808 + return minint - 1 == maxint + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "bitwise left shift wraps to signed 64-bit", %{lua: lua} do + code = """ + local minint = -9223372036854775808 + return 1 << 63 == minint + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "bitwise operations wrap to signed 64-bit", %{lua: lua} do + # Test that bitwise operations produce signed results + code = """ + -- 1 << 63 should be minint (most significant bit set) + local result = 1 << 63 + return result < 0 + """ + + assert {[true], _} = Lua.eval!(lua, code) + + # Test bitwise NOT wraps correctly + code = """ + local minint = -9223372036854775808 + return ~0 == -1 + """ + + assert {[true], _} = Lua.eval!(lua, code) + + # Test bitwise OR with high bit + code = """ + local result = 0 | (1 << 63) + return result < 0 + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "arithmetic overflow wraps for integers", %{lua: lua} do + # Integer addition overflow + code = """ + local maxint = 9223372036854775807 + local result = maxint + maxint + return result < 0 -- Should wrap to negative + """ + + assert {[true], _} = Lua.eval!(lua, code) + + # Integer multiplication overflow + code = """ + local big = 9223372036854775807 + local result = big * 2 + return result == -2 + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "unary minus wraps for minint", %{lua: lua} do + code = """ + local minint = -9223372036854775808 + return -minint == minint -- minint negated wraps back to minint + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "floor division wraps for integer operands", %{lua: lua} do + code = """ + local minint = -9223372036854775808 + return minint // -1 == minint -- Division overflow wraps + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "modulo wraps correctly", %{lua: lua} do + code = """ + local minint = -9223372036854775808 + -- minint % -1 should be 0 in Lua 5.3 + return minint % -1 == 0 + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "float operations do not wrap", %{lua: lua} do + # Float operations should use IEEE 754, not wrap + code = """ + local maxint = 9223372036854775807 + local result = maxint + 1.0 -- Float addition + return result > maxint -- Should be larger, not wrap + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "mixed integer/float arithmetic preserves float semantics", %{lua: lua} do + code = """ + local maxint = 9223372036854775807 + local result = maxint + 1.0 + return type(result) == "number" and result > 0 + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "bitwise shift left by 64 or more yields 0", %{lua: lua} do + code = """ + return (1 << 64) == 0 and (1 << 100) == 0 + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "bitwise shift right by 64 or more yields 0", %{lua: lua} do + code = """ + return (1 << 63) >> 64 == 0 + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + + test "all bitwise operations wrap consistently", %{lua: lua} do + code = """ + local maxint = 9223372036854775807 + local minint = -9223372036854775808 + + -- Test band + local a = (maxint + 1) & 0xFFFFFFFFFFFFFFFF + assert(a == minint) + + -- Test bor + local b = 0 | (1 << 63) + assert(b == minint) + + -- Test bxor + local c = maxint ~ 0xFFFFFFFFFFFFFFFF + assert(c == minint) + + -- Test bnot + local d = ~0 + assert(d == -1) + + return true + """ + + assert {[true], _} = Lua.eval!(lua, code) + end + end + + describe "FuncDecl local assignment" do + setup do + %{lua: Lua.new(sandboxed: [])} + end + + test "function f() updates local f when it exists", %{lua: lua} do + code = """ + local f = function(x) return "first: " .. x end + assert(f("test") == "first: test") + + -- Redefine using function syntax + function f(x) + return "second: " .. x + end + + return f("test") + """ + + assert {["second: test"], _} = Lua.eval!(lua, code) + end + + test "function f() creates global when no local exists", %{lua: lua} do + code = """ + function f(x) + return "global: " .. x + end + + return f("test") + """ + + assert {["global: test"], _} = Lua.eval!(lua, code) + end + + test "function f() in nested scope updates correct local", %{lua: lua} do + code = """ + local f = function() return "outer" end + + do + local f = function() return "inner1" end + assert(f() == "inner1") + + function f() + return "inner2" + end + + assert(f() == "inner2") + end + + -- Outer f should be unchanged + return f() + """ + + assert {["outer"], _} = Lua.eval!(lua, code) + end + + test "function f() works with upvalue capture", %{lua: lua} do + code = """ + local x = 10 + local f = function() return x end + + -- Redefine f - should still capture x + function f() + return x + 5 + end + + return f() + """ + + assert {[15], _} = Lua.eval!(lua, code) + end + + test "multiple redefinitions work correctly", %{lua: lua} do + code = """ + local f = function() return 1 end + assert(f() == 1) + + function f() return 2 end + assert(f() == 2) + + function f() return 3 end + assert(f() == 3) + + return f() + """ + + assert {[3], _} = Lua.eval!(lua, code) + end + + test "function f() with different names in same scope", %{lua: lua} do + code = """ + local f = function() return "f1" end + local g = function() return "g1" end + + function f() return "f2" end + function g() return "g2" end + + return f(), g() + """ + + assert {["f2", "g2"], _} = Lua.eval!(lua, code) + end + + test "function f() in loop creates new local each iteration", %{lua: lua} do + code = """ + local funcs = {} + for i = 1, 3 do + local f = function() return i end + function f() return i * 10 end + funcs[i] = f + end + + return funcs[1](), funcs[2](), funcs[3]() + """ + + assert {[10, 20, 30], _} = Lua.eval!(lua, code) + end + + test "dotted function names always use table assignment", %{lua: lua} do + code = """ + local t = {} + t.f = function() return "first" end + + function t.f() + return "second" + end + + return t.f() + """ + + assert {["second"], _} = Lua.eval!(lua, code) + end + + test "method syntax works correctly", %{lua: lua} do + code = """ + local obj = {value = 10} + + function obj:get() + return self.value + end + + function obj:set(v) + self.value = v + end + + obj:set(20) + return obj:get() + """ + + assert {[20], _} = Lua.eval!(lua, code) + end + + test "local function vs function with same name", %{lua: lua} do + code = """ + -- First define as local + local function f() return "local1" end + assert(f() == "local1") + + -- Redefine using local function again + local function f() return "local2" end + assert(f() == "local2") + + -- Redefine using function syntax (should update the local) + function f() return "updated" end + + return f() + """ + + assert {["updated"], _} = Lua.eval!(lua, code) + end + + test "forward reference pattern works", %{lua: lua} do + code = """ + local f + + local function g() + return f() + 1 + end + + function f() + return 10 + end + + return g() + """ + + assert {[11], _} = Lua.eval!(lua, code) + end + end defp test_file(name) do Path.join(["test", "fixtures", name])