Skip to main content

harn_parser/parser/
contextual.rs

1//! Contextual keywords: words that carry syntax only in one position.
2//!
3//! `tool`, `select`, `require`, and the other words in
4//! [`harn_lexer::CONTEXTUAL_KEYWORDS`] lex as identifiers. The rules here
5//! decide, from the surrounding tokens alone, when one introduces its
6//! construct. The parser and the language server both read them, so editor
7//! highlighting cannot disagree with the parse.
8
9use harn_lexer::{Token, TokenKind, CONTEXTUAL_STATEMENT_KEYWORDS};
10
11/// The contextual statement keyword introduced by the identifier at `pos`,
12/// if any.
13#[must_use]
14pub fn contextual_statement_at(tokens: &[Token], pos: usize) -> Option<&'static str> {
15    let Some(TokenKind::Identifier(name)) = tokens.get(pos).map(|token| &token.kind) else {
16        return None;
17    };
18    let word = CONTEXTUAL_STATEMENT_KEYWORDS
19        .iter()
20        .copied()
21        .find(|word| *word == name)?;
22    introduces_construct(tokens, pos, word).then_some(word)
23}
24
25/// Whether the identifier `name` at `pos` introduces its construct.
26///
27/// Every rule accepts exactly the token sequences the construct accepted
28/// while its word was reserved, so a program that parsed before still parses
29/// the same way. Sequences that were syntax errors then, such as
30/// `select = 1` or `tool.name`, now read the word as an identifier.
31pub(super) fn introduces_construct(tokens: &[Token], pos: usize, name: &str) -> bool {
32    if !matches!(tokens.get(pos).map(|t| &t.kind), Some(TokenKind::Identifier(word)) if word == name)
33    {
34        return false;
35    }
36    let next = tokens.get(pos + 1).map(|t| &t.kind);
37    match name {
38        "select" | "scope" | "block" | "cost_route" => matches!(next, Some(TokenKind::LBrace)),
39        "tool" | "skill" | "eval_pack" => matches!(next, Some(TokenKind::Identifier(_))),
40        "mutex" => match next {
41            Some(TokenKind::LBrace) => true,
42            Some(TokenKind::LParen) => paren_group_precedes_brace(tokens, pos + 1),
43            _ => false,
44        },
45        "deadline" => match next {
46            Some(
47                TokenKind::DurationLiteral(_)
48                | TokenKind::IntLiteral(_)
49                | TokenKind::FloatLiteral(_)
50                | TokenKind::Identifier(_),
51            ) => true,
52            Some(TokenKind::LParen) => paren_group_precedes_brace(tokens, pos + 1),
53            _ => false,
54        },
55        "require" | "emit" => next.is_some_and(|kind| !continues_value_expression(kind)),
56        _ => false,
57    }
58}
59
60/// Tokens that can follow an identifier inside an expression or an
61/// assignment target, or that end it.
62fn continues_value_expression(kind: &TokenKind) -> bool {
63    matches!(
64        kind,
65        TokenKind::Newline
66            | TokenKind::Eof
67            | TokenKind::Semicolon
68            | TokenKind::Comma
69            | TokenKind::Colon
70            | TokenKind::RParen
71            | TokenKind::RBracket
72            | TokenKind::RBrace
73            | TokenKind::Dot
74            | TokenKind::QuestionDot
75            | TokenKind::Question
76            | TokenKind::Assign
77            | TokenKind::PlusAssign
78            | TokenKind::MinusAssign
79            | TokenKind::StarAssign
80            | TokenKind::SlashAssign
81            | TokenKind::PercentAssign
82            | TokenKind::Eq
83            | TokenKind::Neq
84            | TokenKind::And
85            | TokenKind::Or
86            | TokenKind::Pipe
87            | TokenKind::NilCoal
88            | TokenKind::Pow
89            | TokenKind::Lt
90            | TokenKind::Gt
91            | TokenKind::Lte
92            | TokenKind::Gte
93            | TokenKind::Plus
94            | TokenKind::Star
95            | TokenKind::Slash
96            | TokenKind::Percent
97            | TokenKind::In
98            | TokenKind::DotDot
99            | TokenKind::DotDotEq
100            | TokenKind::LineComment { .. }
101            | TokenKind::BlockComment { .. }
102    )
103}
104
105/// Whether the `(` at `open` closes before a `{`.
106fn paren_group_precedes_brace(tokens: &[Token], open: usize) -> bool {
107    let mut depth = 0usize;
108    for (index, token) in tokens.iter().enumerate().skip(open) {
109        match token.kind {
110            TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => depth += 1,
111            TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
112                depth = depth.saturating_sub(1);
113                if depth == 0 {
114                    return matches!(
115                        tokens.get(index + 1).map(|t| &t.kind),
116                        Some(TokenKind::LBrace)
117                    );
118                }
119            }
120            TokenKind::Eof => return false,
121            _ => {}
122        }
123    }
124    false
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use harn_lexer::{Lexer, CONTEXTUAL_KEYWORDS};
131
132    #[test]
133    fn every_contextual_statement_is_a_contextual_keyword() {
134        for word in CONTEXTUAL_STATEMENT_KEYWORDS {
135            assert!(
136                CONTEXTUAL_KEYWORDS.contains(word),
137                "`{word}` must be listed in harn_lexer::CONTEXTUAL_KEYWORDS"
138            );
139        }
140    }
141
142    #[test]
143    fn statement_words_follow_their_construct_shape() {
144        let at = |source: &str| {
145            let tokens = Lexer::new(source).tokenize().unwrap();
146            contextual_statement_at(&tokens, 0)
147        };
148        assert_eq!(at("select { }"), Some("select"));
149        assert_eq!(at("select = 1"), None);
150        assert_eq!(at("tool lookup(q: string) { }"), Some("tool"));
151        assert_eq!(at("tool.name"), None);
152        assert_eq!(at("require x > 0"), Some("require"));
153        assert_eq!(at("require = 1"), None);
154        assert_eq!(at("mutex(key) { }"), Some("mutex"));
155        assert_eq!(at("mutex(key)"), None);
156        assert_eq!(at("deadline 5s { }"), Some("deadline"));
157        assert_eq!(at("deadline + 1"), None);
158    }
159}