Skip to main content

haematite_cli/serve/query/parser/
match.rs

1//! A labelled node or fixed chain of directed hops, with byte locations for every name.
2
3use super::{Input, Refused, Source, Span, Spanned, Stage, bare, first, spans};
4
5#[derive(Debug, PartialEq, Eq)]
6pub struct Node {
7    pub binding: Spanned<String>,
8    pub label: Spanned<String>,
9    pub span: Span,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Direction {
14    Outgoing,
15    Incoming,
16}
17
18#[derive(Debug, PartialEq, Eq)]
19pub struct Relation {
20    pub kind: Spanned<String>,
21    pub direction: Direction,
22    pub span: Span,
23}
24
25/// Read only the outer stage; a following pipe belongs to the pipeline.
26pub(super) fn read(text: &str, start: usize) -> Result<Spanned<Stage>, Refused> {
27    let rest = text.trim_start();
28    let span = spans::stage(rest, start + text.len() - rest.len());
29    let text = rest.split_at(span.end - span.start).0;
30    let mut input = Input { text, rest: text };
31    if input.word() != "match" {
32        return Err(refuse(&input, span.start, "the `match` keyword"));
33    }
34    input.advance(5);
35    input.trim();
36    if !input.rest.starts_with('(') {
37        return Err(Refused::NotYet(
38            "a `match` stage without a labelled node",
39            Span {
40                start: span.start,
41                end: span.start + 5,
42            },
43        ));
44    }
45    let mut nodes = vec![node(&mut input, span.start)?];
46    input.trim();
47    let mut relations = Vec::new();
48    while !input.rest.is_empty() {
49        suffix(&input, span.start)?;
50        let relation = relation(&mut input, span.start)?;
51        if relations.len() == 7 {
52            return Err(Refused::NotYet(
53                "more than seven hops in `match`",
54                relation.span,
55            ));
56        }
57        relations.push(relation);
58        nodes.push(node(&mut input, span.start)?);
59        input.trim();
60    }
61    Ok(Spanned {
62        value: Stage::Match { nodes, relations },
63        span,
64    })
65}
66
67/// A match takes records only from its own label and precedes other stages.
68pub(super) fn position(
69    stage: &Stage,
70    source: &Source,
71    preceding: usize,
72    span: Span,
73) -> Result<(), Refused> {
74    let Stage::Match { nodes, .. } = stage else {
75        return Ok(());
76    };
77    if matches!(source, Source::Shaped(roots) if roots.len() > 1) {
78        return Err(Refused::NotYet(
79            "`match` after a shape with several roots",
80            span,
81        ));
82    }
83    if preceding != 0 {
84        return Err(Refused::NotYet(
85            "`match` anywhere but first or immediately after its collection",
86            span,
87        ));
88    }
89    let Some(node) = nodes.first() else {
90        return Err(Refused::NotYet("a `match` node with no label", span));
91    };
92    if let Source::Collection { name, .. } = source
93        && *name != node.label.value
94    {
95        return Err(Refused::NotYet(
96            "`match` after a collection other than its first node's label",
97            node.label.span,
98        ));
99    }
100    Ok(())
101}
102
103fn node(input: &mut Input<'_>, start: usize) -> Result<Node, Refused> {
104    input.trim();
105    let begin = start + input.position();
106    if !input.take("(") {
107        return Err(refuse(input, start, "a `match` node pattern"));
108    }
109    input.trim();
110    let binding = binding(input, start)?;
111    input.trim();
112    if input.rest.starts_with('{') {
113        return Err(refuse(input, start, "a `match` property map"));
114    }
115    if !input.take(":") {
116        return Err(refuse(input, start, "a `match` node with no label"));
117    }
118    input.trim();
119    let label = name(input, start, "a `match` node with no label")?;
120    input.trim();
121    if input.rest.starts_with('{') {
122        return Err(refuse(input, start, "a `match` property map"));
123    }
124    if input.rest.starts_with(':') {
125        return Err(refuse(input, start, "multiple labels on a `match` node"));
126    }
127    if !input.take(")") {
128        return Err(refuse(input, start, "an unclosed node in `match`"));
129    }
130    Ok(Node {
131        binding,
132        label,
133        span: Span {
134            start: begin,
135            end: start + input.position(),
136        },
137    })
138}
139
140fn binding(input: &mut Input<'_>, start: usize) -> Result<Spanned<String>, Refused> {
141    let length = input
142        .rest
143        .chars()
144        .take_while(|character| {
145            !character.is_whitespace() && !matches!(*character, ':' | ')' | '(' | '{' | '[' | ']')
146        })
147        .map(char::len_utf8)
148        .sum();
149    let value = input.rest.split_at(length).0;
150    let valid = value
151        .bytes()
152        .next()
153        .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_')
154        && value
155            .bytes()
156            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_');
157    if !valid {
158        let token = input.token_span();
159        return Err(Refused::NotYet(
160            "a `match` binding name outside [a-z_][a-z0-9_]*",
161            Span {
162                start: start + token.start,
163                end: start + token.start + length.max(token.end - token.start),
164            },
165        ));
166    }
167    let span = Span {
168        start: start + input.position(),
169        end: start + input.position() + length,
170    };
171    let value = value.to_owned();
172    input.advance(length);
173    Ok(Spanned { value, span })
174}
175
176fn name(
177    input: &mut Input<'_>,
178    start: usize,
179    missing: &'static str,
180) -> Result<Spanned<String>, Refused> {
181    let length = input
182        .rest
183        .chars()
184        .take_while(|character| bare(*character))
185        .map(char::len_utf8)
186        .sum();
187    let value = input.rest.split_at(length).0;
188    if !value.starts_with(first) {
189        return Err(refuse(input, start, missing));
190    }
191    let span = Span {
192        start: start + input.position(),
193        end: start + input.position() + length,
194    };
195    let value = value.to_owned();
196    input.advance(length);
197    Ok(Spanned { value, span })
198}
199
200fn relation(input: &mut Input<'_>, start: usize) -> Result<Relation, Refused> {
201    let begin = start + input.position();
202    let direction = if input.take("<-") {
203        Direction::Incoming
204    } else if input.take("-") {
205        Direction::Outgoing
206    } else {
207        return Err(refuse(input, start, "an unsupported `match` suffix"));
208    };
209    input.trim();
210    if !input.take("[") {
211        return Err(refuse(input, start, "a `match` relation with no kind"));
212    }
213    input.trim();
214    if !input.take(":") {
215        let construct = if input.rest.starts_with(']') {
216            "a `match` relation with no kind"
217        } else {
218            "a `match` relation binding"
219        };
220        return Err(refuse(input, start, construct));
221    }
222    input.trim();
223    let kind = name(input, start, "a `match` relation with no kind")?;
224    input.trim();
225    if input.rest.starts_with('*') {
226        return Err(refuse(input, start, "a variable-length `match` relation"));
227    }
228    if input.rest.starts_with('{') {
229        return Err(refuse(input, start, "a `match` property map"));
230    }
231    if !input.take("]") {
232        return Err(refuse(input, start, "an unclosed `match` relation kind"));
233    }
234    input.trim();
235    let arrow = match direction {
236        Direction::Outgoing => "->",
237        Direction::Incoming => "-",
238    };
239    if direction == Direction::Incoming && input.rest.starts_with("->") {
240        return Err(refuse(input, start, "a bidirectional `match` relation"));
241    }
242    if !input.take(arrow) {
243        return Err(refuse(
244            input,
245            start,
246            "an undirected or incomplete `match` arrow",
247        ));
248    }
249    Ok(Relation {
250        kind,
251        direction,
252        span: Span {
253            start: begin,
254            end: start + input.position(),
255        },
256    })
257}
258
259fn suffix(input: &Input<'_>, start: usize) -> Result<(), Refused> {
260    let construct = if input.word().eq_ignore_ascii_case("where") {
261        Some("a `match` WHERE clause")
262    } else if input.rest.starts_with('{') {
263        Some("a `match` property map")
264    } else if input.rest.starts_with(',') {
265        Some("multiple patterns in `match`")
266    } else if input.rest.starts_with('(') {
267        Some("two `match` nodes without a relation")
268    } else {
269        None
270    };
271    construct.map_or(Ok(()), |construct| Err(refuse(input, start, construct)))
272}
273
274fn refuse(input: &Input<'_>, start: usize, construct: &'static str) -> Refused {
275    let span = input.token_span();
276    Refused::NotYet(
277        construct,
278        Span {
279            start: start + span.start,
280            end: start + span.end,
281        },
282    )
283}
284
285#[cfg(test)]
286#[path = "match_tests.rs"]
287mod tests;
288
289#[cfg(test)]
290#[path = "match_contract_tests.rs"]
291mod contract_tests;
292
293#[cfg(test)]
294#[path = "match_library_tests.rs"]
295mod library_tests;
296
297#[cfg(test)]
298#[path = "match_chain_tests.rs"]
299mod chain_tests;