diff --git a/meta/src/meta/templates/parser.go.template b/meta/src/meta/templates/parser.go.template index 769acdac..1922a657 100644 --- a/meta/src/meta/templates/parser.go.template +++ b/meta/src/meta/templates/parser.go.template @@ -501,8 +501,8 @@ func toPascalCase(s string) string {{ // --- Parse functions --- {parse_nonterminal_defns} -// Parse parses the input string and returns the result -func Parse(input string) (result *pb.Transaction, err error) {{ +// ParseTransaction parses the input string and returns a Transaction +func ParseTransaction(input string) (result *pb.Transaction, err error) {{ defer func() {{ if r := recover(); r != nil {{ if pe, ok := r.(ParseError); ok {{ @@ -526,3 +526,34 @@ func Parse(input string) (result *pb.Transaction, err error) {{ }} return result, nil }} + +// ParseFragment parses the input string and returns a Fragment +func ParseFragment(input string) (result *pb.Fragment, err error) {{ + defer func() {{ + if r := recover(); r != nil {{ + if pe, ok := r.(ParseError); ok {{ + err = pe + return + }} + panic(r) + }} + }}() + + lexer := NewLexer(input) + parser := NewParser(lexer.tokens) + result = parser.parse_fragment() + + // Check for unconsumed tokens (except EOF) + if parser.pos < len(parser.tokens) {{ + remainingToken := parser.lookahead(0) + if remainingToken.Type != "$" {{ + return nil, ParseError{{msg: fmt.Sprintf("Unexpected token at end of input: %v", remainingToken)}} + }} + }} + return result, nil +}} + +// Parse parses the input string and returns a Transaction +func Parse(input string) (result *pb.Transaction, err error) {{ + return ParseTransaction(input) +}} diff --git a/meta/src/meta/templates/parser.jl.template b/meta/src/meta/templates/parser.jl.template index 8f34aba1..6528b5e6 100644 --- a/meta/src/meta/templates/parser.jl.template +++ b/meta/src/meta/templates/parser.jl.template @@ -279,7 +279,7 @@ end # --- Parse functions --- {parse_nonterminal_defns} -function parse(input::String) +function parse_transaction(input::String) lexer = Lexer(input) parser = ParserState(lexer.tokens) result = parse_{start_name}(parser) @@ -293,8 +293,26 @@ function parse(input::String) return result end -# Export main parse function and error type -export parse, ParseError +function parse_fragment(input::String) + lexer = Lexer(input) + parser = ParserState(lexer.tokens) + result = parse_fragment(parser) + # Check for unconsumed tokens (except EOF) + if parser.pos <= length(parser.tokens) + remaining_token = lookahead(parser, 0) + if remaining_token.type != "\$" + throw(ParseError("Unexpected token at end of input: $remaining_token")) + end + end + return result +end + +function parse(input::String) + return parse_transaction(input) +end + +# Export main parse functions and error type +export parse, parse_transaction, parse_fragment, ParseError # Export scanner functions for testing export scan_string, scan_int, scan_float, scan_int128, scan_uint128, scan_decimal # Export Lexer for testing diff --git a/meta/src/meta/templates/parser.py.template b/meta/src/meta/templates/parser.py.template index a7847d68..11cceae4 100644 --- a/meta/src/meta/templates/parser.py.template +++ b/meta/src/meta/templates/parser.py.template @@ -271,8 +271,8 @@ class Parser: # --- Parse methods --- {parse_nonterminal_defns} -def parse(input_str: str) -> Any: - """Parse input string and return parse tree.""" +def parse_transaction(input_str: str) -> Any: + """Parse input string and return a Transaction.""" lexer = Lexer(input_str) parser = Parser(lexer.tokens) result = parser.parse_{start_name}() @@ -282,3 +282,21 @@ def parse(input_str: str) -> Any: if remaining_token.type != "$": raise ParseError(f"Unexpected token at end of input: {{remaining_token}}") return result + + +def parse_fragment(input_str: str) -> Any: + """Parse input string and return a Fragment.""" + lexer = Lexer(input_str) + parser = Parser(lexer.tokens) + result = parser.parse_fragment() + # Check for unconsumed tokens (except EOF) + if parser.pos < len(parser.tokens): + remaining_token = parser.lookahead(0) + if remaining_token.type != "$": + raise ParseError(f"Unexpected token at end of input: {{remaining_token}}") + return result + + +def parse(input_str: str) -> Any: + """Parse input string and return a Transaction.""" + return parse_transaction(input_str) diff --git a/sdks/go/src/parser.go b/sdks/go/src/parser.go index 32010315..240bf20d 100644 --- a/sdks/go/src/parser.go +++ b/sdks/go/src/parser.go @@ -3741,8 +3741,8 @@ func (p *Parser) parse_export_csv_column() *pb.ExportCSVColumn { } -// Parse parses the input string and returns the result -func Parse(input string) (result *pb.Transaction, err error) { +// ParseTransaction parses the input string and returns a Transaction +func ParseTransaction(input string) (result *pb.Transaction, err error) { defer func() { if r := recover(); r != nil { if pe, ok := r.(ParseError); ok { @@ -3766,3 +3766,34 @@ func Parse(input string) (result *pb.Transaction, err error) { } return result, nil } + +// ParseFragment parses the input string and returns a Fragment +func ParseFragment(input string) (result *pb.Fragment, err error) { + defer func() { + if r := recover(); r != nil { + if pe, ok := r.(ParseError); ok { + err = pe + return + } + panic(r) + } + }() + + lexer := NewLexer(input) + parser := NewParser(lexer.tokens) + result = parser.parse_fragment() + + // Check for unconsumed tokens (except EOF) + if parser.pos < len(parser.tokens) { + remainingToken := parser.lookahead(0) + if remainingToken.Type != "$" { + return nil, ParseError{msg: fmt.Sprintf("Unexpected token at end of input: %v", remainingToken)} + } + } + return result, nil +} + +// Parse parses the input string and returns a Transaction +func Parse(input string) (result *pb.Transaction, err error) { + return ParseTransaction(input) +} diff --git a/sdks/go/test/parser_test.go b/sdks/go/test/parser_test.go index 423016c4..ad5dfb87 100644 --- a/sdks/go/test/parser_test.go +++ b/sdks/go/test/parser_test.go @@ -32,6 +32,65 @@ func TestBasicParsing(t *testing.T) { } } +// TestParseTransaction tests the ParseTransaction entry point. +func TestParseTransaction(t *testing.T) { + input := `(transaction (epoch (writes) (reads)))` + result, err := lqp.ParseTransaction(input) + if err != nil { + t.Fatalf("Failed to parse transaction: %v", err) + } + if result == nil { + t.Fatal("ParseTransaction returned nil") + } + if len(result.Epochs) != 1 { + t.Errorf("Expected 1 epoch, got %d", len(result.Epochs)) + } +} + +// TestParseFragment tests the ParseFragment entry point. +func TestParseFragment(t *testing.T) { + input := `(fragment :test_frag (def :my_rel ([x::INT] (relatom :my_rel x))))` + result, err := lqp.ParseFragment(input) + if err != nil { + t.Fatalf("Failed to parse fragment: %v", err) + } + if result == nil { + t.Fatal("ParseFragment returned nil") + } +} + +// TestParseDelegatesToParseTransaction verifies Parse and ParseTransaction return equal results. +func TestParseDelegatesToParseTransaction(t *testing.T) { + input := `(transaction (epoch (writes) (reads)))` + r1, err := lqp.Parse(input) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + r2, err := lqp.ParseTransaction(input) + if err != nil { + t.Fatalf("ParseTransaction failed: %v", err) + } + if !proto.Equal(r1, r2) { + t.Error("Parse and ParseTransaction should return equal results") + } +} + +// TestParseFragmentRejectsTransaction verifies ParseFragment rejects transaction input. +func TestParseFragmentRejectsTransaction(t *testing.T) { + _, err := lqp.ParseFragment(`(transaction (epoch (writes) (reads)))`) + if err == nil { + t.Error("ParseFragment should reject transaction input") + } +} + +// TestParseTransactionRejectsFragment verifies ParseTransaction rejects fragment input. +func TestParseTransactionRejectsFragment(t *testing.T) { + _, err := lqp.ParseTransaction(`(fragment :f (def :r ([x::INT] (relatom :r x))))`) + if err == nil { + t.Error("ParseTransaction should reject fragment input") + } +} + // TestParseLQPFiles parses all LQP files and compares against binary snapshots. func TestParseLQPFiles(t *testing.T) { root := repoRoot(t) diff --git a/sdks/julia/LogicalQueryProtocol.jl/src/parser.jl b/sdks/julia/LogicalQueryProtocol.jl/src/parser.jl index 361e1130..1cbb71f6 100644 --- a/sdks/julia/LogicalQueryProtocol.jl/src/parser.jl +++ b/sdks/julia/LogicalQueryProtocol.jl/src/parser.jl @@ -3192,7 +3192,7 @@ function parse_export_csv_column(parser::ParserState)::Proto.ExportCSVColumn end -function parse(input::String) +function parse_transaction(input::String) lexer = Lexer(input) parser = ParserState(lexer.tokens) result = parse_transaction(parser) @@ -3206,8 +3206,26 @@ function parse(input::String) return result end -# Export main parse function and error type -export parse, ParseError +function parse_fragment(input::String) + lexer = Lexer(input) + parser = ParserState(lexer.tokens) + result = parse_fragment(parser) + # Check for unconsumed tokens (except EOF) + if parser.pos <= length(parser.tokens) + remaining_token = lookahead(parser, 0) + if remaining_token.type != "\$" + throw(ParseError("Unexpected token at end of input: $remaining_token")) + end + end + return result +end + +function parse(input::String) + return parse_transaction(input) +end + +# Export main parse functions and error type +export parse, parse_transaction, parse_fragment, ParseError # Export scanner functions for testing export scan_string, scan_int, scan_float, scan_int128, scan_uint128, scan_decimal # Export Lexer for testing diff --git a/sdks/julia/LogicalQueryProtocol.jl/test/parser_tests.jl b/sdks/julia/LogicalQueryProtocol.jl/test/parser_tests.jl index 2939db31..dfde78a2 100644 --- a/sdks/julia/LogicalQueryProtocol.jl/test/parser_tests.jl +++ b/sdks/julia/LogicalQueryProtocol.jl/test/parser_tests.jl @@ -112,6 +112,34 @@ end @test r.scale == 1 end +@testitem "parse_transaction entry point" setup=[ParserSetup] begin + input = "(transaction (epoch (writes) (reads)))" + result = Parser.parse_transaction(input) + @test result isa Proto.Transaction + @test length(result.epochs) == 1 +end + +@testitem "parse_fragment entry point" setup=[ParserSetup] begin + input = "(fragment :test_frag (def :my_rel ([x::INT] (relatom :my_rel x))))" + result = Parser.parse_fragment(input) + @test result isa Proto.Fragment +end + +@testitem "parse delegates to parse_transaction" setup=[ParserSetup] begin + input = "(transaction (epoch (writes) (reads)))" + @test Parser.parse(input) == Parser.parse_transaction(input) +end + +@testitem "parse_fragment rejects transaction" setup=[ParserSetup] begin + @test_throws ParseError Parser.parse_fragment("(transaction (epoch (writes) (reads)))") +end + +@testitem "parse_transaction rejects fragment" setup=[ParserSetup] begin + @test_throws ParseError Parser.parse_transaction( + "(fragment :f (def :r ([x::INT] (relatom :r x))))" + ) +end + @testitem "Parser - Lexer tokenization" setup=[ParserSetup] begin lexer = Lexer("(transaction (epoch (writes) (reads)))") # Tokens: ( transaction ( epoch ( writes ) ( reads ) ) ) $ diff --git a/sdks/python/src/lqp/gen/parser.py b/sdks/python/src/lqp/gen/parser.py index 2c023fb0..502855f4 100644 --- a/sdks/python/src/lqp/gen/parser.py +++ b/sdks/python/src/lqp/gen/parser.py @@ -2849,8 +2849,8 @@ def parse_export_csv_column(self) -> transactions_pb2.ExportCSVColumn: return _t1310 -def parse(input_str: str) -> Any: - """Parse input string and return parse tree.""" +def parse_transaction(input_str: str) -> Any: + """Parse input string and return a Transaction.""" lexer = Lexer(input_str) parser = Parser(lexer.tokens) result = parser.parse_transaction() @@ -2860,3 +2860,21 @@ def parse(input_str: str) -> Any: if remaining_token.type != "$": raise ParseError(f"Unexpected token at end of input: {remaining_token}") return result + + +def parse_fragment(input_str: str) -> Any: + """Parse input string and return a Fragment.""" + lexer = Lexer(input_str) + parser = Parser(lexer.tokens) + result = parser.parse_fragment() + # Check for unconsumed tokens (except EOF) + if parser.pos < len(parser.tokens): + remaining_token = parser.lookahead(0) + if remaining_token.type != "$": + raise ParseError(f"Unexpected token at end of input: {remaining_token}") + return result + + +def parse(input_str: str) -> Any: + """Parse input string and return a Transaction.""" + return parse_transaction(input_str) diff --git a/sdks/python/tests/test_parser.py b/sdks/python/tests/test_parser.py index de05ea46..b21e043d 100644 --- a/sdks/python/tests/test_parser.py +++ b/sdks/python/tests/test_parser.py @@ -3,7 +3,8 @@ import pytest from pytest_snapshot.plugin import Snapshot -from lqp.gen.parser import parse +from lqp.gen.parser import ParseError, parse, parse_fragment, parse_transaction +from lqp.proto.v1 import fragments_pb2, transactions_pb2 from .utils import BIN_SNAPSHOTS_DIR, get_lqp_input_files @@ -20,3 +21,32 @@ def test_parse_lqp(snapshot: Snapshot, input_file): snapshot.snapshot_dir = BIN_SNAPSHOTS_DIR snapshot_filename = os.path.basename(input_file).replace(".lqp", ".bin") snapshot.assert_match(binary_output, snapshot_filename) + + +_SIMPLE_TXN = "(transaction (epoch (writes) (reads)))" +_SIMPLE_FRAGMENT = "(fragment :test_frag (def :my_rel ([x::INT] (relatom :my_rel x))))" + + +def test_parse_transaction(): + result = parse_transaction(_SIMPLE_TXN) + assert isinstance(result, transactions_pb2.Transaction) + assert len(result.epochs) == 1 + + +def test_parse_fragment(): + result = parse_fragment(_SIMPLE_FRAGMENT) + assert isinstance(result, fragments_pb2.Fragment) + + +def test_parse_delegates_to_parse_transaction(): + assert parse(_SIMPLE_TXN) == parse_transaction(_SIMPLE_TXN) + + +def test_parse_fragment_rejects_transaction(): + with pytest.raises(ParseError): + parse_fragment(_SIMPLE_TXN) + + +def test_parse_transaction_rejects_fragment(): + with pytest.raises(ParseError): + parse_transaction(_SIMPLE_FRAGMENT)