From e42fd608b0baa738b483a02fb6228f29db6fb223 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Wed, 11 Feb 2026 11:00:59 -0800 Subject: [PATCH] feat(phase-10): Polish & remaining TODOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes Phase 10 of the Lua 5.3 VM implementation plan. ## Changes ### 1. Restored string.lower doctests (3 locations) Since string stdlib was implemented in Phase 5, restored the commented-out doctests in lib/lua.ex that demonstrate string.lower functionality: - call_function/3 doctest - Function reference doctest - API module doctest example ### 2. Implemented local function declarations (Statement.LocalFunc) Added compiler support for `local function name() ... end` syntax: - Scope resolver allocates register for local function name - Scope resolver processes function body scope - Codegen generates closure and stores in local register Limitations (marked as :skip in tests): - Self-recursive local functions not yet supported (requires upvalue self-reference) - Proper scope cleanup for nested scopes pending ### 3. Implemented do...end blocks (Statement.Do) Added compiler support for `do...end` blocks: - Scope resolver processes block body - Codegen generates block instructions Limitations (marked as :skip in tests): - Nested scope creation/cleanup not fully implemented ### 4. Comprehensive test coverage Added 9 new integration tests: - 4 tests for local function declarations - 5 tests for do...end blocks - 2 tests marked :skip for known limitations ## Test Results - All 1129 tests pass (up from 1120) - 34 skipped (up from 32) - 52 doctests, 14 properties ## Files Modified - lib/lua.ex — restored 3 string.lower doctests - lib/lua/compiler/codegen.ex — added LocalFunc and Do statement compilation - lib/lua/compiler/scope.ex — added LocalFunc and Do scope resolution - test/lua/compiler/integration_test.exs — added 9 new tests Implements Phase 10 from plan.md --- lib/lua.ex | 22 ++-- lib/lua/compiler/codegen.ex | 25 ++++ lib/lua/compiler/scope.ex | 20 +++ test/lua/compiler/integration_test.exs | 161 +++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 14 deletions(-) diff --git a/lib/lua.ex b/lib/lua.ex index 8084045..74ae082 100644 --- a/lib/lua.ex +++ b/lib/lua.ex @@ -569,10 +569,9 @@ defmodule Lua do @doc """ Calls a function in Lua's state - # TODO: Restore once string stdlib is implemented - #iex> {:ok, [ret], _lua} = Lua.call_function(Lua.new(), [:string, :lower], ["HELLO ROBERT"]) - #iex> ret - #"hello robert" + iex> {:ok, [ret], _lua} = Lua.call_function(Lua.new(), [:string, :lower], ["HELLO ROBERT"]) + iex> ret + "hello robert" iex> lua = Lua.new() iex> lua = Lua.set!(lua, [:double], fn [val] -> [val * 2] end) @@ -580,11 +579,10 @@ defmodule Lua do References to functions can also be passed - # TODO: Restore once string stdlib is implemented - #iex> {[ref], lua} = Lua.eval!("return string.lower", decode: false) - #iex> {:ok, [ret], _lua} = Lua.call_function(lua, ref, ["FUNCTION REF"]) - #iex> ret - #"function ref" + iex> {[ref], lua} = Lua.eval!(Lua.new(), "return string.lower", decode: false) + iex> {:ok, [ret], _lua} = Lua.call_function(lua, ref, ["FUNCTION REF"]) + iex> ret + "function ref" iex> {[ref], lua} = Lua.eval!(Lua.new(), "return function(x) return x end", decode: false) iex> {:ok, [ret], _lua} = Lua.call_function(lua, ref, [42]) @@ -660,12 +658,8 @@ defmodule Lua do defmodule MyAPI do use Lua.API, scope: "example" - # TODO: Restore once string stdlib is implemented - # deflua foo(value), state do - # Lua.call_function!(state, [:string, :lower], [value]) - # end deflua foo(value), state do - Lua.call_function!(state, [:my_func], [value]) + Lua.call_function!(state, [:string, :lower], [value]) end end ``` diff --git a/lib/lua/compiler/codegen.ex b/lib/lua/compiler/codegen.ex index 4d0877b..eadd60f 100644 --- a/lib/lua/compiler/codegen.ex +++ b/lib/lua/compiler/codegen.ex @@ -440,6 +440,31 @@ defmodule Lua.Compiler.Codegen do end end + # LocalFunc: local function name(params) body end + defp gen_statement(%Statement.LocalFunc{name: name} = local_func, ctx) do + # Generate closure for the function + {closure_instructions, closure_reg, ctx} = gen_closure_from_node(local_func, ctx) + + # Get the local variable's register from scope + dest_reg = ctx.scope.locals[name] + + # Move closure to the local's register if needed + move_instructions = + if closure_reg == dest_reg do + [] + else + [Instruction.move(dest_reg, closure_reg)] + end + + {closure_instructions ++ move_instructions, ctx} + end + + # Do: do...end block + defp gen_statement(%Statement.Do{body: body}, ctx) do + # Simply generate code for the inner block + gen_block(body, ctx) + end + # Stub for other statements defp gen_statement(_stmt, ctx), do: {[], ctx} diff --git a/lib/lua/compiler/scope.ex b/lib/lua/compiler/scope.ex index 0b7c8b4..f0fbf80 100644 --- a/lib/lua/compiler/scope.ex +++ b/lib/lua/compiler/scope.ex @@ -224,6 +224,26 @@ defmodule Lua.Compiler.Scope do resolve_block(body, state) end + defp resolve_statement(%Statement.LocalFunc{name: name, params: params, body: body} = local_func, state) do + # First, allocate a register for the local function name + reg = state.next_register + state = %{state | locals: Map.put(state.locals, name, reg)} + state = %{state | next_register: reg + 1} + + # Update max_register in current function scope + 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)} + + # Then resolve the function body scope (like FuncDecl) + resolve_function_scope(local_func, params, body, state) + end + + defp resolve_statement(%Statement.Do{body: body}, state) do + # Do blocks just resolve their inner body + resolve_block(body, state) + end + # For now, stub out other statement types - we'll implement them incrementally defp resolve_statement(_stmt, state), do: state diff --git a/test/lua/compiler/integration_test.exs b/test/lua/compiler/integration_test.exs index 27bfce8..9b8f1cb 100644 --- a/test/lua/compiler/integration_test.exs +++ b/test/lua/compiler/integration_test.exs @@ -1749,4 +1749,165 @@ defmodule Lua.Compiler.IntegrationTest do assert Enum.sort(decoded) == [{"a", 1}, {"b", 2}] end end + + describe "local function declarations" do + test "basic local function" do + code = """ + local function add(a, b) + return a + b + end + return add(3, 4) + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [7] + end + + test "local function with closure" do + code = """ + local function make_counter() + local count = 0 + local function increment() + count = count + 1 + return count + end + return increment + end + local counter = make_counter() + return counter(), counter(), counter() + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [1, 2, 3] + end + + @tag :skip + test "local function recursive (requires self-reference upvalue support)" do + code = """ + local function factorial(n) + if n <= 1 then + return 1 + else + return n * factorial(n - 1) + end + end + return factorial(5) + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [120] + end + + test "local function can be reassigned" do + code = """ + local function f() + return 1 + end + local x = f() + f = function() return 2 end + local y = f() + return x, y + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [1, 2] + end + end + + describe "do...end blocks" do + test "basic do block" do + code = """ + local x = 1 + do + local y = 2 + x = x + y + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [3] + end + + @tag :skip + test "do block creates new scope (requires proper scope cleanup)" do + code = """ + local x = 1 + do + local x = 2 + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [1] + end + + test "nested do blocks" do + code = """ + local x = 1 + do + x = 2 + do + x = 3 + end + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [3] + end + + test "empty do block" do + code = """ + local x = 1 + do + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [1] + end + + test "do block with return" do + code = """ + do + return 42 + end + return 99 + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, results, _state} = VM.execute(proto) + + assert results == [42] + end + end end