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
75 changes: 64 additions & 11 deletions lib/lua/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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} ->
Expand Down
19 changes: 5 additions & 14 deletions lib/lua/vm/stdlib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions test/lua/parser/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
74 changes: 74 additions & 0 deletions test/lua/parser/statement_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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