Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions crates/plotnik-lib/src/diagnostics/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub enum DiagnosticKind {
EmptyTree,
BareIdentifier,
InvalidSeparator,
AnchorInAlternation,
InvalidFieldEquals,
InvalidSupertypeSyntax,
InvalidTypeAnnotationSyntax,
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions crates/plotnik-lib/src/parser/grammar/structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
68 changes: 68 additions & 0 deletions crates/plotnik-lib/src/query/anchors_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
");
}
12 changes: 11 additions & 1 deletion docs/lang-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down