harn_parser/parser/
contextual.rs1use harn_lexer::{Token, TokenKind, CONTEXTUAL_STATEMENT_KEYWORDS};
10
11#[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
25pub(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
60fn 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
105fn 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}