Skip to content

Commit 8e6516e

Browse files
authored
[LQP] Add provenance to LQP parser (#220)
Add provenance (source location) tracking to the LQP parser across all SDKs (Python, Go, Julia). Update validator to use it.
1 parent 29469c9 commit 8e6516e

60 files changed

Lines changed: 7808 additions & 5702 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
strategy:
6060
fail-fast: true
6161
matrix:
62-
julia-version: ["1.10", "1.11", "1.12"]
62+
julia-version: ["1.10", "1.12"]
6363

6464
steps:
6565
- uses: actions/checkout@v4

Makefile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Usage:
44
# make Build protobuf bindings, parsers, and printers.
55
# make protobuf Lint, check breaking changes, and generate protobuf code.
6+
# make force-protobuf Force-regenerate all protobuf code.
67
# make parsers Regenerate Python, Julia, and Go parsers from the grammar.
78
# make parser-X Regenerate a single parser (X = python, julia, go).
89
# make force-parsers Force-regenerate all parsers.
@@ -75,7 +76,7 @@ JL_PROTO_GENERATED := \
7576
$(JL_PROTO_DIR)/fragments_pb.jl \
7677
$(JL_PROTO_DIR)/transactions_pb.jl
7778

78-
.PHONY: all protobuf parsers parser-python parser-julia parser-go \
79+
.PHONY: all protobuf force-protobuf parsers parser-python parser-julia parser-go \
7980
force-parsers force-parser-python force-parser-julia force-parser-go \
8081
printers printer-python printer-julia printer-go \
8182
force-printers force-printer-python force-printer-julia force-printer-go \
@@ -93,6 +94,23 @@ protobuf: $(PY_PROTO_GENERATED) $(GO_PROTO_GENERATED) $(JL_PROTO_GENERATED)
9394
touch-proto-generated:
9495
touch $(PY_PROTO_GENERATED) $(GO_PROTO_GENERATED) $(JL_PROTO_GENERATED)
9596

97+
force-protobuf:
98+
buf lint
99+
buf breaking --against ".git#branch=main,subdir=proto"
100+
buf generate
101+
mkdir -p $(PY_PROTO_DIR)
102+
cp gen/python/relationalai/lqp/v1/*_pb2.py* $(PY_PROTO_DIR)/
103+
for file in $(PY_PROTO_DIR)/*_pb2.py*; do \
104+
sed 's/from relationalai\.lqp\.v1/from lqp\.proto\.v1/g' "$$file" > "$$file.tmp" && \
105+
mv "$$file.tmp" "$$file"; \
106+
sed 's/import relationalai\.lqp\.v1/import lqp\.proto\.v1/g' "$$file" > "$$file.tmp" && \
107+
mv "$$file.tmp" "$$file"; \
108+
done
109+
mkdir -p $(GO_PROTO_DIR)
110+
cp gen/go/relationalai/lqp/v1/*.pb.go $(GO_PROTO_DIR)/
111+
rm -rf gen/python gen/go
112+
cd sdks/julia && julia --project=LogicalQueryProtocol.jl generate_proto.jl
113+
96114
$(PY_PROTO_GENERATED) $(GO_PROTO_GENERATED): $(PROTO_FILES)
97115
buf lint
98116
buf breaking --against ".git#branch=main,subdir=proto"

meta/src/meta/codegen_python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def gen_list_type(self, element_type: str) -> str:
174174
return f"list[{element_type}]"
175175

176176
def gen_option_type(self, element_type: str) -> str:
177-
return f"Optional[{element_type}]"
177+
return f"{element_type} | None"
178178

179179
def gen_dict_type(self, key_type: str, value_type: str) -> str:
180180
return f"dict[{key_type}, {value_type}]"

meta/src/meta/codegen_templates.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ class BuiltinTemplate:
106106
"format_bytes": BuiltinTemplate('"0x" + {0}.hex()'),
107107
"pp_dispatch": BuiltinTemplate("None", ["self.pprint_dispatch({0})"]),
108108
"get_at": BuiltinTemplate("{0}[{1}]"),
109+
# Provenance tracking
110+
"span_start": BuiltinTemplate("self.span_start()"),
111+
"record_span": BuiltinTemplate("None", ["self.record_span({0}, {1})"]),
109112
}
110113

111114

@@ -191,6 +194,9 @@ class BuiltinTemplate:
191194
"format_bytes": BuiltinTemplate('"0x" * bytes2hex({0})'),
192195
"pp_dispatch": BuiltinTemplate("nothing", ["_pprint_dispatch(pp, {0})"]),
193196
"get_at": BuiltinTemplate("{0}[{1} + 1]"),
197+
# Provenance tracking
198+
"span_start": BuiltinTemplate("span_start(parser)"),
199+
"record_span": BuiltinTemplate("nothing", ["record_span!(parser, {0}, {1})"]),
194200
}
195201

196202

@@ -280,6 +286,9 @@ class BuiltinTemplate:
280286
"format_bytes": BuiltinTemplate('fmt.Sprintf("0x%x", {0})'),
281287
"pp_dispatch": BuiltinTemplate("nil", ["p.pprintDispatch({0})"]),
282288
"get_at": BuiltinTemplate("{0}[{1}]"),
289+
# Provenance tracking
290+
"span_start": BuiltinTemplate("int64(p.spanStart())"),
291+
"record_span": BuiltinTemplate("nil", ["p.recordSpan(int({0}), {1})"]),
283292
}
284293

285294
__all__ = [

meta/src/meta/parser_gen.py

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
Terminal,
9191
)
9292
from .grammar_utils import is_epsilon, rhs_elements
93+
from .proto_ast import ProtoMessage
9394
from .target import (
9495
Assign,
9596
BaseType,
@@ -100,6 +101,7 @@
100101
ListExpr,
101102
ListType,
102103
Lit,
104+
MessageType,
103105
ParseNonterminal,
104106
ParseNonterminalDef,
105107
Seq,
@@ -132,28 +134,55 @@ class AmbiguousGrammarError(Exception):
132134

133135

134136
def generate_parse_functions(
135-
grammar: Grammar, indent: str = ""
137+
grammar: Grammar,
138+
indent: str = "",
139+
proto_messages: dict[tuple[str, str], ProtoMessage] | None = None,
136140
) -> list[ParseNonterminalDef]:
137141
parser_methods = []
138142
reachable, _ = grammar.analysis.partition_nonterminals_by_reachability()
139143
for nt in reachable:
140144
rules = grammar.rules[nt]
141-
method_code = _generate_parse_method(nt, rules, grammar, indent)
145+
method_code = _generate_parse_method(nt, rules, grammar, indent, proto_messages)
142146
parser_methods.append(method_code)
143147
return parser_methods
144148

145149

150+
def _wrap_with_span(body: TargetExpr, return_type) -> TargetExpr:
151+
"""Wrap a nonterminal body with span_start/record_span."""
152+
span_var = Var(gensym("span_start"), BaseType("Int64"))
153+
result_var = Var(gensym("result"), return_type)
154+
type_name = return_type.name if isinstance(return_type, MessageType) else ""
155+
return Let(
156+
span_var,
157+
Call(make_builtin("span_start"), []),
158+
Let(
159+
result_var,
160+
body,
161+
Seq(
162+
[
163+
Call(make_builtin("record_span"), [span_var, Lit(type_name)]),
164+
result_var,
165+
]
166+
),
167+
),
168+
)
169+
170+
146171
def _generate_parse_method(
147-
lhs: Nonterminal, rules: list[Rule], grammar: Grammar, indent: str = ""
172+
lhs: Nonterminal,
173+
rules: list[Rule],
174+
grammar: Grammar,
175+
indent: str = "",
176+
proto_messages: dict[tuple[str, str], ProtoMessage] | None = None,
148177
) -> ParseNonterminalDef:
149-
"""Generate parse method code as string (preserving existing logic)."""
178+
"""Generate parse method for a nonterminal with provenance tracking."""
150179
return_type = None
151180
rhs = None
152181
follow_set = FollowSet(grammar, lhs)
153182
if len(rules) == 1:
154183
rule = rules[0]
155184
rhs = _generate_parse_rhs_ir(
156-
rule.rhs, grammar, follow_set, True, rule.constructor
185+
rule.rhs, grammar, follow_set, True, rule.constructor, proto_messages
157186
)
158187
return_type = rule.constructor.return_type
159188
else:
@@ -171,7 +200,6 @@ def _generate_parse_method(
171200
],
172201
)
173202
for i, rule in enumerate(rules):
174-
# Ensure the return type is the same for all actions for this nonterminal.
175203
assert return_type is None or return_type == rule.constructor.return_type, (
176204
f"Return type mismatch at rule {i}: {return_type} != {rule.constructor.return_type}"
177205
)
@@ -183,12 +211,19 @@ def _generate_parse_method(
183211
make_builtin("equal"), [Var(prediction, BaseType("Int64")), Lit(i)]
184212
),
185213
_generate_parse_rhs_ir(
186-
rule.rhs, grammar, follow_set, True, rule.constructor
214+
rule.rhs,
215+
grammar,
216+
follow_set,
217+
True,
218+
rule.constructor,
219+
proto_messages,
187220
),
188221
tail,
189222
)
190223
rhs = Let(Var(prediction, BaseType("Int64")), predictor, tail)
191224
assert return_type is not None
225+
if isinstance(return_type, MessageType):
226+
rhs = _wrap_with_span(rhs, return_type)
192227
return ParseNonterminalDef(lhs, [], return_type, rhs, indent)
193228

194229

@@ -377,30 +412,19 @@ def _generate_parse_rhs_ir(
377412
follow_set: TerminalSequenceSet,
378413
apply_action: bool = False,
379414
action: Lambda | None = None,
415+
proto_messages: dict[tuple[str, str], ProtoMessage] | None = None,
380416
) -> TargetExpr:
381-
"""Generate IR for parsing an RHS.
382-
383-
Args:
384-
rhs: The RHS to parse
385-
grammar: The grammar
386-
follow_set: TerminalSequenceSet for computing follow lazily
387-
apply_action: Whether to apply the semantic action
388-
action: The semantic action to apply (required if apply_action is True)
389-
390-
Returns IR expression for leaf nodes (Literal, Terminal, Nonterminal).
391-
Returns None for complex cases that still use string generation.
392-
"""
417+
"""Generate IR for parsing an RHS with provenance tracking."""
393418
if isinstance(rhs, Sequence):
394419
return _generate_parse_rhs_ir_sequence(
395-
rhs, grammar, follow_set, apply_action, action
420+
rhs, grammar, follow_set, apply_action, action, proto_messages
396421
)
397422
elif isinstance(rhs, LitTerminal):
398423
parse_expr = Call(make_builtin("consume_literal"), [Lit(rhs.name)])
399424
if apply_action and action:
400425
return Seq([parse_expr, apply_lambda(action, [])])
401426
return parse_expr
402427
elif isinstance(rhs, NamedTerminal):
403-
# Use terminal's actual type for consume_terminal instead of generic Token
404428
from .target import FunctionType
405429

406430
terminal_type = rhs.target_type()
@@ -427,14 +451,18 @@ def _generate_parse_rhs_ir(
427451
elif isinstance(rhs, Option):
428452
assert grammar is not None
429453
predictor = _build_option_predictor(grammar, rhs.rhs, follow_set)
430-
parse_result = _generate_parse_rhs_ir(rhs.rhs, grammar, follow_set, False, None)
454+
parse_result = _generate_parse_rhs_ir(
455+
rhs.rhs, grammar, follow_set, False, None, proto_messages
456+
)
431457
return IfElse(predictor, Call(make_builtin("some"), [parse_result]), Lit(None))
432458
elif isinstance(rhs, Star):
433459
assert grammar is not None
434460
xs = Var(gensym("xs"), ListType(rhs.rhs.target_type()))
435461
cond = Var(gensym("cond"), BaseType("Boolean"))
436462
predictor = _build_option_predictor(grammar, rhs.rhs, follow_set)
437-
parse_item = _generate_parse_rhs_ir(rhs.rhs, grammar, follow_set, False, None)
463+
parse_item = _generate_parse_rhs_ir(
464+
rhs.rhs, grammar, follow_set, False, None, proto_messages
465+
)
438466
item = Var(gensym("item"), rhs.rhs.target_type())
439467
loop_body = Seq(
440468
[
@@ -458,6 +486,7 @@ def _generate_parse_rhs_ir_sequence(
458486
follow_set: TerminalSequenceSet,
459487
apply_action: bool = False,
460488
action: Lambda | None = None,
489+
proto_messages: dict[tuple[str, str], ProtoMessage] | None = None,
461490
) -> TargetExpr:
462491
if is_epsilon(rhs):
463492
return Lit(None)
@@ -473,7 +502,9 @@ def _generate_parse_rhs_ir_sequence(
473502
follow_set_i = ConcatSet(first_following, follow_set)
474503
else:
475504
follow_set_i = follow_set
476-
elem_ir = _generate_parse_rhs_ir(elem, grammar, follow_set_i, False, None)
505+
elem_ir = _generate_parse_rhs_ir(
506+
elem, grammar, follow_set_i, False, None, proto_messages
507+
)
477508
if isinstance(elem, LitTerminal):
478509
exprs.append(elem_ir)
479510
else:
@@ -494,13 +525,10 @@ def _generate_parse_rhs_ir_sequence(
494525
lambda_call = apply_lambda(action, arg_vars)
495526
exprs.append(lambda_call)
496527
elif len(arg_vars) > 1:
497-
# Multiple values - wrap in tuple
498528
exprs.append(Call(make_builtin("tuple"), arg_vars))
499529
elif len(arg_vars) == 1:
500-
# Single value - return the variable
501530
exprs.append(arg_vars[0])
502531
else:
503-
# no non-literal elements, return None
504532
return Lit(None)
505533

506534
if len(exprs) == 1:

meta/src/meta/parser_gen_common.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def generate_parser(
1919
codegen: CodeGenerator,
2020
template_path: Path,
2121
command_line: str | None = None,
22+
proto_messages=None,
2223
) -> str:
2324
"""Generate a parser from a grammar using the given code generator and template.
2425
@@ -27,11 +28,14 @@ def generate_parser(
2728
codegen: Language-specific code generator
2829
template_path: Path to the language-specific template file
2930
command_line: Optional command line string for the file header
31+
proto_messages: Optional proto message definitions for provenance tracking
3032
"""
3133
template = template_path.read_text()
3234
indent = codegen.parse_def_indent
3335

34-
defns = generate_parse_functions(grammar, indent=indent)
36+
defns = generate_parse_functions(
37+
grammar, indent=indent, proto_messages=proto_messages
38+
)
3539
lines = []
3640
for defn in defns:
3741
lines.append("")

meta/src/meta/parser_gen_go.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ def generate_parser_go(
1414
) -> str:
1515
"""Generate LL(k) recursive-descent parser in Go."""
1616
codegen = GoCodeGenerator(proto_messages=proto_messages)
17-
return generate_parser(grammar, codegen, _TEMPLATE_PATH, command_line)
17+
return generate_parser(
18+
grammar, codegen, _TEMPLATE_PATH, command_line, proto_messages
19+
)
1820

1921

2022
__all__ = [

meta/src/meta/parser_gen_julia.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ def generate_parser_julia(
1515
) -> str:
1616
"""Generate LL(k) recursive-descent parser in Julia."""
1717
codegen = JuliaCodeGenerator(proto_messages=proto_messages, config=PARSER_CONFIG)
18-
return generate_parser(grammar, codegen, _TEMPLATE_PATH, command_line)
18+
return generate_parser(
19+
grammar, codegen, _TEMPLATE_PATH, command_line, proto_messages
20+
)
1921

2022

2123
__all__ = [

meta/src/meta/parser_gen_python.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ def generate_parser_python(
1414
) -> str:
1515
"""Generate LL(k) recursive-descent parser in Python."""
1616
codegen = PythonCodeGenerator(proto_messages=proto_messages)
17-
return generate_parser(grammar, codegen, _TEMPLATE_PATH, command_line)
17+
return generate_parser(
18+
grammar, codegen, _TEMPLATE_PATH, command_line, proto_messages
19+
)

meta/src/meta/target_builtins.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ def is_builtin(name: str) -> bool:
249249
# === Sequence indexing ===
250250
register_builtin("get_at", [SequenceType(T), INT64], T)
251251

252+
# === Provenance tracking ===
253+
register_builtin("span_start", [], INT64)
254+
register_builtin("record_span", [INT64, STRING], VOID)
255+
252256

253257
# === Validation functions ===
254258

0 commit comments

Comments
 (0)