Skip to content

Commit 84fe7a1

Browse files
comnikclaude
andauthored
Fix symbol parsing (#242)
* Fix SYMBOL parsing * Fix relation id parsing to use native byte order * Revert "Fix relation id parsing to use native byte order" This reverts commit 891b958. * Update special_chars_in_ids snapshots after endianness revert The revert of the endianness commit also reverted the binary and pretty_debug snapshot changes that were part of the SYMBOL fix. Regenerate them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 317285b commit 84fe7a1

11 files changed

Lines changed: 100 additions & 9 deletions

File tree

meta/src/meta/grammar.y

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
%token UINT32 UInt32 r'\d+u32'
4444
%token INT128 logic.Int128Value r'[-]?\d+i128'
4545
%token STRING String r'"(?:[^"\\]|\\.)*"'
46-
%token SYMBOL String r'[a-zA-Z_][a-zA-Z0-9_./#-]*'
46+
%token SYMBOL String r'[a-zA-Z_][a-zA-Z0-9_.#/-]*'
4747
%token UINT128 logic.UInt128Value r'0x[0-9a-fA-F]+'
4848

4949
# Token aliases for formula constants (use hookable formatting in pretty printer)

sdks/go/src/parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ var (
166166
{"UINT32", regexp.MustCompile(`^\d+u32`), func(s string) TokenValue { return TokenValue{kind: kindUint32, u32: scanUint32(s)} }},
167167
{"INT128", regexp.MustCompile(`^[-]?\d+i128`), func(s string) TokenValue { return TokenValue{kind: kindInt128, int128: scanInt128(s)} }},
168168
{"STRING", regexp.MustCompile(`^"(?:[^"\\]|\\.)*"`), func(s string) TokenValue { return TokenValue{kind: kindString, str: scanString(s)} }},
169-
{"SYMBOL", regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_./#-]*`), func(s string) TokenValue { return TokenValue{kind: kindString, str: scanSymbol(s)} }},
169+
{"SYMBOL", regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_.#/-]*`), func(s string) TokenValue { return TokenValue{kind: kindString, str: scanSymbol(s)} }},
170170
{"UINT128", regexp.MustCompile(`^0x[0-9a-fA-F]+`), func(s string) TokenValue { return TokenValue{kind: kindUint128, uint128: scanUint128(s)} }},
171171
}
172172
)

sdks/go/test/lexer_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package test
2+
3+
import (
4+
"testing"
5+
6+
lqp "github.com/RelationalAI/logical-query-protocol/sdks/go/src"
7+
)
8+
9+
// TestSymbolLexerRegex verifies that the SYMBOL regex treats hyphen as a literal
10+
// character and does not accidentally include characters like $, %, etc.
11+
func TestSymbolLexerRegex(t *testing.T) {
12+
t.Run("hyphenated symbol", func(t *testing.T) {
13+
// A hyphenated relation name should parse without error.
14+
input := `(fragment :test (def :my-rel ([x::INT] (relatom :my-rel x))))`
15+
result, _, err := lqp.ParseFragment(input)
16+
if err != nil {
17+
t.Fatalf("Failed to parse hyphenated symbol: %v", err)
18+
}
19+
if result == nil {
20+
t.Fatal("ParseFragment returned nil")
21+
}
22+
})
23+
24+
t.Run("symbol with hash and slash", func(t *testing.T) {
25+
input := `(fragment :test (def :base/#output ([x::INT] (relatom :base/#output x))))`
26+
result, _, err := lqp.ParseFragment(input)
27+
if err != nil {
28+
t.Fatalf("Failed to parse symbol with hash and slash: %v", err)
29+
}
30+
if result == nil {
31+
t.Fatal("ParseFragment returned nil")
32+
}
33+
})
34+
35+
t.Run("dollar terminates symbol", func(t *testing.T) {
36+
// '$' is not a valid SYMBOL character, so this should fail to parse.
37+
input := `(fragment :test (def :foo$bar ([x::INT] (relatom :foo$bar x))))`
38+
_, _, err := lqp.ParseFragment(input)
39+
if err == nil {
40+
t.Error("Expected parse error for symbol containing '$'")
41+
}
42+
})
43+
}

sdks/julia/LogicalQueryProtocol.jl/src/parser.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ const _TOKEN_SPECS = [
169169
("UINT32", r"\d+u32", scan_uint32),
170170
("INT128", r"[-]?\d+i128", scan_int128),
171171
("STRING", r"\"(?:[^\"\\]|\\.)*\"", scan_string),
172-
("SYMBOL", r"[a-zA-Z_][a-zA-Z0-9_./#-]*", scan_symbol),
172+
("SYMBOL", r"[a-zA-Z_][a-zA-Z0-9_.#/-]*", scan_symbol),
173173
("UINT128", r"0x[0-9a-fA-F]+", scan_uint128),
174174
]
175175

sdks/julia/LogicalQueryProtocol.jl/test/parser_tests.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,20 @@ end
161161
)
162162
end
163163

164+
@testitem "Parser - SYMBOL lexer regex" setup=[ParserSetup] begin
165+
# Hyphen must be a literal character, not part of a range
166+
lexer = Lexer("my-relation")
167+
@test lexer.tokens[1].type == "SYMBOL"
168+
@test lexer.tokens[1].value == "my-relation"
169+
170+
lexer = Lexer("base/#output")
171+
@test lexer.tokens[1].type == "SYMBOL"
172+
@test lexer.tokens[1].value == "base/#output"
173+
174+
# '$' is not a valid SYMBOL character — the lexer should fail on it
175+
@test_throws ParseError Lexer("foo\$bar")
176+
end
177+
164178
@testitem "Parser - Lexer tokenization" setup=[ParserSetup] begin
165179
lexer = Lexer("(transaction (epoch (writes) (reads)))")
166180
# Tokens: ( transaction ( epoch ( writes ) ( reads ) ) ) $

sdks/python/src/lqp/gen/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def __repr__(self) -> str:
122122
("STRING", re.compile(r'"(?:[^"\\]|\\.)*"'), lambda x: Lexer.scan_string(x)),
123123
(
124124
"SYMBOL",
125-
re.compile(r"[a-zA-Z_][a-zA-Z0-9_./#-]*"),
125+
re.compile(r"[a-zA-Z_][a-zA-Z0-9_.#/-]*"),
126126
lambda x: Lexer.scan_symbol(x),
127127
),
128128
("UINT128", re.compile(r"0x[0-9a-fA-F]+"), lambda x: Lexer.scan_uint128(x)),

sdks/python/tests/test_parser.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@ def test_parse_transaction_rejects_fragment():
6868
parse_transaction(_SIMPLE_FRAGMENT)
6969

7070

71+
class TestSymbolLexing:
72+
"""Tests for SYMBOL token regex — hyphen must be literal, not a range."""
73+
74+
def test_hyphenated_symbol(self):
75+
tokens = Lexer("my-relation").tokens
76+
assert tokens[0].type == "SYMBOL"
77+
assert tokens[0].value == "my-relation"
78+
79+
def test_symbol_with_hash_and_slash(self):
80+
tokens = Lexer("base/#output").tokens
81+
assert tokens[0].type == "SYMBOL"
82+
assert tokens[0].value == "base/#output"
83+
84+
def test_dollar_is_not_part_of_symbol(self):
85+
# '$' is not a valid SYMBOL character — the lexer should fail on it
86+
with pytest.raises(ParseError):
87+
Lexer("foo$bar")
88+
89+
7190
class TestScanFloat32:
7291
"""Tests for parsing float32 literals including inf32 and nan32."""
7392

tests/bin/special_chars_in_ids.bin

194 Bytes
Binary file not shown.

tests/lqp/special_chars_in_ids.lqp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
;; Test that identifiers can contain / # characters
1+
;; Test that identifiers can contain / # - characters
22
(transaction
33
(epoch
44
(writes
@@ -10,10 +10,16 @@
1010
(= x 1)
1111
(atom :other/rel x x))))
1212
(def :my#relation
13+
([x::INT y::INT]
14+
(and
15+
(atom :my/relation x)
16+
(= y x))))
17+
(def :my-relation
1318
([x::INT y::INT]
1419
(and
1520
(atom :my/relation x)
1621
(= y x)))))))
1722
(reads
1823
(output :my/relation :my/relation)
19-
(output :my#relation :my#relation))))
24+
(output :my#relation :my#relation)
25+
(output :my-relation :my-relation))))

tests/pretty/special_chars_in_ids.lqp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,9 @@
66
(fragment
77
:f1
88
(def :my/relation ([x::INT] (and (= x 1) (atom :other/rel x x))))
9-
(def :my#relation ([x::INT y::INT] (and (atom :my/relation x) (= y x)))))))
10-
(reads (output :my/relation :my/relation) (output :my#relation :my#relation))))
9+
(def :my#relation ([x::INT y::INT] (and (atom :my/relation x) (= y x))))
10+
(def :my-relation ([x::INT y::INT] (and (atom :my/relation x) (= y x)))))))
11+
(reads
12+
(output :my/relation :my/relation)
13+
(output :my#relation :my#relation)
14+
(output :my-relation :my-relation))))

0 commit comments

Comments
 (0)