Skip to content

Commit 342e33f

Browse files
authored
feat: add anchor placement validation (#198)
1 parent 160be4c commit 342e33f

5 files changed

Lines changed: 93 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ Rule: anchor is as strict as its strictest operand.
105105
106106
; WRONG: boundary anchors without parent node
107107
{. (a)} ; use (parent {. (a)})
108+
109+
; WRONG: anchors directly in alternations
110+
[(a) . (b)] ; use [{(a) . (b)} (c)]
108111
```
109112

110113
## Type System Rules

crates/plotnik-lib/src/diagnostics/message.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub enum DiagnosticKind {
3232
EmptyTree,
3333
BareIdentifier,
3434
InvalidSeparator,
35+
AnchorInAlternation,
3536
InvalidFieldEquals,
3637
InvalidSupertypeSyntax,
3738
InvalidTypeAnnotationSyntax,
@@ -151,6 +152,9 @@ impl DiagnosticKind {
151152
Self::AnchorWithoutContext => {
152153
Some("wrap in a named node: `(parent . (child))`")
153154
}
155+
Self::AnchorInAlternation => {
156+
Some("use `[{(a) . (b)} (c)]` to anchor within a branch")
157+
}
154158
_ => None,
155159
}
156160
}
@@ -174,6 +178,7 @@ impl DiagnosticKind {
174178
Self::EmptyTree => "empty `()` is not allowed",
175179
Self::BareIdentifier => "bare identifier is not valid",
176180
Self::InvalidSeparator => "unexpected separator",
181+
Self::AnchorInAlternation => "anchors cannot appear directly in alternations",
177182
Self::InvalidFieldEquals => "use `:` instead of `=`",
178183
Self::InvalidSupertypeSyntax => "references cannot have supertypes",
179184
Self::InvalidTypeAnnotationSyntax => "use `::` for type annotations",

crates/plotnik-lib/src/parser/grammar/structures.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ impl Parser<'_, '_> {
277277
}
278278
continue;
279279
}
280+
// Anchors cannot appear directly in alternations - they create empty branches
281+
if self.currently_is(SyntaxKind::Dot) {
282+
self.error(DiagnosticKind::AnchorInAlternation);
283+
self.skip_token();
284+
continue;
285+
}
280286
if self.currently_is_one_of(EXPR_FIRST_TOKENS) {
281287
self.start_node(SyntaxKind::Branch);
282288
self.parse_expr();

crates/plotnik-lib/src/query/anchors_tests.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,71 @@ fn nested_named_node_provides_context() {
169169
NamedNode first
170170
");
171171
}
172+
173+
// Parser-level: anchors in alternations
174+
175+
#[test]
176+
fn anchor_in_alternation_error() {
177+
let input = "Q = [(a) . (b)]";
178+
179+
let res = Query::expect_invalid(input);
180+
181+
insta::assert_snapshot!(res, @r"
182+
error: anchors cannot appear directly in alternations
183+
|
184+
1 | Q = [(a) . (b)]
185+
| ^
186+
|
187+
help: use `[{(a) . (b)} (c)]` to anchor within a branch
188+
");
189+
}
190+
191+
#[test]
192+
fn multiple_anchors_in_alternation_error() {
193+
let input = "Q = [. (a) . (b) .]";
194+
195+
let res = Query::expect_invalid(input);
196+
197+
insta::assert_snapshot!(res, @r"
198+
error: anchors cannot appear directly in alternations
199+
|
200+
1 | Q = [. (a) . (b) .]
201+
| ^
202+
|
203+
help: use `[{(a) . (b)} (c)]` to anchor within a branch
204+
205+
error: anchors cannot appear directly in alternations
206+
|
207+
1 | Q = [. (a) . (b) .]
208+
| ^
209+
|
210+
help: use `[{(a) . (b)} (c)]` to anchor within a branch
211+
212+
error: anchors cannot appear directly in alternations
213+
|
214+
1 | Q = [. (a) . (b) .]
215+
| ^
216+
|
217+
help: use `[{(a) . (b)} (c)]` to anchor within a branch
218+
");
219+
}
220+
221+
#[test]
222+
fn anchor_in_seq_inside_alt_ok() {
223+
let input = "Q = [{(a) . (b)} (c)]";
224+
225+
let res = Query::expect_valid_ast(input);
226+
227+
insta::assert_snapshot!(res, @r"
228+
Root
229+
Def Q
230+
Alt
231+
Branch
232+
Seq
233+
NamedNode a
234+
.
235+
NamedNode b
236+
Branch
237+
NamedNode c
238+
");
239+
}

docs/lang-reference.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,9 +783,19 @@ Anchors require parent node context to be meaningful:
783783
Q = . (a) ; definition level (no parent node)
784784
Q = {. (a)} ; sequence boundary without parent
785785
Q = {(a) .} ; sequence boundary without parent
786+
Q = [(a) . (b)] ; directly in alternation
786787
```
787788

788-
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.
789+
To anchor within alternation branches, wrap in a sequence:
790+
791+
```
792+
Q = [{(a) . (b)} (c)] ; valid: anchor inside sequence branch
793+
```
794+
795+
The rules:
796+
- **Boundary anchors** (at start/end of sequence) need a parent named node to provide first/last child or adjacent sibling semantics
797+
- **Interior anchors** (between items in a sequence) are always valid because both sides are explicitly defined
798+
- **Alternations** cannot contain anchors directly—anchors must be inside a branch expression
789799

790800
---
791801

0 commit comments

Comments
 (0)