feat(compiler): parse + typecheck undirected traversal — $a <edge> $b

iss-gq-undirected-traversal, the Expand-internal design (no plan-level
Union; unblocked from iss-744/579): the grammar gains an undirected_edge
alternative in the traversal rule (angle brackets are collision-free —
comparisons live in the structurally separate filter production), the
AST Traversal carries `undirected`, Direction gains `Both`, and
typecheck resolves undirected patterns to Both after the new T22 rule:
undirected requires a same-endpoint-type edge (an asymmetric edge is
well-typed in at most one orientation, so the form is rejected with
guidance to use the directional pattern). Lowering passes Both through;
the reverse-expand orientation flip is a no-op for a symmetric
traversal. Bounds ({min,max}) and not{} compose unchanged.

Direction has no serde derives and IR never crosses a wire — no
compatibility surface. Parser/typecheck tests cover the bare, bounded,
inside-not forms, Both resolution on Knows (Person->Person), and the
T22 rejection on WorksAt (Person->Company).
This commit is contained in:
aaltshuler 2026-07-05 22:46:08 +03:00 committed by Andrew Altshuler
parent db217e7db2
commit d4b21ce4eb
8 changed files with 125 additions and 4 deletions

View file

@ -427,7 +427,15 @@ fn parse_traversal(pair: pest::iterators::Pair<Rule>) -> Result<Traversal> {
let mut inner = pair.into_inner();
let src_var = inner.next().unwrap().as_str();
let src = src_var.strip_prefix('$').unwrap_or(src_var).to_string();
let edge_name = inner.next().unwrap().as_str().to_string();
let edge_pair = inner.next().unwrap();
let (edge_name, undirected) = match edge_pair.as_rule() {
// `<edge>` — the inner edge_ident carries the name.
Rule::undirected_edge => (
edge_pair.into_inner().next().unwrap().as_str().to_string(),
true,
),
_ => (edge_pair.as_str().to_string(), false),
};
let mut min_hops = 1u32;
let mut max_hops = Some(1u32);
@ -452,6 +460,7 @@ fn parse_traversal(pair: pest::iterators::Pair<Rule>) -> Result<Traversal> {
dst,
min_hops,
max_hops,
undirected,
})
}