Skip to content
Open
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
35 changes: 33 additions & 2 deletions meta/src/meta/templates/parser.go.template
Original file line number Diff line number Diff line change
Expand Up @@ -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 {{
Expand All @@ -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)
}}
24 changes: 21 additions & 3 deletions meta/src/meta/templates/parser.jl.template
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
22 changes: 20 additions & 2 deletions meta/src/meta/templates/parser.py.template
Original file line number Diff line number Diff line change
Expand Up @@ -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}()
Expand All @@ -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)
35 changes: 33 additions & 2 deletions sdks/go/src/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
59 changes: 59 additions & 0 deletions sdks/go/test/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 21 additions & 3 deletions sdks/julia/LogicalQueryProtocol.jl/src/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions sdks/julia/LogicalQueryProtocol.jl/test/parser_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) ) $
Expand Down
22 changes: 20 additions & 2 deletions sdks/python/src/lqp/gen/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
32 changes: 31 additions & 1 deletion sdks/python/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Loading