use super::{Input, Refused, Source, Span, Spanned, Stage, bare, first, spans};
#[derive(Debug, PartialEq, Eq)]
pub struct Node {
pub binding: Spanned<String>,
pub label: Spanned<String>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Outgoing,
Incoming,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Relation {
pub kind: Spanned<String>,
pub direction: Direction,
pub span: Span,
}
pub(super) fn read(text: &str, start: usize) -> Result<Spanned<Stage>, Refused> {
let rest = text.trim_start();
let span = spans::stage(rest, start + text.len() - rest.len());
let text = rest.split_at(span.end - span.start).0;
let mut input = Input { text, rest: text };
if input.word() != "match" {
return Err(refuse(&input, span.start, "the `match` keyword"));
}
input.advance(5);
input.trim();
if !input.rest.starts_with('(') {
return Err(Refused::NotYet(
"a `match` stage without a labelled node",
Span {
start: span.start,
end: span.start + 5,
},
));
}
let mut nodes = vec![node(&mut input, span.start)?];
input.trim();
let mut relations = Vec::new();
while !input.rest.is_empty() {
suffix(&input, span.start)?;
let relation = relation(&mut input, span.start)?;
if relations.len() == 7 {
return Err(Refused::NotYet(
"more than seven hops in `match`",
relation.span,
));
}
relations.push(relation);
nodes.push(node(&mut input, span.start)?);
input.trim();
}
Ok(Spanned {
value: Stage::Match { nodes, relations },
span,
})
}
pub(super) fn position(
stage: &Stage,
source: &Source,
preceding: usize,
span: Span,
) -> Result<(), Refused> {
let Stage::Match { nodes, .. } = stage else {
return Ok(());
};
if matches!(source, Source::Shaped(roots) if roots.len() > 1) {
return Err(Refused::NotYet(
"`match` after a shape with several roots",
span,
));
}
if preceding != 0 {
return Err(Refused::NotYet(
"`match` anywhere but first or immediately after its collection",
span,
));
}
let Some(node) = nodes.first() else {
return Err(Refused::NotYet("a `match` node with no label", span));
};
if let Source::Collection { name, .. } = source
&& *name != node.label.value
{
return Err(Refused::NotYet(
"`match` after a collection other than its first node's label",
node.label.span,
));
}
Ok(())
}
fn node(input: &mut Input<'_>, start: usize) -> Result<Node, Refused> {
input.trim();
let begin = start + input.position();
if !input.take("(") {
return Err(refuse(input, start, "a `match` node pattern"));
}
input.trim();
let binding = binding(input, start)?;
input.trim();
if input.rest.starts_with('{') {
return Err(refuse(input, start, "a `match` property map"));
}
if !input.take(":") {
return Err(refuse(input, start, "a `match` node with no label"));
}
input.trim();
let label = name(input, start, "a `match` node with no label")?;
input.trim();
if input.rest.starts_with('{') {
return Err(refuse(input, start, "a `match` property map"));
}
if input.rest.starts_with(':') {
return Err(refuse(input, start, "multiple labels on a `match` node"));
}
if !input.take(")") {
return Err(refuse(input, start, "an unclosed node in `match`"));
}
Ok(Node {
binding,
label,
span: Span {
start: begin,
end: start + input.position(),
},
})
}
fn binding(input: &mut Input<'_>, start: usize) -> Result<Spanned<String>, Refused> {
let length = input
.rest
.chars()
.take_while(|character| {
!character.is_whitespace() && !matches!(*character, ':' | ')' | '(' | '{' | '[' | ']')
})
.map(char::len_utf8)
.sum();
let value = input.rest.split_at(length).0;
let valid = value
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_')
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_');
if !valid {
let token = input.token_span();
return Err(Refused::NotYet(
"a `match` binding name outside [a-z_][a-z0-9_]*",
Span {
start: start + token.start,
end: start + token.start + length.max(token.end - token.start),
},
));
}
let span = Span {
start: start + input.position(),
end: start + input.position() + length,
};
let value = value.to_owned();
input.advance(length);
Ok(Spanned { value, span })
}
fn name(
input: &mut Input<'_>,
start: usize,
missing: &'static str,
) -> Result<Spanned<String>, Refused> {
let length = input
.rest
.chars()
.take_while(|character| bare(*character))
.map(char::len_utf8)
.sum();
let value = input.rest.split_at(length).0;
if !value.starts_with(first) {
return Err(refuse(input, start, missing));
}
let span = Span {
start: start + input.position(),
end: start + input.position() + length,
};
let value = value.to_owned();
input.advance(length);
Ok(Spanned { value, span })
}
fn relation(input: &mut Input<'_>, start: usize) -> Result<Relation, Refused> {
let begin = start + input.position();
let direction = if input.take("<-") {
Direction::Incoming
} else if input.take("-") {
Direction::Outgoing
} else {
return Err(refuse(input, start, "an unsupported `match` suffix"));
};
input.trim();
if !input.take("[") {
return Err(refuse(input, start, "a `match` relation with no kind"));
}
input.trim();
if !input.take(":") {
let construct = if input.rest.starts_with(']') {
"a `match` relation with no kind"
} else {
"a `match` relation binding"
};
return Err(refuse(input, start, construct));
}
input.trim();
let kind = name(input, start, "a `match` relation with no kind")?;
input.trim();
if input.rest.starts_with('*') {
return Err(refuse(input, start, "a variable-length `match` relation"));
}
if input.rest.starts_with('{') {
return Err(refuse(input, start, "a `match` property map"));
}
if !input.take("]") {
return Err(refuse(input, start, "an unclosed `match` relation kind"));
}
input.trim();
let arrow = match direction {
Direction::Outgoing => "->",
Direction::Incoming => "-",
};
if direction == Direction::Incoming && input.rest.starts_with("->") {
return Err(refuse(input, start, "a bidirectional `match` relation"));
}
if !input.take(arrow) {
return Err(refuse(
input,
start,
"an undirected or incomplete `match` arrow",
));
}
Ok(Relation {
kind,
direction,
span: Span {
start: begin,
end: start + input.position(),
},
})
}
fn suffix(input: &Input<'_>, start: usize) -> Result<(), Refused> {
let construct = if input.word().eq_ignore_ascii_case("where") {
Some("a `match` WHERE clause")
} else if input.rest.starts_with('{') {
Some("a `match` property map")
} else if input.rest.starts_with(',') {
Some("multiple patterns in `match`")
} else if input.rest.starts_with('(') {
Some("two `match` nodes without a relation")
} else {
None
};
construct.map_or(Ok(()), |construct| Err(refuse(input, start, construct)))
}
fn refuse(input: &Input<'_>, start: usize, construct: &'static str) -> Refused {
let span = input.token_span();
Refused::NotYet(
construct,
Span {
start: start + span.start,
end: start + span.end,
},
)
}
#[cfg(test)]
#[path = "match_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "match_contract_tests.rs"]
mod contract_tests;
#[cfg(test)]
#[path = "match_library_tests.rs"]
mod library_tests;
#[cfg(test)]
#[path = "match_chain_tests.rs"]
mod chain_tests;