diff --git a/AGENTS.md b/AGENTS.md index 286196a5..832078fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,9 @@ Rule: anchor is as strict as its strictest operand. ; WRONG: boundary anchors without parent node {. (a)} ; use (parent {. (a)}) + +; WRONG: anchors directly in alternations +[(a) . (b)] ; use [{(a) . (b)} (c)] ``` ## Type System Rules diff --git a/crates/plotnik-lib/src/diagnostics/message.rs b/crates/plotnik-lib/src/diagnostics/message.rs index 0b8101a3..5f3a93fa 100644 --- a/crates/plotnik-lib/src/diagnostics/message.rs +++ b/crates/plotnik-lib/src/diagnostics/message.rs @@ -32,6 +32,7 @@ pub enum DiagnosticKind { EmptyTree, BareIdentifier, InvalidSeparator, + AnchorInAlternation, InvalidFieldEquals, InvalidSupertypeSyntax, InvalidTypeAnnotationSyntax, @@ -151,6 +152,9 @@ impl DiagnosticKind { Self::AnchorWithoutContext => { Some("wrap in a named node: `(parent . (child))`") } + Self::AnchorInAlternation => { + Some("use `[{(a) . (b)} (c)]` to anchor within a branch") + } _ => None, } } @@ -174,6 +178,7 @@ impl DiagnosticKind { Self::EmptyTree => "empty `()` is not allowed", Self::BareIdentifier => "bare identifier is not valid", Self::InvalidSeparator => "unexpected separator", + Self::AnchorInAlternation => "anchors cannot appear directly in alternations", Self::InvalidFieldEquals => "use `:` instead of `=`", Self::InvalidSupertypeSyntax => "references cannot have supertypes", Self::InvalidTypeAnnotationSyntax => "use `::` for type annotations", diff --git a/crates/plotnik-lib/src/parser/grammar/structures.rs b/crates/plotnik-lib/src/parser/grammar/structures.rs index c0a243af..6252ef06 100644 --- a/crates/plotnik-lib/src/parser/grammar/structures.rs +++ b/crates/plotnik-lib/src/parser/grammar/structures.rs @@ -277,6 +277,12 @@ impl Parser<'_, '_> { } continue; } + // Anchors cannot appear directly in alternations - they create empty branches + if self.currently_is(SyntaxKind::Dot) { + self.error(DiagnosticKind::AnchorInAlternation); + self.skip_token(); + continue; + } if self.currently_is_one_of(EXPR_FIRST_TOKENS) { self.start_node(SyntaxKind::Branch); self.parse_expr(); diff --git a/crates/plotnik-lib/src/query/anchors_tests.rs b/crates/plotnik-lib/src/query/anchors_tests.rs index 9fb025b9..26261cfa 100644 --- a/crates/plotnik-lib/src/query/anchors_tests.rs +++ b/crates/plotnik-lib/src/query/anchors_tests.rs @@ -169,3 +169,71 @@ fn nested_named_node_provides_context() { NamedNode first "); } + +// Parser-level: anchors in alternations + +#[test] +fn anchor_in_alternation_error() { + let input = "Q = [(a) . (b)]"; + + let res = Query::expect_invalid(input); + + insta::assert_snapshot!(res, @r" + error: anchors cannot appear directly in alternations + | + 1 | Q = [(a) . (b)] + | ^ + | + help: use `[{(a) . (b)} (c)]` to anchor within a branch + "); +} + +#[test] +fn multiple_anchors_in_alternation_error() { + let input = "Q = [. (a) . (b) .]"; + + let res = Query::expect_invalid(input); + + insta::assert_snapshot!(res, @r" + error: anchors cannot appear directly in alternations + | + 1 | Q = [. (a) . (b) .] + | ^ + | + help: use `[{(a) . (b)} (c)]` to anchor within a branch + + error: anchors cannot appear directly in alternations + | + 1 | Q = [. (a) . (b) .] + | ^ + | + help: use `[{(a) . (b)} (c)]` to anchor within a branch + + error: anchors cannot appear directly in alternations + | + 1 | Q = [. (a) . (b) .] + | ^ + | + help: use `[{(a) . (b)} (c)]` to anchor within a branch + "); +} + +#[test] +fn anchor_in_seq_inside_alt_ok() { + let input = "Q = [{(a) . (b)} (c)]"; + + let res = Query::expect_valid_ast(input); + + insta::assert_snapshot!(res, @r" + Root + Def Q + Alt + Branch + Seq + NamedNode a + . + NamedNode b + Branch + NamedNode c + "); +} diff --git a/docs/lang-reference.md b/docs/lang-reference.md index 1cabd67a..8fab903f 100644 --- a/docs/lang-reference.md +++ b/docs/lang-reference.md @@ -783,9 +783,19 @@ Anchors require parent node context to be meaningful: Q = . (a) ; definition level (no parent node) Q = {. (a)} ; sequence boundary without parent Q = {(a) .} ; sequence boundary without parent +Q = [(a) . (b)] ; directly in alternation ``` -The rule: **boundary anchors need a parent named node** to provide first/last child or adjacent sibling semantics. Interior anchors (between items in a sequence) are always valid because both sides are explicitly defined. +To anchor within alternation branches, wrap in a sequence: + +``` +Q = [{(a) . (b)} (c)] ; valid: anchor inside sequence branch +``` + +The rules: +- **Boundary anchors** (at start/end of sequence) need a parent named node to provide first/last child or adjacent sibling semantics +- **Interior anchors** (between items in a sequence) are always valid because both sides are explicitly defined +- **Alternations** cannot contain anchors directly—anchors must be inside a branch expression ---