diff --git a/lib/lua/parser.ex b/lib/lua/parser.ex index 1dc7145..a37ac3a 100644 --- a/lib/lua/parser.ex +++ b/lib/lua/parser.ex @@ -105,9 +105,14 @@ defmodule Lua.Parser do {:eof, _} -> {:ok, Block.new(Enum.reverse(stmts)), tokens} + # Skip empty statements (semicolons) + {:delimiter, :semicolon, _} -> + {_, rest} = consume(tokens) + parse_block_acc(rest, stmts) + # Skip orphaned comments at end of block (before terminator) {:comment, _, _, _} -> - # Check if comments are orphaned (followed only by terminator/EOF) + # Check if comments are orphaned (followed only by terminator/EOF/semicolon) tokens_after_comments = skip_orphaned_comments(tokens) case peek(tokens_after_comments) do @@ -117,6 +122,11 @@ defmodule Lua.Parser do {:eof, _} -> {:ok, Block.new(Enum.reverse(stmts)), tokens_after_comments} + # Comments followed by semicolons - skip both and continue + {:delimiter, :semicolon, _} -> + {_, rest} = consume(tokens_after_comments) + parse_block_acc(rest, stmts) + _ -> # Not orphaned, parse as normal statement (comments will be collected) case parse_stmt(tokens) do @@ -199,11 +209,6 @@ defmodule Lua.Parser do {:delimiter, :double_colon, _} -> parse_label(tokens) - # Semicolon (statement separator, optional) - {:delimiter, :semicolon, _} -> - {_, rest} = consume(tokens) - parse_stmt(rest) - _ -> # Try to parse as assignment or function call parse_assign_or_call(tokens) @@ -657,6 +662,11 @@ defmodule Lua.Parser do # Parse prefix expressions (primary expressions and unary operators) defp parse_prefix(tokens) do case peek(tokens) do + # Skip comments in expressions + {:comment, _, _, _} -> + {_, rest} = consume(tokens) + parse_prefix(rest) + # Literals {:keyword, nil, pos} -> {_, rest} = consume(tokens) @@ -813,6 +823,24 @@ defmodule Lua.Parser do {:error, reason} end + # Postfix: function call with string literal (syntactic sugar) + {:string, value, pos} -> + string_arg = %Expr.String{value: value, meta: Meta.new(pos)} + {_, rest} = consume(tokens) + new_left = %Expr.Call{func: left, args: [string_arg], meta: nil} + parse_infix(new_left, rest, min_prec) + + # Postfix: function call with table constructor (syntactic sugar) + {:delimiter, :lbrace, _} -> + case parse_table(tokens) do + {:ok, table, rest} -> + new_left = %Expr.Call{func: left, args: [table], meta: nil} + parse_infix(new_left, rest, min_prec) + + {:error, reason} -> + {:error, reason} + end + # Postfix: indexing {:delimiter, :lbracket, _} -> case parse_index(tokens) do @@ -842,13 +870,38 @@ defmodule Lua.Parser do case expect(rest, :identifier) do {:ok, {_, method, _}, rest2} -> - case parse_call_args(rest2) do - {:ok, args, rest3} -> - new_left = %Expr.MethodCall{object: left, method: method, args: args, meta: nil} + # Method calls support syntactic sugar: obj:method"str" or obj:method{...} + case rest2 do + # Regular method call with parentheses + [{:delimiter, :lparen, _} | _] -> + case parse_call_args(rest2) do + {:ok, args, rest3} -> + new_left = %Expr.MethodCall{object: left, method: method, args: args, meta: nil} + parse_infix(new_left, rest3, min_prec) + + {:error, reason} -> + {:error, reason} + end + + # Method call with string literal (syntactic sugar) + [{:string, value, pos} | rest3] -> + string_arg = %Expr.String{value: value, meta: Meta.new(pos)} + new_left = %Expr.MethodCall{object: left, method: method, args: [string_arg], meta: nil} parse_infix(new_left, rest3, min_prec) - {:error, reason} -> - {:error, reason} + # Method call with table constructor (syntactic sugar) + [{:delimiter, :lbrace, _} | _] -> + case parse_table(rest2) do + {:ok, table, rest3} -> + new_left = %Expr.MethodCall{object: left, method: method, args: [table], meta: nil} + parse_infix(new_left, rest3, min_prec) + + {:error, reason} -> + {:error, reason} + end + + _ -> + {:error, "Expected '(', string, or table after method name"} end {:error, reason} -> diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 6c5e423..fb40a14 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -339,16 +339,11 @@ defmodule Lua.VM.Stdlib do defp lua_load([chunk | _rest], state) when is_binary(chunk) do case Lua.Parser.parse(chunk) do {:ok, ast} -> - case Lua.Compiler.compile(ast) do - {:ok, prototype} -> - # Create a closure from the compiled prototype - closure = {:lua_closure, prototype, %{}} - {[closure], state} - - {:error, reason} -> - error_msg = format_compile_error(reason) - {[nil, error_msg], state} - end + # Compiler currently never returns errors, always succeeds + {:ok, prototype} = Lua.Compiler.compile(ast) + # Create a closure from the compiled prototype + closure = {:lua_closure, prototype, %{}} + {[closure], state} {:error, reason} -> error_msg = format_parse_error(reason) @@ -367,10 +362,6 @@ defmodule Lua.VM.Stdlib do end defp format_parse_error(error) when is_binary(error), do: error - defp format_parse_error(error), do: inspect(error) - - defp format_compile_error(error) when is_binary(error), do: error - defp format_compile_error(error), do: inspect(error) # setmetatable(table, metatable) — sets the metatable for a table defp lua_setmetatable([{:tref, _} = tref, metatable], state) do diff --git a/test/lua/parser/expr_test.exs b/test/lua/parser/expr_test.exs index 8572df9..cac5316 100644 --- a/test/lua/parser/expr_test.exs +++ b/test/lua/parser/expr_test.exs @@ -84,4 +84,92 @@ defmodule Lua.Parser.ExprTest do assert {:ok, _} = parse_return_expr("return t.a.b.c") end end + + describe "function call syntactic sugar" do + test "parses function call with string literal" do + # f "str" is syntactic sugar for f("str") + assert {:ok, %Expr.Call{func: %Expr.Var{name: "f"}, args: [%Expr.String{value: "hello"}]}} = + parse_return_expr(~s(return f "hello")) + + # Works with require pattern + assert {:ok, %Expr.Call{func: %Expr.Var{name: "require"}, args: [%Expr.String{value: "debug"}]}} = + parse_return_expr(~s(return require "debug")) + end + + test "parses function call with long string literal" do + # f [[str]] is syntactic sugar for f([[str]]) + assert {:ok, %Expr.Call{func: %Expr.Var{name: "load"}, args: [%Expr.String{value: "return 1 + 2"}]}} = + parse_return_expr("return load[[return 1 + 2]]") + end + + test "parses function call with table constructor" do + # f{...} is syntactic sugar for f({...}) + assert {:ok, %Expr.Call{func: %Expr.Var{name: "f"}, args: [%Expr.Table{fields: []}]}} = + parse_return_expr("return f{}") + + assert {:ok, %Expr.Call{func: %Expr.Var{name: "print"}, args: [%Expr.Table{fields: [_, _, _]}]}} = + parse_return_expr("return print{1, 2, 3}") + + assert {:ok, %Expr.Call{func: %Expr.Var{name: "config"}, args: [%Expr.Table{}]}} = + parse_return_expr("return config{a = 1, b = 2}") + end + + test "parses chained function calls with syntactic sugar" do + # Can chain multiple calls + assert {:ok, %Expr.Call{}} = parse_return_expr(~s(return f "a" "b")) + + # Can mix syntactic sugar with regular calls + assert {:ok, %Expr.Call{}} = parse_return_expr(~s[return f "a"(x)]) + end + + test "parses method calls with syntactic sugar" do + # obj:method "str" is valid + assert {:ok, %Expr.MethodCall{object: %Expr.Var{name: "obj"}, method: "m"}} = + parse_return_expr(~s(return obj:m "hello")) + + # obj:method{} is valid + assert {:ok, %Expr.MethodCall{object: %Expr.Var{name: "obj"}, method: "m"}} = + parse_return_expr("return obj:m{1, 2}") + end + end + + describe "comments in expressions" do + test "parses comments before expressions" do + code = """ + return -- comment + 42 + """ + + assert {:ok, %Expr.Number{value: 42}} = parse_return_expr(code) + end + + test "parses comments in binary expressions" do + code = """ + return 1 + -- comment + 2 + """ + + assert {:ok, %Expr.BinOp{op: :add}} = parse_return_expr(code) + end + + test "parses comments after operators" do + code = """ + return f(1,2,'a') + ~= -- force SETLINE before nil + nil + """ + + assert {:ok, %Expr.BinOp{op: :ne}} = parse_return_expr(code) + end + + test "parses multiple comments in complex expressions" do + code = """ + return (1 + -- comment 1 + 2) * -- comment 2 + 3 + """ + + assert {:ok, %Expr.BinOp{op: :mul}} = parse_return_expr(code) + end + end end diff --git a/test/lua/parser/statement_test.exs b/test/lua/parser/statement_test.exs index c1093a0..f855742 100644 --- a/test/lua/parser/statement_test.exs +++ b/test/lua/parser/statement_test.exs @@ -590,4 +590,78 @@ defmodule Lua.Parser.StatementTest do """) end end + + describe "empty statements (semicolons)" do + test "parses multiple semicolons at start of code" do + assert {:ok, chunk} = Parser.parse(~s(;;print "hi";;)) + assert %{block: %{stmts: [%Statement.CallStmt{}]}} = chunk + end + + test "parses semicolons in do-end blocks" do + assert {:ok, _chunk} = Parser.parse("do ;;; end") + assert {:ok, _chunk} = Parser.parse("do ; a = 3; assert(a == 3) end") + end + + test "parses semicolons after comments" do + assert {:ok, chunk} = Parser.parse("-- comment\n\n;;print \"hi\"") + assert %{block: %{stmts: [%Statement.CallStmt{}]}} = chunk + end + + test "parses semicolons between statements" do + assert {:ok, chunk} = Parser.parse("a = 1; b = 2; c = 3") + + assert %{ + block: %{ + stmts: [ + %Statement.Assign{}, + %Statement.Assign{}, + %Statement.Assign{} + ] + } + } = chunk + end + + test "parses multiple consecutive semicolons between statements" do + assert {:ok, chunk} = Parser.parse("a = 1;; b = 2;;; c = 3") + + assert %{ + block: %{ + stmts: [ + %Statement.Assign{}, + %Statement.Assign{}, + %Statement.Assign{} + ] + } + } = chunk + end + + test "parses semicolons at end of file" do + assert {:ok, chunk} = Parser.parse("print \"test\";;") + assert %{block: %{stmts: [%Statement.CallStmt{}]}} = chunk + end + + test "parses only semicolons as empty program" do + assert {:ok, chunk} = Parser.parse(";;;") + assert %{block: %{stmts: []}} = chunk + end + + test "parses semicolons with various statement types" do + code = """ + ;local x = 1; + ;if x then; return x; end; + ; + """ + + assert {:ok, chunk} = Parser.parse(code) + + assert %{ + block: %{ + stmts: [ + %Statement.Local{}, + %Statement.If{} + ] + } + } = chunk + end + end end