Skip to main content

lemma/parsing/
rules.rs

1use super::ast::{DepthTracker, Span};
2use super::Rule;
3use crate::error::LemmaError;
4use crate::semantic::*;
5use crate::Source;
6use pest::iterators::Pair;
7use std::sync::Arc;
8
9pub(crate) fn parse_rule_definition(
10    pair: Pair<Rule>,
11    depth_tracker: &mut DepthTracker,
12    attribute: &str,
13    doc_name: &str,
14) -> Result<LemmaRule, LemmaError> {
15    let span = Span::from_pest_span(pair.as_span());
16    let pair_str = pair.as_str();
17    let mut rule_name = None;
18    let mut rule_expression = None;
19
20    for inner_pair in pair.into_inner() {
21        match inner_pair.as_rule() {
22            Rule::rule_name => rule_name = Some(inner_pair.as_str().to_string()),
23            Rule::rule_expression => {
24                rule_expression = Some(parse_rule_expression(
25                    inner_pair,
26                    depth_tracker,
27                    attribute,
28                    doc_name,
29                )?)
30            }
31            _ => {}
32        }
33    }
34
35    let name = rule_name.ok_or_else(|| {
36        LemmaError::engine(
37            "Grammar error: rule_definition missing rule_name",
38            span.clone(),
39            attribute,
40            Arc::from(pair_str),
41            doc_name,
42            1,
43            None::<String>,
44        )
45    })?;
46    let (expression, unless_clauses) = rule_expression.ok_or_else(|| {
47        LemmaError::engine(
48            "Grammar error: rule_definition missing rule_expression",
49            span.clone(),
50            attribute,
51            Arc::from(pair_str),
52            doc_name,
53            1,
54            None::<String>,
55        )
56    })?;
57
58    Ok(LemmaRule {
59        name,
60        expression,
61        unless_clauses,
62        source_location: Some(Source::new(
63            attribute.to_string(),
64            span.clone(),
65            doc_name.to_string(),
66        )),
67    })
68}
69
70fn parse_rule_expression(
71    pair: Pair<Rule>,
72    depth_tracker: &mut DepthTracker,
73    attribute: &str,
74    doc_name: &str,
75) -> Result<(Expression, Vec<UnlessClause>), LemmaError> {
76    let span = Span::from_pest_span(pair.as_span());
77    let pair_str = pair.as_str();
78    let mut expression = None;
79    let mut unless_clauses = Vec::new();
80
81    for inner_pair in pair.into_inner() {
82        match inner_pair.as_rule() {
83            Rule::expression => {
84                expression = Some(crate::parsing::expressions::parse_expression(
85                    inner_pair,
86                    depth_tracker,
87                    attribute,
88                    doc_name,
89                )?);
90            }
91            Rule::veto_expression => {
92                expression = Some(parse_veto_expression(inner_pair, attribute, doc_name)?);
93            }
94            Rule::unless_statement => {
95                let unless_clause =
96                    parse_unless_statement(inner_pair, depth_tracker, attribute, doc_name)?;
97                unless_clauses.push(unless_clause);
98            }
99            _ => {}
100        }
101    }
102
103    let expr = expression.ok_or_else(|| {
104        LemmaError::engine(
105            "Grammar error: rule_expression missing expression",
106            span,
107            attribute,
108            Arc::from(pair_str),
109            doc_name,
110            1,
111            None::<String>,
112        )
113    })?;
114    Ok((expr, unless_clauses))
115}
116
117fn parse_veto_expression(
118    pair: Pair<Rule>,
119    attribute: &str,
120    doc_name: &str,
121) -> Result<Expression, LemmaError> {
122    let veto_span = Span::from_pest_span(pair.as_span());
123    // Pest grammar: ^"veto" ~ (SPACE+ ~ text_literal)?
124    // If text_literal child exists, extract the string content (without quotes)
125    let message = pair
126        .clone()
127        .into_inner()
128        .find(|p| p.as_rule() == Rule::text_literal)
129        .map(|string_pair| {
130            let content = string_pair.as_str();
131            content[1..content.len() - 1].to_string()
132        });
133    let kind = ExpressionKind::Veto(VetoExpression { message });
134    Ok(Expression::new(
135        kind,
136        Some(Source::new(
137            attribute.to_string(),
138            veto_span,
139            doc_name.to_string(),
140        )),
141    ))
142}
143
144fn parse_unless_statement(
145    pair: Pair<Rule>,
146    depth_tracker: &mut DepthTracker,
147    attribute: &str,
148    doc_name: &str,
149) -> Result<UnlessClause, LemmaError> {
150    let span = Span::from_pest_span(pair.as_span());
151    let mut condition = None;
152    let mut result = None;
153
154    for inner_pair in pair.clone().into_inner() {
155        match inner_pair.as_rule() {
156            Rule::expression => {
157                if condition.is_none() {
158                    condition = Some(crate::parsing::expressions::parse_expression(
159                        inner_pair,
160                        depth_tracker,
161                        attribute,
162                        doc_name,
163                    )?);
164                } else {
165                    result = Some(crate::parsing::expressions::parse_expression(
166                        inner_pair,
167                        depth_tracker,
168                        attribute,
169                        doc_name,
170                    )?);
171                }
172            }
173            Rule::veto_expression => {
174                result = Some(parse_veto_expression(inner_pair, attribute, doc_name)?);
175            }
176            _ => {}
177        }
178    }
179
180    let cond = condition.ok_or_else(|| {
181        LemmaError::engine(
182            "Grammar error: unless_statement missing condition",
183            span.clone(),
184            attribute,
185            Arc::from(pair.as_str()),
186            doc_name,
187            1,
188            None::<String>,
189        )
190    })?;
191    let res = result.ok_or_else(|| {
192        LemmaError::engine(
193            "Grammar error: unless_statement missing result",
194            span.clone(),
195            attribute,
196            Arc::from(pair.as_str()),
197            doc_name,
198            1,
199            None::<String>,
200        )
201    })?;
202
203    Ok(UnlessClause {
204        condition: cond,
205        result: res,
206        source_location: Some(Source::new(
207            attribute.to_string(),
208            span.clone(),
209            doc_name.to_string(),
210        )),
211    })
212}
213
214// ============================================================================
215// Tests
216// ============================================================================
217
218#[cfg(test)]
219mod tests {
220    use crate::parsing::parse;
221    use crate::{ExpressionKind, ResourceLimits, Value};
222
223    #[test]
224    fn parse_document_with_unless_clause_records_unless_clause() {
225        let input = r#"doc person
226rule is_active = service_started? and not service_ended?
227unless maintenance_mode then false"#;
228        let result = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
229        assert_eq!(result.len(), 1);
230        assert_eq!(result[0].rules.len(), 1);
231        assert_eq!(result[0].rules[0].unless_clauses.len(), 1);
232    }
233
234    #[test]
235    fn parse_multiple_unless_clauses_records_all_unless_clauses() {
236        let input = r#"doc test
237rule is_eligible = age >= 18 and has_license
238unless emergency_mode then true
239unless system_override then accept"#;
240
241        let result = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
242        assert_eq!(result.len(), 1);
243        assert_eq!(result[0].rules.len(), 1);
244        assert_eq!(result[0].rules[0].unless_clauses.len(), 2);
245    }
246
247    #[test]
248    fn parse_multiple_rules_in_document_preserves_rule_names() {
249        let input = r#"doc test
250rule is_adult = age >= 18
251rule is_senior = age >= 65
252rule is_minor = age < 18
253rule can_vote = age >= 18 and is_citizen"#;
254
255        let result = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
256        assert_eq!(result.len(), 1);
257        assert_eq!(result[0].rules.len(), 4);
258        assert_eq!(result[0].rules[0].name, "is_adult");
259        assert_eq!(result[0].rules[1].name, "is_senior");
260        assert_eq!(result[0].rules[2].name, "is_minor");
261        assert_eq!(result[0].rules[3].name, "can_vote");
262    }
263
264    #[test]
265    fn veto_in_unless_clauses_parses_with_message() {
266        let input = r#"doc test
267rule is_adult = age >= 18 unless age < 0 then veto "Age must be 0 or higher""#;
268        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
269        assert_eq!(docs.len(), 1);
270        assert_eq!(docs[0].rules.len(), 1);
271
272        let rule = &docs[0].rules[0];
273        assert_eq!(rule.name, "is_adult");
274        assert_eq!(rule.unless_clauses.len(), 1);
275
276        match &rule.unless_clauses[0].result.kind {
277            ExpressionKind::Veto(veto) => {
278                assert_eq!(veto.message, Some("Age must be 0 or higher".to_string()));
279            }
280            other => panic!("Expected veto expression, got {:?}", other),
281        }
282
283        let input = r#"doc test
284rule is_adult = age >= 18
285  unless age > 150 then veto "Age cannot be over 150"
286  unless age < 0 then veto "Age must be 0 or higher""#;
287        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
288        let rule = &docs[0].rules[0];
289        assert_eq!(rule.unless_clauses.len(), 2);
290
291        match &rule.unless_clauses[0].result.kind {
292            ExpressionKind::Veto(veto) => {
293                assert_eq!(veto.message, Some("Age cannot be over 150".to_string()));
294            }
295            other => panic!("Expected veto expression, got {:?}", other),
296        }
297
298        match &rule.unless_clauses[1].result.kind {
299            ExpressionKind::Veto(veto) => {
300                assert_eq!(veto.message, Some("Age must be 0 or higher".to_string()));
301            }
302            other => panic!("Expected veto expression, got {:?}", other),
303        }
304    }
305
306    #[test]
307    fn veto_without_message_parses_as_veto_with_no_message() {
308        let input = r#"doc test
309rule adult = age >= 18 unless age > 150 then veto"#;
310        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
311        let rule = &docs[0].rules[0];
312        assert_eq!(rule.unless_clauses.len(), 1);
313
314        match &rule.unless_clauses[0].result.kind {
315            ExpressionKind::Veto(veto) => {
316                assert_eq!(veto.message, None);
317            }
318            other => panic!("Expected veto expression, got {:?}", other),
319        }
320    }
321
322    #[test]
323    fn mixed_veto_and_regular_unless_parses_both_results() {
324        let input = r#"doc test
325rule adjusted_age = age + 1
326  unless age < 0 then veto "Invalid age"
327  unless age > 100 then 100"#;
328        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
329        let rule = &docs[0].rules[0];
330        assert_eq!(rule.unless_clauses.len(), 2);
331
332        match &rule.unless_clauses[0].result.kind {
333            ExpressionKind::Veto(veto) => {
334                assert_eq!(veto.message, Some("Invalid age".to_string()));
335            }
336            other => panic!("Expected veto expression, got {:?}", other),
337        }
338
339        match &rule.unless_clauses[1].result.kind {
340            ExpressionKind::Literal(lit) => match &lit.value {
341                Value::Number(n) => assert_eq!(*n, rust_decimal::Decimal::new(100, 0)),
342                other => panic!("Expected literal number, got {:?}", other),
343            },
344            other => panic!("Expected literal result, got {:?}", other),
345        }
346    }
347}