Skip to main content

lemma/parsing/
parser.rs

1use crate::error::Error;
2use crate::limits::ResourceLimits;
3use crate::parsing::ast::{try_parse_type_constraint_command, *};
4use crate::parsing::lexer::{
5    can_be_label, can_be_repository_qualifier_segment, is_boolean_keyword, is_keyword,
6    is_math_function, is_spec_body_keyword, token_is_calendar_period_marker,
7    token_kind_to_boolean_value, token_kind_to_primitive, Lexer, LexerCheckpoint, Token, TokenKind,
8};
9use crate::parsing::source::Source;
10use indexmap::IndexMap;
11use rust_decimal::Decimal;
12use std::sync::Arc;
13
14#[derive(Debug)]
15struct DeprecatedStandaloneWith {
16    path: Reference,
17    rhs: WithRhs,
18    source_location: Source,
19}
20
21fn merge_deprecated_standalone_with(
22    data: &mut [LemmaData],
23    pending: Vec<DeprecatedStandaloneWith>,
24) -> Result<(), Error> {
25    for item in pending {
26        let alias = item.path.segments[0].clone();
27        let relative = Reference {
28            segments: item.path.segments[1..].to_vec(),
29            name: item.path.name.clone(),
30        };
31        let import = data.iter_mut().find(|datum| {
32            datum.reference.is_local()
33                && datum.reference.name == alias
34                && matches!(&datum.value, DataValue::Import { .. })
35        });
36        if import.is_none() {
37            return Err(Error::parsing(
38                format!("`uses {alias}: …` is required for standalone `with {alias}.…`"),
39                item.source_location,
40                Some(format!("Add `uses {alias}: <spec_name>` in this spec")),
41            ));
42        }
43        let datum = import.expect("BUG: import row exists after is_none check");
44        let DataValue::Import { bindings, .. } = &mut datum.value else {
45            unreachable!("BUG: matched Import arm in find predicate");
46        };
47        bindings.push(UsesBinding {
48            path: relative,
49            rhs: item.rhs,
50            source_location: item.source_location,
51            deprecated_standalone_with: true,
52        });
53    }
54    Ok(())
55}
56
57#[derive(Debug)]
58pub struct ParseResult {
59    pub repositories: IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>>,
60    pub expression_count: usize,
61}
62
63impl ParseResult {
64    /// Specs in parse order: repository groups follow declaration order; specs within each group follow source order.
65    #[must_use]
66    pub fn flatten_specs(&self) -> Vec<&LemmaSpec> {
67        self.repositories
68            .values()
69            .flat_map(|specs| specs.iter())
70            .collect()
71    }
72
73    #[must_use]
74    pub fn into_flattened_specs(self) -> Vec<LemmaSpec> {
75        self.repositories.into_values().flatten().collect()
76    }
77}
78
79pub fn parse(
80    content: &str,
81    source_type: crate::parsing::source::SourceType,
82    limits: &ResourceLimits,
83) -> Result<ParseResult, Error> {
84    if content.len() > limits.max_source_size_bytes {
85        return Err(Error::resource_limit_exceeded(
86            "max_source_size_bytes",
87            format!(
88                "{} bytes ({} MB)",
89                limits.max_source_size_bytes,
90                limits.max_source_size_bytes / (1024 * 1024)
91            ),
92            format!(
93                "{} bytes ({:.2} MB)",
94                content.len(),
95                content.len() as f64 / (1024.0 * 1024.0)
96            ),
97            "Reduce source size or split into multiple specs",
98            None,
99            None,
100            None,
101        ));
102    }
103
104    let mut parser = Parser::new(content, source_type, limits);
105    let repositories = parser.parse_file()?;
106    let mut result = ParseResult {
107        repositories,
108        expression_count: parser.expression_count,
109    };
110    canonicalize_parse_result(&mut result);
111    Ok(result)
112}
113
114fn canonicalize_parse_result(result: &mut ParseResult) {
115    let old = std::mem::take(&mut result.repositories);
116    let mut new_map: IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>> = IndexMap::new();
117    for (repo, mut specs) in old {
118        let mut canonical_repo = (*repo).clone();
119        canonicalize_repository(&mut canonical_repo);
120        for spec in &mut specs {
121            canonicalize_lemma_spec(spec);
122        }
123        new_map
124            .entry(Arc::new(canonical_repo))
125            .or_default()
126            .extend(specs);
127    }
128    result.repositories = new_map;
129}
130
131struct Parser {
132    lexer: Lexer,
133    source_type: crate::parsing::source::SourceType,
134    depth_tracker: DepthTracker,
135    expression_count: usize,
136    max_expression_count: usize,
137    max_spec_name_length: usize,
138    max_data_name_length: usize,
139    max_rule_name_length: usize,
140    last_span: Span,
141}
142
143impl Parser {
144    fn new(
145        content: &str,
146        source_type: crate::parsing::source::SourceType,
147        limits: &ResourceLimits,
148    ) -> Self {
149        Parser {
150            lexer: Lexer::new(content, &source_type),
151            source_type,
152            depth_tracker: DepthTracker::with_max_depth(limits.max_expression_depth),
153            expression_count: 0,
154            max_expression_count: limits.max_expression_count,
155            max_spec_name_length: crate::limits::MAX_SPEC_NAME_LENGTH,
156            max_data_name_length: crate::limits::MAX_DATA_NAME_LENGTH,
157            max_rule_name_length: crate::limits::MAX_RULE_NAME_LENGTH,
158            last_span: Span {
159                start: 0,
160                end: 0,
161                line: 1,
162                col: 0,
163            },
164        }
165    }
166
167    fn source_type(&self) -> crate::parsing::source::SourceType {
168        self.source_type.clone()
169    }
170
171    fn peek(&mut self) -> Result<&Token, Error> {
172        self.lexer.peek()
173    }
174
175    fn next(&mut self) -> Result<Token, Error> {
176        let token = self.lexer.next_token()?;
177        self.last_span = token.span.clone();
178        Ok(token)
179    }
180
181    fn at(&mut self, kind: &TokenKind) -> Result<bool, Error> {
182        Ok(&self.peek()?.kind == kind)
183    }
184
185    fn at_any(&mut self, kinds: &[TokenKind]) -> Result<bool, Error> {
186        let current = &self.peek()?.kind;
187        Ok(kinds.contains(current))
188    }
189
190    fn checkpoint(&self) -> (LexerCheckpoint, usize) {
191        (self.lexer.checkpoint(), self.expression_count)
192    }
193
194    fn restore(&mut self, checkpoint: (LexerCheckpoint, usize)) {
195        self.lexer.restore(checkpoint.0);
196        self.expression_count = checkpoint.1;
197    }
198
199    fn expect(&mut self, kind: &TokenKind) -> Result<Token, Error> {
200        let token = self.next()?;
201        if &token.kind == kind {
202            Ok(token)
203        } else {
204            Err(self.error_at_token(&token, format!("Expected {}, found {}", kind, token.kind)))
205        }
206    }
207
208    fn at_calendar_period_marker(&mut self) -> Result<bool, Error> {
209        Ok(token_is_calendar_period_marker(self.peek()?))
210    }
211
212    fn expect_calendar_period_marker(&mut self) -> Result<Token, Error> {
213        let token = self.next()?;
214        if token_is_calendar_period_marker(&token) {
215            Ok(token)
216        } else {
217            Err(self.error_at_token(&token, "Expected 'calendar' (date-period predicate marker)"))
218        }
219    }
220
221    fn next_calendar_period_marker(&mut self) -> Result<Token, Error> {
222        self.expect_calendar_period_marker()
223    }
224
225    fn error_at_token(&self, token: &Token, message: impl Into<String>) -> Error {
226        Error::parsing(
227            message,
228            Source::new(self.source_type(), token.span.clone()),
229            None::<String>,
230        )
231    }
232
233    fn error_at_token_with_suggestion(
234        &self,
235        token: &Token,
236        message: impl Into<String>,
237        suggestion: impl Into<String>,
238    ) -> Error {
239        Error::parsing(
240            message,
241            Source::new(self.source_type(), token.span.clone()),
242            Some(suggestion),
243        )
244    }
245
246    fn parse_spec_ref_trailing_effective(&mut self) -> Result<Option<DateTimeValue>, Error> {
247        let mut effective = None;
248        if self.at(&TokenKind::NumberLit)? {
249            let peeked = self.peek()?;
250            if peeked.text.len() == 4 && peeked.text.chars().all(|c| c.is_ascii_digit()) {
251                effective = self.try_parse_effective_from()?;
252            }
253        }
254        Ok(effective)
255    }
256
257    fn make_source(&self, span: Span) -> Source {
258        Source::new(self.source_type(), span)
259    }
260
261    fn span_from(&self, start: &Span) -> Span {
262        // Create a span from start to the current lexer position.
263        // We peek to get the current position.
264        Span {
265            start: start.start,
266            end: start.end.max(start.start),
267            line: start.line,
268            col: start.col,
269        }
270    }
271
272    fn span_covering(&self, start: &Span, end: &Span) -> Span {
273        Span {
274            start: start.start,
275            end: end.end,
276            line: start.line,
277            col: start.col,
278        }
279    }
280
281    // ========================================================================
282    // Top-level: file and spec
283    // ========================================================================
284
285    fn parse_file(&mut self) -> Result<IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>>, Error> {
286        let mut map: IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>> = IndexMap::new();
287        let mut current_repo = Arc::new(LemmaRepository::new(None));
288
289        loop {
290            if self.at(&TokenKind::Eof)? {
291                break;
292            }
293
294            if self.at(&TokenKind::Repo)? {
295                let repo_token = self.expect(&TokenKind::Repo)?;
296                let start_line = repo_token.span.line;
297                let (qualifier, _) = self.parse_repository_qualifier()?;
298                crate::limits::check_max_length(
299                    &qualifier.name,
300                    self.max_spec_name_length,
301                    "repository name",
302                    Some(Source::new(self.source_type(), repo_token.span)),
303                )?;
304                current_repo = Arc::new(
305                    LemmaRepository::new(Some(qualifier.name)).with_start_line(start_line),
306                );
307                map.entry(Arc::clone(&current_repo)).or_default();
308                continue;
309            }
310
311            if self.at(&TokenKind::Spec)? {
312                let spec = self.parse_spec()?;
313                map.entry(Arc::clone(&current_repo)).or_default().push(spec);
314                continue;
315            }
316
317            let token = self.next()?;
318            return Err(self.error_at_token_with_suggestion(
319                &token,
320                format!(
321                    "Expected a top-level `repo` or `spec` declaration, found {}",
322                    token.kind
323                ),
324                "Each Lemma file is a sequence of optional `repo <name>` sections followed by `spec <name>` blocks",
325            ));
326        }
327
328        Ok(map)
329    }
330
331    fn parse_spec(&mut self) -> Result<LemmaSpec, Error> {
332        let spec_token = self.expect(&TokenKind::Spec)?;
333        let start_line = spec_token.span.line;
334
335        let (name, name_span) = self.parse_spec_name()?;
336        crate::limits::check_max_length(
337            &name,
338            self.max_spec_name_length,
339            "spec",
340            Some(Source::new(self.source_type(), name_span)),
341        )?;
342
343        let effective_from = self.try_parse_effective_from()?;
344
345        let commentary = self.try_parse_commentary()?;
346
347        let mut spec = LemmaSpec::new(name.clone())
348            .with_source_type(self.source_type())
349            .with_start_line(start_line);
350        spec.effective_from = crate::parsing::ast::EffectiveDate::from_option(effective_from);
351
352        if let Some(commentary_text) = commentary {
353            spec = spec.set_commentary(commentary_text);
354        }
355
356        // First pass: collect type definitions
357        // We need to peek and handle type definitions first, but since we consume tokens
358        // linearly, we'll collect all items in one pass.
359        let mut data = Vec::new();
360        let mut rules = Vec::new();
361        let mut meta_fields = Vec::new();
362        let mut pending_deprecated_with: Vec<DeprecatedStandaloneWith> = Vec::new();
363
364        loop {
365            let peek_kind = self.peek()?.kind.clone();
366            match peek_kind {
367                TokenKind::Data => {
368                    let datum = self.parse_data()?;
369                    data.push(datum);
370                }
371                TokenKind::With => {
372                    pending_deprecated_with.push(self.parse_deprecated_standalone_with()?);
373                }
374                TokenKind::Rule => {
375                    let rule = self.parse_rule()?;
376                    rules.push(rule);
377                }
378                TokenKind::Meta => {
379                    let meta = self.parse_meta()?;
380                    meta_fields.push(meta);
381                }
382                TokenKind::Uses => {
383                    let uses_data = self.parse_uses_statement()?;
384                    data.push(uses_data);
385                }
386                TokenKind::Spec | TokenKind::Repo | TokenKind::Eof => break,
387                _ => {
388                    let token = self.next()?;
389                    return Err(self.error_at_token_with_suggestion(
390                        &token,
391                        format!(
392                            "Expected 'data', 'rule', 'meta', 'uses', or a new 'spec', found '{}'",
393                            token.text
394                        ),
395                        "Check the spelling or add the appropriate keyword",
396                    ));
397                }
398            }
399        }
400
401        merge_deprecated_standalone_with(&mut data, pending_deprecated_with)?;
402
403        for data in data {
404            spec = spec.add_data(data);
405        }
406        for rule in rules {
407            spec = spec.add_rule(rule);
408        }
409        for meta in meta_fields {
410            spec = spec.add_meta_field(meta);
411        }
412
413        Ok(spec)
414    }
415
416    /// Parse a spec name: identifier segments separated by `/`, `-`, or `.`.
417    ///
418    /// Allows: `my_spec`, `contracts/employment/jack`, `nl.tax.brackets`.
419    /// The `@` prefix is not allowed in spec names — it is valid in
420    /// repository names (`repo @org/name`) and qualifiers (`uses @org/name`).
421    fn parse_spec_name(&mut self) -> Result<(String, Span), Error> {
422        if self.at(&TokenKind::At)? {
423            let at_tok = self.next()?;
424            return Err(Error::parsing(
425                "'@' is not allowed in spec names; it is valid for repository names (`repo @org/name`) and qualifiers (`uses @org/name`)",
426                self.make_source(at_tok.span),
427                Some(
428                    "Write `spec my_spec`, then reference registry specs as `uses alias: @org/repo spec_name` or `data x: alias.TypeName` after importing with `uses`.",
429                ),
430            ));
431        }
432
433        let first = self.next()?;
434        if !first.kind.is_identifier_like() {
435            return Err(self.error_at_token(
436                &first,
437                format!("Expected a spec name, found {}", first.kind),
438            ));
439        }
440        let mut name = first.text.clone();
441        let start_span = first.span.clone();
442        let mut end_span = first.span.clone();
443
444        loop {
445            if self.at(&TokenKind::Slash)? {
446                self.next()?;
447                let seg = self.next()?;
448                if !seg.kind.is_identifier_like() {
449                    return Err(self.error_at_token(
450                        &seg,
451                        format!(
452                            "Expected identifier after '/' in spec name, found {}",
453                            seg.kind
454                        ),
455                    ));
456                }
457                name.push('/');
458                name.push_str(&seg.text);
459                end_span = seg.span.clone();
460            } else if self.at(&TokenKind::Dot)? {
461                self.next()?;
462                let seg = self.next()?;
463                if !seg.kind.is_identifier_like() {
464                    return Err(self.error_at_token(
465                        &seg,
466                        format!(
467                            "Expected identifier after '.' in spec name, found {}",
468                            seg.kind
469                        ),
470                    ));
471                }
472                name.push('.');
473                name.push_str(&seg.text);
474                end_span = seg.span.clone();
475            } else if self.at(&TokenKind::Minus)? {
476                let minus_span = self.peek()?.span.clone();
477                self.next()?;
478                let peeked = self.peek()?;
479                if !peeked.kind.is_identifier_like() {
480                    let span = self.span_covering(&start_span, &minus_span);
481                    return Err(Error::parsing(
482                        "Trailing '-' after spec name",
483                        self.make_source(span),
484                        None::<String>,
485                    ));
486                }
487                let seg = self.next()?;
488                name.push('-');
489                name.push_str(&seg.text);
490                end_span = seg.span.clone();
491            } else {
492                break;
493            }
494        }
495
496        let full_span = self.span_covering(&start_span, &end_span);
497        Ok((name, full_span))
498    }
499
500    /// Parse a repository qualifier: `[@] identifier ((Slash | Dot | Minus) identifier)*`.
501    ///
502    /// The `@` prefix, when present, is included in the name string (e.g. `"@org/repo"`).
503    /// Slashes, dots and minuses between segments are stitched into the name verbatim
504    /// so the qualifier round-trips exactly.
505    ///
506    /// Used in `repo` declarations and registry qualifiers (`uses`).
507    fn parse_repository_qualifier(&mut self) -> Result<(RepositoryQualifier, Span), Error> {
508        let has_at = self.at(&TokenKind::At)?;
509        let start_span = if has_at {
510            let at_tok = self.next()?;
511            at_tok.span.clone()
512        } else {
513            Span {
514                start: 0,
515                end: 0,
516                line: 0,
517                col: 0,
518            }
519        };
520
521        let first = self.next()?;
522        if !can_be_repository_qualifier_segment(&first.kind) {
523            return Err(self.error_at_token(
524                &first,
525                format!(
526                    "Expected a repository qualifier segment, found {}",
527                    first.kind
528                ),
529            ));
530        }
531        if !has_at && is_keyword(&first.kind) {
532            return Err(self.error_at_token(
533                &first,
534                format!(
535                    "'{}' is a reserved keyword and cannot be used as a repository name",
536                    first.text
537                ),
538            ));
539        }
540        let start_span = if has_at {
541            start_span
542        } else {
543            first.span.clone()
544        };
545        let mut name = first.text.clone();
546
547        loop {
548            let next_kind = self.peek()?.kind.clone();
549            match next_kind {
550                TokenKind::Slash => {
551                    self.next()?;
552                    name.push('/');
553                    let seg = self.next()?;
554                    if !can_be_repository_qualifier_segment(&seg.kind) {
555                        return Err(self.error_at_token(
556                            &seg,
557                            format!(
558                                "Expected identifier after '/' in repository qualifier segment, found {}",
559                                seg.kind
560                            ),
561                        ));
562                    }
563                    name.push_str(&seg.text);
564                }
565                TokenKind::Dot => {
566                    self.next()?;
567                    name.push('.');
568                    let seg = self.next()?;
569                    if !can_be_repository_qualifier_segment(&seg.kind) {
570                        return Err(self.error_at_token(
571                            &seg,
572                            format!(
573                                "Expected identifier after '.' in repository qualifier segment, found {}",
574                                seg.kind
575                            ),
576                        ));
577                    }
578                    name.push_str(&seg.text);
579                }
580                TokenKind::Minus => {
581                    let minus_text_peek = self.lexer.peek_second()?;
582                    if !can_be_repository_qualifier_segment(&minus_text_peek.kind) {
583                        break;
584                    }
585                    self.next()?;
586                    name.push('-');
587                    let seg = self.next()?;
588                    name.push_str(&seg.text);
589                }
590                _ => break,
591            }
592        }
593
594        if has_at {
595            name.insert(0, '@');
596        }
597
598        let full_span = self.span_covering(&start_span, &self.last_span);
599        Ok((RepositoryQualifier { name }, full_span))
600    }
601
602    /// Parses `[<repository_qualifier>] <spec> [<effective>]`
603    pub fn parse_spec_ref_target(&mut self) -> Result<SpecRef, Error> {
604        let mut repository = None;
605        let mut repository_span = None;
606
607        if self.at(&TokenKind::At)? {
608            let (q, span) = self.parse_repository_qualifier()?;
609            repository = Some(q);
610            repository_span = Some(span);
611        } else {
612            let saved_state = self.lexer.clone();
613            if let Ok((potential_repository, span)) = self.parse_repository_qualifier() {
614                if let Ok(next_tok) = self.peek() {
615                    if next_tok.kind.is_identifier_like() {
616                        repository = Some(potential_repository);
617                        repository_span = Some(span);
618                    } else {
619                        self.lexer = saved_state;
620                    }
621                } else {
622                    self.lexer = saved_state;
623                }
624            } else {
625                self.lexer = saved_state;
626            }
627        }
628
629        let (spec_name, spec_name_span) = self.parse_spec_name()?;
630        let effective = self.parse_spec_ref_trailing_effective()?;
631        let target_span = self.span_covering(&spec_name_span, &self.last_span);
632
633        let has_repository = repository.is_some();
634        Ok(SpecRef {
635            name: spec_name,
636            repository,
637            effective,
638            repository_span: if has_repository {
639                repository_span
640            } else {
641                None
642            },
643            target_span: Some(target_span),
644        })
645    }
646
647    fn try_parse_effective_from(&mut self) -> Result<Option<DateTimeValue>, Error> {
648        // effective_from is a date/time token right after the spec name.
649        // It's tricky because it looks like a number (e.g. 2026-03-04).
650        // In the old grammar it was a special atomic rule.
651        // We'll check if the next token is a NumberLit that looks like a year.
652        if !self.at(&TokenKind::NumberLit)? {
653            return Ok(None);
654        }
655
656        let peeked = self.peek()?;
657        let peeked_text = peeked.text.clone();
658        let peeked_span = peeked.span.clone();
659
660        // Check if it could be a date: 4-digit number followed by -
661        if peeked_text.len() == 4 && peeked_text.chars().all(|c| c.is_ascii_digit()) {
662            // Collect the full datetime string by consuming tokens
663            let mut dt_str = String::new();
664            let num_tok = self.next()?; // consume the year number
665            dt_str.push_str(&num_tok.text);
666
667            // Try to consume -MM-DD and optional T... parts
668            while self.at(&TokenKind::Minus)? {
669                self.next()?; // consume -
670                dt_str.push('-');
671                let part = self.next()?;
672                dt_str.push_str(&part.text);
673            }
674
675            // Check for T (time part)
676            if self.at(&TokenKind::Identifier)? {
677                let peeked = self.peek()?;
678                if peeked.text.starts_with('T') || peeked.text.starts_with('t') {
679                    let time_part = self.next()?;
680                    dt_str.push_str(&time_part.text);
681                    // Consume any : separated parts
682                    while self.at(&TokenKind::Colon)? {
683                        self.next()?;
684                        dt_str.push(':');
685                        let part = self.next()?;
686                        dt_str.push_str(&part.text);
687                    }
688                    // Check for timezone (+ or Z)
689                    if self.at(&TokenKind::Plus)? {
690                        self.next()?;
691                        dt_str.push('+');
692                        let tz_part = self.next()?;
693                        dt_str.push_str(&tz_part.text);
694                        if self.at(&TokenKind::Colon)? {
695                            self.next()?;
696                            dt_str.push(':');
697                            let tz_min = self.next()?;
698                            dt_str.push_str(&tz_min.text);
699                        }
700                    }
701                }
702            }
703
704            // Try to parse as datetime
705            if let Ok(dtv) = dt_str.parse::<DateTimeValue>() {
706                return Ok(Some(dtv));
707            }
708
709            return Err(Error::parsing(
710                format!("Invalid date/time in spec declaration: '{}'", dt_str),
711                self.make_source(peeked_span),
712                None::<String>,
713            ));
714        }
715
716        Ok(None)
717    }
718
719    fn try_parse_commentary(&mut self) -> Result<Option<String>, Error> {
720        if !self.at(&TokenKind::Commentary)? {
721            return Ok(None);
722        }
723        let token = self.next()?;
724        let trimmed = token.text.trim().to_string();
725        if trimmed.is_empty() {
726            Ok(None)
727        } else {
728            Ok(Some(trimmed))
729        }
730    }
731
732    // ========================================================================
733    // Data parsing
734    // ========================================================================
735
736    fn parse_data(&mut self) -> Result<LemmaData, Error> {
737        let data_token = self.expect(&TokenKind::Data)?;
738        let start_span = data_token.span.clone();
739
740        let reference = self.parse_reference()?;
741        for segment in reference
742            .segments
743            .iter()
744            .chain(std::iter::once(&reference.name))
745        {
746            crate::limits::check_max_length(
747                segment,
748                self.max_data_name_length,
749                "data",
750                Some(Source::new(self.source_type(), start_span.clone())),
751            )?;
752        }
753
754        self.expect(&TokenKind::Colon)?;
755
756        if !reference.segments.is_empty() {
757            let tok = self.peek()?.clone();
758            return Err(self.error_at_token_with_suggestion(
759                &tok,
760                "Dotted paths require `uses` with `-> with`; `data` declares types and values on local names only.",
761                "Use `uses alias: spec` with `  -> with path.to.slot: <value or reference>`.",
762            ));
763        }
764
765        let value = self.parse_data_value()?;
766
767        let span = self.span_covering(&start_span, &self.last_span);
768        let source = self.make_source(span);
769
770        Ok(LemmaData::new(reference, value, source))
771    }
772
773    fn with_rhs_starts_as_literal(kind: &TokenKind) -> bool {
774        matches!(
775            kind,
776            TokenKind::StringLit | TokenKind::NumberLit | TokenKind::Minus | TokenKind::Plus
777        ) || is_boolean_keyword(kind)
778    }
779
780    fn parse_deprecated_standalone_with(&mut self) -> Result<DeprecatedStandaloneWith, Error> {
781        let with_token = self.expect(&TokenKind::With)?;
782        let start_span = with_token.span.clone();
783        let path = self.parse_reference()?;
784        if path.segments.is_empty() {
785            return Err(self.error_at_token_with_suggestion(
786                &with_token,
787                "Standalone `with` must target an imported spec path (`with alias.field: …`).",
788                "Use `data name: …` for local slots, or nest under `uses` with `  -> with path: …`.",
789            ));
790        }
791        let (rhs, _) =
792            self.parse_assignment_after_key(|parser| parser.parse_with_rhs(), "with", false)?;
793        let span = self.span_covering(&start_span, &self.last_span);
794        Ok(DeprecatedStandaloneWith {
795            path,
796            rhs,
797            source_location: self.make_source(span),
798        })
799    }
800
801    fn parse_with_rhs(&mut self) -> Result<WithRhs, Error> {
802        let peek_kind = self.peek()?.kind.clone();
803
804        if Self::with_rhs_starts_as_literal(&peek_kind) {
805            let value = self.parse_literal_value()?;
806            return Ok(WithRhs::Literal(value));
807        }
808
809        if can_be_label(&peek_kind) {
810            let target = self.parse_reference()?;
811            if self.at(&TokenKind::Arrow)? && self.lexer.peek_second()?.kind != TokenKind::With {
812                let tok = self.peek()?.clone();
813                return Err(self.error_at_token_with_suggestion(
814                    &tok,
815                    "Constraint chains (`-> ...`) are not allowed on a `uses` binding reference.",
816                    "Use `data name: <type> -> ...` for type constraints on local slots.",
817                ));
818            }
819            return Ok(WithRhs::Reference { target });
820        }
821
822        let tok = self.peek()?.clone();
823        Err(self.error_at_token(
824            &tok,
825            format!(
826                "Expected a reference or literal after `-> with ...:`, found {}",
827                tok.kind
828            ),
829        ))
830    }
831
832    fn parse_reference(&mut self) -> Result<Reference, Error> {
833        let mut segments = Vec::new();
834
835        let first = self.next()?;
836        // Keywords cannot be used as names
837        if is_keyword(&first.kind) {
838            return Err(self.error_at_token_with_suggestion(
839                &first,
840                format!(
841                    "'{}' is a reserved keyword and cannot be used as a name",
842                    first.text
843                ),
844                "Choose a different name that is not a reserved keyword",
845            ));
846        }
847
848        if !can_be_label(&first.kind) {
849            return Err(self.error_at_token(
850                &first,
851                format!("Expected an identifier, found {}", first.kind),
852            ));
853        }
854
855        segments.push(first.text.clone());
856
857        // Consume . separated segments
858        while self.at(&TokenKind::Dot)? {
859            self.next()?; // consume .
860            let seg = self.next()?;
861            if !can_be_label(&seg.kind) {
862                return Err(self.error_at_token(
863                    &seg,
864                    format!("Expected an identifier after '.', found {}", seg.kind),
865                ));
866            }
867            segments.push(seg.text.clone());
868        }
869
870        Ok(Reference::from_path(segments))
871    }
872
873    fn parse_data_value(&mut self) -> Result<DataValue, Error> {
874        if self.at(&TokenKind::Spec)? {
875            let token = self.next()?;
876            return Err(self.error_at_token_with_suggestion(
877                &token,
878                "Cannot import a spec with `data`; use `uses`",
879                "Use `uses <spec_name>` or `uses <alias>: <spec_name>`",
880            ));
881        }
882
883        let peek_kind = self.peek()?.kind.clone();
884
885        if token_kind_to_primitive(&peek_kind).is_some() || can_be_label(&peek_kind) {
886            let (base, constraints) = self.parse_type_arrow_chain()?;
887            return Ok(DataValue::Definition {
888                base: Some(base),
889                constraints,
890                value: None,
891            });
892        }
893
894        // Otherwise, it's a literal value
895        let value = self.parse_literal_value()?;
896        Ok(DataValue::Definition {
897            base: None,
898            constraints: None,
899            value: Some(value),
900        })
901    }
902
903    /// Parse a single `uses` item: `[alias ':']` then [`Self::parse_spec_ref_target`] (optional
904    /// repository qualifier, spec name, optional effective date pin).
905    fn parse_uses_item(&mut self, start_span: &Span) -> Result<LemmaData, Error> {
906        let explicit_alias = if can_be_label(&self.peek()?.kind)
907            && self.lexer.peek_second()?.kind == TokenKind::Colon
908        {
909            let alias_tok = self.next()?;
910            self.expect(&TokenKind::Colon)?;
911            Some(alias_tok)
912        } else {
913            None
914        };
915
916        let spec_ref = self.parse_spec_ref_target()?;
917
918        let spec_name_source = spec_ref
919            .target_span
920            .as_ref()
921            .map(|sp| Source::new(self.source_type(), sp.clone()));
922
923        crate::limits::check_max_length(
924            &spec_ref.name,
925            self.max_spec_name_length,
926            "spec",
927            spec_name_source.clone(),
928        )?;
929
930        let alias = if let Some(ref alias_tok) = explicit_alias {
931            crate::limits::check_max_length(
932                &alias_tok.text,
933                self.max_data_name_length,
934                "data",
935                Some(Source::new(self.source_type(), alias_tok.span.clone())),
936            )?;
937            alias_tok.text.clone()
938        } else {
939            let implicit = spec_ref.name.clone();
940            crate::limits::check_max_length(
941                &implicit,
942                self.max_data_name_length,
943                "data",
944                spec_name_source,
945            )?;
946            implicit
947        };
948
949        let mut bindings = Vec::new();
950        while self.at(&TokenKind::Arrow)? {
951            if self.lexer.peek_second()?.kind != TokenKind::With {
952                let tok = self.peek()?.clone();
953                return Err(self.error_at_token_with_suggestion(
954                    &tok,
955                    "Expected `with` after `->` in a `uses` block.",
956                    "Write `  -> with path.to.slot: <value or reference>` under the `uses` line.",
957                ));
958            }
959            self.next()?; // ->
960            let with_token = self.expect(&TokenKind::With)?;
961            let binding_start_span = with_token.span.clone();
962
963            let path = self.parse_reference()?;
964            for segment in path.segments.iter().chain(std::iter::once(&path.name)) {
965                crate::limits::check_max_length(
966                    segment,
967                    self.max_data_name_length,
968                    "uses binding",
969                    Some(Source::new(self.source_type(), binding_start_span.clone())),
970                )?;
971            }
972            if path
973                .segments
974                .first()
975                .is_some_and(|segment| segment == &alias)
976            {
977                return Err(self.error_at_token_with_suggestion(
978                    &with_token,
979                    format!(
980                        "Binding path must be relative to the imported spec, not prefixed with import alias `{alias}`."
981                    ),
982                    format!(
983                        "Use `-> with {}: …` without the `{alias}.` prefix.",
984                        path.name
985                    ),
986                ));
987            }
988
989            let (rhs, _) =
990                self.parse_assignment_after_key(|parser| parser.parse_with_rhs(), "with", false)?;
991
992            let binding_span = self.span_covering(&binding_start_span, &self.last_span);
993            bindings.push(crate::parsing::ast::UsesBinding {
994                path,
995                rhs,
996                source_location: self.make_source(binding_span),
997                deprecated_standalone_with: false,
998            });
999        }
1000
1001        let span = self.span_covering(start_span, &self.last_span);
1002        Ok(LemmaData::new(
1003            Reference::local(alias),
1004            DataValue::Import { spec_ref, bindings },
1005            self.make_source(span),
1006        ))
1007    }
1008
1009    fn parse_uses_statement(&mut self) -> Result<LemmaData, Error> {
1010        let uses_token = self.expect(&TokenKind::Uses)?;
1011        let start_span = uses_token.span.clone();
1012        self.parse_uses_item(&start_span)
1013    }
1014
1015    // ========================================================================
1016    // Rule parsing
1017    // ========================================================================
1018
1019    fn parse_rule(&mut self) -> Result<LemmaRule, Error> {
1020        let rule_token = self.expect(&TokenKind::Rule)?;
1021        let start_span = rule_token.span.clone();
1022
1023        let name_tok = self.next()?;
1024        if is_keyword(&name_tok.kind) {
1025            return Err(self.error_at_token_with_suggestion(
1026                &name_tok,
1027                format!(
1028                    "'{}' is a reserved keyword and cannot be used as a rule name",
1029                    name_tok.text
1030                ),
1031                "Choose a different name that is not a reserved keyword",
1032            ));
1033        }
1034        if !can_be_label(&name_tok.kind) {
1035            return Err(self.error_at_token(
1036                &name_tok,
1037                format!("Expected a rule name, found {}", name_tok.kind),
1038            ));
1039        }
1040        let rule_name = name_tok.text.clone();
1041        crate::limits::check_max_length(
1042            &rule_name,
1043            self.max_rule_name_length,
1044            "rule",
1045            Some(Source::new(self.source_type(), name_tok.span.clone())),
1046        )?;
1047
1048        self.expect(&TokenKind::Colon)?;
1049
1050        // Parse the base expression or veto result (`veto "msg"`). `veto is …` is an expression.
1051        let expression = if self.at(&TokenKind::Veto)? && !self.at_bare_veto_followed_by_is()? {
1052            self.parse_veto_expression()?
1053        } else {
1054            self.parse_expression()?
1055        };
1056
1057        // Parse unless clauses
1058        let mut unless_clauses = Vec::new();
1059        while self.at(&TokenKind::Unless)? {
1060            unless_clauses.push(self.parse_unless_clause()?);
1061        }
1062
1063        let end_span = if let Some(last_unless) = unless_clauses.last() {
1064            last_unless.source_location.span.clone()
1065        } else if let Some(ref loc) = expression.source_location {
1066            loc.span.clone()
1067        } else {
1068            start_span.clone()
1069        };
1070
1071        let span = self.span_covering(&start_span, &end_span);
1072        Ok(LemmaRule {
1073            name: rule_name,
1074            expression,
1075            unless_clauses,
1076            source_location: self.make_source(span),
1077        })
1078    }
1079
1080    fn parse_veto_expression(&mut self) -> Result<Expression, Error> {
1081        let veto_tok = self.expect(&TokenKind::Veto)?;
1082        let start_span = veto_tok.span.clone();
1083
1084        let message = if self.at(&TokenKind::StringLit)? {
1085            let str_tok = self.next()?;
1086            let content = unquote_string(&str_tok.text);
1087            Some(content)
1088        } else {
1089            None
1090        };
1091
1092        let span = self.span_from(&start_span);
1093        self.new_expression(
1094            ExpressionKind::Veto(VetoExpression { message }),
1095            self.make_source(span),
1096        )
1097    }
1098
1099    fn parse_unless_clause(&mut self) -> Result<UnlessClause, Error> {
1100        let unless_tok = self.expect(&TokenKind::Unless)?;
1101        let start_span = unless_tok.span.clone();
1102
1103        let condition = self.parse_expression()?;
1104
1105        self.expect(&TokenKind::Then)?;
1106
1107        let result = if self.at(&TokenKind::Veto)? {
1108            self.parse_veto_expression()?
1109        } else {
1110            self.parse_expression()?
1111        };
1112
1113        let end_span = result
1114            .source_location
1115            .as_ref()
1116            .map(|s| s.span.clone())
1117            .unwrap_or_else(|| start_span.clone());
1118        let span = self.span_covering(&start_span, &end_span);
1119
1120        Ok(UnlessClause {
1121            condition,
1122            result,
1123            source_location: self.make_source(span),
1124        })
1125    }
1126
1127    fn parse_leaf_parent_type(&mut self) -> Result<ParentType, Error> {
1128        let name_tok = self.next()?;
1129        self.parse_leaf_parent_type_from_first_token(name_tok)
1130    }
1131
1132    fn parse_leaf_parent_type_from_first_token(
1133        &mut self,
1134        name_tok: Token,
1135    ) -> Result<ParentType, Error> {
1136        if let Some(kind) = token_kind_to_primitive(&name_tok.kind) {
1137            Ok(ParentType::Primitive { primitive: kind })
1138        } else if can_be_label(&name_tok.kind) {
1139            Ok(ParentType::Custom {
1140                name: name_tok.text.clone(),
1141            })
1142        } else {
1143            Err(self.error_at_token(
1144                &name_tok,
1145                format!("Expected a type name, found {}", name_tok.kind),
1146            ))
1147        }
1148    }
1149
1150    /// Parse a type arrow chain: [`ParentType`] (`alias.type` allowed) followed by `(-> command)*`.
1151    fn parse_type_arrow_chain(&mut self) -> Result<(ParentType, Option<Vec<Constraint>>), Error> {
1152        let first = self.parse_leaf_parent_type()?;
1153
1154        let base = if let ParentType::Custom { name } = &first {
1155            if self.at(&TokenKind::Dot)? {
1156                self.next()?;
1157                let inner = self.parse_leaf_parent_type()?;
1158                ParentType::Qualified {
1159                    spec_alias: name.clone(),
1160                    inner: Box::new(inner),
1161                }
1162            } else {
1163                first
1164            }
1165        } else {
1166            if self.at(&TokenKind::Dot)? {
1167                let dot_tok = self.peek()?.clone();
1168                return Err(self.error_at_token_with_suggestion(
1169                    &dot_tok,
1170                    "A primitive type cannot be the left segment of a qualified parent path",
1171                    "Use `data name: alias.typename` where `alias` is the `uses` import name and `typename` is the parent type.",
1172                ));
1173            }
1174            first
1175        };
1176
1177        let base = if self.at(&TokenKind::Identifier)? && self.peek()?.text == "range" {
1178            self.next()?;
1179            ParentType::Ranged {
1180                inner: Box::new(base),
1181            }
1182        } else {
1183            base
1184        };
1185
1186        let constraints = self.parse_trailing_constraints()?;
1187
1188        Ok((base, constraints))
1189    }
1190
1191    fn parse_trailing_constraints(&mut self) -> Result<Option<Vec<Constraint>>, Error> {
1192        let mut commands = Vec::new();
1193        while self.at(&TokenKind::Arrow)? {
1194            let arrow_token = self.next()?;
1195            let constraint_start_span = arrow_token.span.clone();
1196            let (cmd, cmd_args, deprecated_without_colon) = self.parse_constraint_command()?;
1197            let constraint_span = self.span_covering(&constraint_start_span, &self.last_span);
1198            commands.push(Constraint {
1199                command: cmd,
1200                args: cmd_args,
1201                source_location: self.make_source(constraint_span),
1202                deprecated_without_colon,
1203            });
1204        }
1205        let constraints = if commands.is_empty() {
1206            None
1207        } else {
1208            Some(commands)
1209        };
1210        Ok(constraints)
1211    }
1212
1213    fn parse_constraint_command(
1214        &mut self,
1215    ) -> Result<(TypeConstraintCommand, Vec<CommandArg>, bool), Error> {
1216        let name_tok = self.next()?;
1217        if !can_be_label(&name_tok.kind) {
1218            return Err(self.error_at_token(
1219                &name_tok,
1220                format!("Expected a command name, found {}", name_tok.kind),
1221            ));
1222        }
1223        let cmd = try_parse_type_constraint_command(&name_tok.text).ok_or_else(|| {
1224            self.error_at_token(
1225                &name_tok,
1226                format!(
1227                    "Unknown constraint command '{}'. Valid commands: help, suggest, unit, trait, minimum, maximum, decimals, option, options, length",
1228                    name_tok.text
1229                ),
1230            )
1231        })?;
1232
1233        match cmd.continuation_shape() {
1234            ContinuationShape::Assignment => {
1235                let (args, deprecated_without_colon) = self.parse_unit_as_assignment()?;
1236                Ok((cmd, args, deprecated_without_colon))
1237            }
1238            ContinuationShape::SpaceSeparated => {
1239                Ok((cmd, self.parse_generic_command_args()?, false))
1240            }
1241        }
1242    }
1243
1244    /// Shared spine for assignment continuations (`with`, `unit`): `key: value` or deprecated space.
1245    fn parse_assignment_after_key<T>(
1246        &mut self,
1247        parse_value: impl FnOnce(&mut Self) -> Result<T, Error>,
1248        assignment_context: &str,
1249        allow_deprecated_without_colon: bool,
1250    ) -> Result<(T, bool), Error> {
1251        if self.at(&TokenKind::Colon)? {
1252            self.next()?;
1253            let value = parse_value(self)?;
1254            return Ok((value, false));
1255        }
1256        if allow_deprecated_without_colon {
1257            let value = parse_value(self)?;
1258            return Ok((value, true));
1259        }
1260        let tok = self.peek()?.clone();
1261        Err(self.error_at_token_with_suggestion(
1262            &tok,
1263            format!("Expected `:` after `{assignment_context}` key."),
1264            format!("Use `-> {assignment_context} <key>: <value>`."),
1265        ))
1266    }
1267
1268    fn parse_unit_as_assignment(&mut self) -> Result<(Vec<CommandArg>, bool), Error> {
1269        if self.at_command_terminator()? {
1270            return Ok((Vec::new(), false));
1271        }
1272
1273        let peek_kind = self.peek()?.kind.clone();
1274        if !can_be_label(&peek_kind) {
1275            return Ok((Vec::new(), false));
1276        }
1277
1278        let unit_name_tok = self.next()?;
1279        let unit_name_arg = CommandArg::Label(unit_name_tok.text.clone());
1280
1281        let (unit_arg, deprecated_without_colon) =
1282            self.parse_assignment_after_key(|parser| parser.parse_unit_payload(), "unit", true)?;
1283
1284        Ok((
1285            vec![unit_name_arg, CommandArg::UnitExpr(unit_arg)],
1286            deprecated_without_colon,
1287        ))
1288    }
1289
1290    /// Value side of `-> unit <name>: <payload>` (factor, compound, or prefix + compound).
1291    fn parse_unit_payload(&mut self) -> Result<UnitArg, Error> {
1292        if self.at_command_terminator()? {
1293            let tok = self.peek()?.clone();
1294            return Err(self.error_at_token_with_suggestion(
1295                &tok,
1296                "Expected a unit conversion factor or compound unit expression after `:`.",
1297                "Use `-> unit <name>: <factor>` or `-> unit <name>: <compound>` (e.g. `unit eur: 1.00`, `unit eur_per_hour: eur/hour`).",
1298            ));
1299        }
1300
1301        let numeric_prefix: Option<Decimal> = if self.at(&TokenKind::NumberLit)? {
1302            let num_tok = self.next()?;
1303            Some(parse_decimal_string(&num_tok.text, &num_tok.span, self)?)
1304        } else {
1305            None
1306        };
1307
1308        let peek_kind_after_prefix = self.peek()?.kind.clone();
1309        let has_compound_expr =
1310            can_be_label(&peek_kind_after_prefix) && !self.at_command_terminator()?;
1311
1312        if has_compound_expr {
1313            let factors = self.parse_unit_factors()?;
1314            let prefix = numeric_prefix.unwrap_or(Decimal::ONE);
1315            Ok(UnitArg::Expr(prefix, factors))
1316        } else if let Some(factor) = numeric_prefix {
1317            Ok(UnitArg::Factor(factor))
1318        } else {
1319            let tok = self.peek()?.clone();
1320            Err(self.error_at_token_with_suggestion(
1321                &tok,
1322                "Expected a unit conversion factor or compound unit expression after `:`.",
1323                "Use `-> unit <name>: <factor>` or `-> unit <name>: <compound>` (e.g. `unit eur: 1.00`, `unit eur_per_hour: eur/hour`).",
1324            ))
1325        }
1326    }
1327
1328    /// Parse arguments for a generic (non-unit) constraint command.
1329    fn parse_generic_command_args(&mut self) -> Result<Vec<CommandArg>, Error> {
1330        let mut args = Vec::new();
1331        loop {
1332            if self.at(&TokenKind::Arrow)?
1333                || self.at(&TokenKind::Eof)?
1334                || is_spec_body_keyword(&self.peek()?.kind)
1335                || self.at(&TokenKind::Spec)?
1336            {
1337                break;
1338            }
1339
1340            let peek_kind = self.peek()?.kind.clone();
1341            match peek_kind {
1342                TokenKind::NumberLit
1343                | TokenKind::Minus
1344                | TokenKind::Plus
1345                | TokenKind::StringLit => {
1346                    let value = self.parse_literal_value()?;
1347                    args.push(CommandArg::Literal(value));
1348                }
1349                ref k if is_boolean_keyword(k) => {
1350                    let value = self.parse_literal_value()?;
1351                    args.push(CommandArg::Literal(value));
1352                }
1353                ref k if can_be_label(k) => {
1354                    let tok = self.next()?;
1355                    args.push(CommandArg::Label(tok.text));
1356                }
1357                _ => break,
1358            }
1359        }
1360        Ok(args)
1361    }
1362
1363    fn parse_scalar_literal_value(&mut self) -> Result<Value, Error> {
1364        let peeked = self.peek()?;
1365        match &peeked.kind {
1366            TokenKind::StringLit => {
1367                let tok = self.next()?;
1368                let content = unquote_string(&tok.text);
1369                Ok(Value::Text(content))
1370            }
1371            k if is_boolean_keyword(k) => {
1372                let tok = self.next()?;
1373                Ok(Value::Boolean(token_kind_to_boolean_value(&tok.kind)))
1374            }
1375            TokenKind::NumberLit => self.parse_number_literal(),
1376            TokenKind::Minus | TokenKind::Plus => self.parse_signed_number_literal(),
1377            _ => {
1378                let tok = self.next()?;
1379                Err(self.error_at_token(
1380                    &tok,
1381                    format!(
1382                        "Expected a value (number, text, boolean, date, etc.), found '{}'",
1383                        tok.text
1384                    ),
1385                ))
1386            }
1387        }
1388    }
1389
1390    /// Returns true when the current token ends the current command argument list.
1391    fn at_command_terminator(&mut self) -> Result<bool, Error> {
1392        if self.at(&TokenKind::Arrow)? || self.at(&TokenKind::Eof)? || self.at(&TokenKind::Spec)? {
1393            return Ok(true);
1394        }
1395        Ok(is_spec_body_keyword(&self.peek()?.kind))
1396    }
1397
1398    /// Parse a sequence of `<measure_ref>[^[-]<integer>]` terms joined by `*` or `/`.
1399    ///
1400    /// `*` is explicit multiplication and resets to numerator mode.
1401    /// `/` switches to denominator mode for all subsequent factors until the next `*`.
1402    /// Exponents in denominator mode are negated.
1403    /// An explicit `^<integer>` overrides the default (±1), still relative to the current mode.
1404    ///
1405    /// Space has no meaning in Lemma — `kg * m / s^2` and `kg*m/s^2` are identical.
1406    /// Space-adjacent labels without an intervening `*` or `/` end the expression;
1407    /// the bare label belongs to the next syntactic element.
1408    ///
1409    /// Examples:
1410    /// - `meter/second`            → `[{meter, +1}, {second, -1}]`
1411    /// - `meter/second^2`          → `[{meter, +1}, {second, -2}]`
1412    /// - `kg * meter / second^2`   → `[{kg, +1}, {meter, +1}, {second, -2}]`
1413    /// - `meter / second * kg`     → `[{meter, +1}, {second, -1}, {kg, +1}]`
1414    fn parse_unit_factors(&mut self) -> Result<Vec<UnitFactor>, Error> {
1415        let mut factors: Vec<UnitFactor> = Vec::new();
1416        let mut denominator_mode = false;
1417        // Tracks whether the loop is sitting immediately after an operator (* or /).
1418        // On the first iteration there is an implicit "we just started", so we allow
1419        // the first label without a preceding operator.
1420        let mut operator_just_consumed = true;
1421
1422        loop {
1423            if self.at_command_terminator()? {
1424                if !operator_just_consumed {
1425                    break;
1426                }
1427                // An operator was consumed but no label followed — that is a parse error
1428                // only if we already emitted at least one factor (dangling operator).
1429                // If factors is empty the caller will handle the missing expression.
1430                break;
1431            }
1432
1433            // `*` — explicit multiplication; reset to numerator mode.
1434            if self.at(&TokenKind::Star)? {
1435                if operator_just_consumed && !factors.is_empty() {
1436                    let bad_tok = self.next()?;
1437                    return Err(self.error_at_token(
1438                        &bad_tok,
1439                        "Unexpected '*' in unit expression: two consecutive operators".to_string(),
1440                    ));
1441                }
1442                self.next()?;
1443                denominator_mode = false;
1444                operator_just_consumed = true;
1445                continue;
1446            }
1447
1448            // `/` — switch to denominator mode.
1449            if self.at(&TokenKind::Slash)? {
1450                if operator_just_consumed && !factors.is_empty() {
1451                    let bad_tok = self.next()?;
1452                    return Err(self.error_at_token(
1453                        &bad_tok,
1454                        "Unexpected '/' in unit expression: two consecutive operators".to_string(),
1455                    ));
1456                }
1457                self.next()?;
1458                denominator_mode = true;
1459                operator_just_consumed = true;
1460                continue;
1461            }
1462
1463            // A label is a measure reference.
1464            let peek_kind = self.peek()?.kind.clone();
1465            if !can_be_label(&peek_kind) {
1466                break;
1467            }
1468
1469            // A label without a preceding operator ends the expression.
1470            // The first factor is permitted without an operator (operator_just_consumed starts true).
1471            if !operator_just_consumed {
1472                break;
1473            }
1474            operator_just_consumed = false;
1475
1476            let (measure_ref, _end_span) = self.parse_unit_path()?;
1477
1478            // Optional exponent: `^` followed by an optional `-` and an integer.
1479            let explicit_exp: Option<i32> = if self.at(&TokenKind::Caret)? {
1480                self.next()?; // consume `^`
1481
1482                let negative = if self.at(&TokenKind::Minus)? {
1483                    self.next()?; // consume `-`
1484                    true
1485                } else {
1486                    false
1487                };
1488
1489                if !self.at(&TokenKind::NumberLit)? {
1490                    let bad_tok = self.next()?;
1491                    return Err(self.error_at_token(
1492                        &bad_tok,
1493                        format!(
1494                            "Expected an integer exponent after '^' in unit expression, found {}",
1495                            bad_tok.kind
1496                        ),
1497                    ));
1498                }
1499
1500                let exp_tok = self.next()?;
1501                let raw: i32 = exp_tok.text.parse::<i32>().map_err(|_| {
1502                    self.error_at_token(
1503                        &exp_tok,
1504                        format!(
1505                            "Exponent '{}' is not a valid integer in unit expression",
1506                            exp_tok.text
1507                        ),
1508                    )
1509                })?;
1510
1511                if raw == 0 {
1512                    return Err(self.error_at_token(
1513                        &exp_tok,
1514                        "Exponent cannot be zero in a unit expression".to_string(),
1515                    ));
1516                }
1517
1518                Some(if negative { -raw } else { raw })
1519            } else {
1520                None
1521            };
1522
1523            // Apply denominator mode: negate the exponent (or its default) when in denominator.
1524            let final_exp = match (explicit_exp, denominator_mode) {
1525                (Some(exponent), true) => -exponent,
1526                (Some(exponent), false) => exponent,
1527                (None, true) => -1,
1528                (None, false) => 1,
1529            };
1530
1531            factors.push(UnitFactor {
1532                measure_ref,
1533                exp: final_exp,
1534            });
1535        }
1536
1537        Ok(factors)
1538    }
1539
1540    // ========================================================================
1541    // Meta parsing
1542    // ========================================================================
1543
1544    fn parse_meta(&mut self) -> Result<MetaField, Error> {
1545        let meta_tok = self.expect(&TokenKind::Meta)?;
1546        let start_span = meta_tok.span.clone();
1547
1548        let key_tok = self.next()?;
1549        let key = key_tok.text.clone();
1550
1551        self.expect(&TokenKind::Colon)?;
1552
1553        let value = self.parse_meta_value()?;
1554
1555        let span = self.span_covering(&start_span, &self.last_span);
1556
1557        Ok(MetaField {
1558            key,
1559            value,
1560            source_location: self.make_source(span),
1561        })
1562    }
1563
1564    fn parse_meta_value(&mut self) -> Result<MetaValue, Error> {
1565        // Try literal first (string, number, boolean, date)
1566        let peeked = self.peek()?;
1567        match &peeked.kind {
1568            TokenKind::StringLit => {
1569                let value = self.parse_literal_value()?;
1570                return Ok(MetaValue::Literal(value));
1571            }
1572            TokenKind::NumberLit => {
1573                let value = self.parse_literal_value()?;
1574                return Ok(MetaValue::Literal(value));
1575            }
1576            k if is_boolean_keyword(k) => {
1577                let value = self.parse_literal_value()?;
1578                return Ok(MetaValue::Literal(value));
1579            }
1580            _ => {}
1581        }
1582
1583        // Otherwise, consume as unquoted meta identifier
1584        // meta_identifier: (ASCII_ALPHANUMERIC | "_" | "-" | "." | "/")+
1585        let mut ident = String::new();
1586        loop {
1587            let peeked = self.peek()?;
1588            match &peeked.kind {
1589                k if k.is_identifier_like() => {
1590                    let tok = self.next()?;
1591                    ident.push_str(&tok.text);
1592                }
1593                TokenKind::Dot => {
1594                    self.next()?;
1595                    ident.push('.');
1596                }
1597                TokenKind::Slash => {
1598                    self.next()?;
1599                    ident.push('/');
1600                }
1601                TokenKind::Minus => {
1602                    self.next()?;
1603                    ident.push('-');
1604                }
1605                TokenKind::NumberLit => {
1606                    let tok = self.next()?;
1607                    ident.push_str(&tok.text);
1608                }
1609                _ => break,
1610            }
1611        }
1612
1613        if ident.is_empty() {
1614            let tok = self.peek()?.clone();
1615            return Err(self.error_at_token(&tok, "Expected a meta value"));
1616        }
1617
1618        Ok(MetaValue::Unquoted(ident))
1619    }
1620
1621    // ========================================================================
1622    // Literal value parsing
1623    // ========================================================================
1624
1625    fn parse_literal_value(&mut self) -> Result<Value, Error> {
1626        let left = self.parse_scalar_literal_value()?;
1627        if self.at(&TokenKind::Ellipsis)? {
1628            self.next()?;
1629            let right = self.parse_scalar_literal_value()?;
1630            Ok(Value::Range(Box::new(left), Box::new(right)))
1631        } else {
1632            Ok(left)
1633        }
1634    }
1635
1636    fn parse_signed_number_literal(&mut self) -> Result<Value, Error> {
1637        let sign_tok = self.next()?;
1638        let sign_span = sign_tok.span.clone();
1639        let is_negative = sign_tok.kind == TokenKind::Minus;
1640
1641        if !self.at(&TokenKind::NumberLit)? {
1642            let tok = self.peek()?.clone();
1643            return Err(self.error_at_token(
1644                &tok,
1645                format!(
1646                    "Expected a number after '{}', found '{}'",
1647                    sign_tok.text, tok.text
1648                ),
1649            ));
1650        }
1651
1652        let value = self.parse_number_literal()?;
1653        if !is_negative {
1654            return Ok(value);
1655        }
1656        match try_negate_numeric_literal(value) {
1657            Ok(negated) => Ok(negated),
1658            Err(other) => Err(Error::parsing(
1659                format!("Cannot negate this value: {}", other),
1660                self.make_source(sign_span),
1661                None::<String>,
1662            )),
1663        }
1664    }
1665
1666    fn parse_number_literal(&mut self) -> Result<Value, Error> {
1667        let num_tok = self.next()?;
1668        let num_text = &num_tok.text;
1669        let num_span = num_tok.span.clone();
1670
1671        // Check if followed by - which could make it a date (YYYY-MM-DD)
1672        if num_text.len() == 4
1673            && num_text.chars().all(|c| c.is_ascii_digit())
1674            && self.at(&TokenKind::Minus)?
1675        {
1676            return self.parse_date_literal(num_text.clone(), num_span);
1677        }
1678
1679        // Check what follows the number
1680        let peeked = self.peek()?;
1681
1682        // Number followed by : could be a time literal (HH:MM:SS)
1683        if num_text.len() == 2
1684            && num_text.chars().all(|c| c.is_ascii_digit())
1685            && peeked.kind == TokenKind::Colon
1686        {
1687            // Only if we're in a data value context... this is ambiguous.
1688            // Time literals look like: 14:30:00 or 14:30
1689            // But we might also have "rule x: expr" where : is assignment.
1690            // The grammar handles this at the grammar level. For us,
1691            // we need to check if the context is right.
1692            // Let's try to parse as time if the following pattern matches.
1693            return self.try_parse_time_literal(num_text.clone(), num_span);
1694        }
1695
1696        // Check for %% (permille) - must be before %
1697        if peeked.kind == TokenKind::PercentPercent {
1698            let pp_tok = self.next()?;
1699            // Check it's not followed by a digit
1700            if let Ok(next_peek) = self.peek() {
1701                if next_peek.kind == TokenKind::NumberLit {
1702                    return Err(self.error_at_token(
1703                        &pp_tok,
1704                        "Permille literal cannot be followed by a digit",
1705                    ));
1706                }
1707            }
1708            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1709            return Ok(Value::NumberWithUnit(decimal, "permille".to_string()));
1710        }
1711
1712        // Check for % (percent)
1713        if peeked.kind == TokenKind::Percent {
1714            let pct_tok = self.next()?;
1715            // Check it's not followed by a digit or another %
1716            if let Ok(next_peek) = self.peek() {
1717                if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
1718                    return Err(self.error_at_token(
1719                        &pct_tok,
1720                        "Percent literal cannot be followed by a digit",
1721                    ));
1722                }
1723            }
1724            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1725            return Ok(Value::NumberWithUnit(decimal, "percent".to_string()));
1726        }
1727
1728        // Check for "permille" keyword
1729        if peeked.kind == TokenKind::Permille {
1730            self.next()?; // consume "permille"
1731            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1732            return Ok(Value::NumberWithUnit(decimal, "permille".to_string()));
1733        }
1734
1735        if can_be_label(&peeked.kind) {
1736            let (unit_path, _end_span) = self.parse_unit_path()?;
1737            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1738            return Ok(Value::NumberWithUnit(decimal, unit_path));
1739        }
1740
1741        // Plain number
1742        let decimal = parse_decimal_string(num_text, &num_span, self)?;
1743        Ok(Value::Number(decimal))
1744    }
1745
1746    /// One or more labels separated by `.` (bare unit or qualified unit path).
1747    fn parse_unit_path(&mut self) -> Result<(String, Span), Error> {
1748        let first = self.next()?;
1749        if !can_be_label(&first.kind) {
1750            return Err(self.error_at_token(
1751                &first,
1752                format!("Expected a unit name, found {}", first.kind),
1753            ));
1754        }
1755        let mut path = first.text.clone();
1756        let mut end_span = first.span.clone();
1757        while self.at(&TokenKind::Dot)? {
1758            self.next()?;
1759            let seg = self.next()?;
1760            if !can_be_label(&seg.kind) {
1761                return Err(self.error_at_token(
1762                    &seg,
1763                    format!("Expected a unit path segment after '.', found {}", seg.kind),
1764                ));
1765            }
1766            path.push('.');
1767            path.push_str(&seg.text);
1768            end_span = seg.span.clone();
1769        }
1770        Ok((path, end_span))
1771    }
1772
1773    fn parse_date_literal(&mut self, year_text: String, start_span: Span) -> Result<Value, Error> {
1774        let mut dt_str = year_text;
1775
1776        // Consume -MM
1777        self.expect(&TokenKind::Minus)?;
1778        dt_str.push('-');
1779        let month_tok = self.expect(&TokenKind::NumberLit)?;
1780        dt_str.push_str(&month_tok.text);
1781
1782        // Consume -DD
1783        self.expect(&TokenKind::Minus)?;
1784        dt_str.push('-');
1785        let day_tok = self.expect(&TokenKind::NumberLit)?;
1786        dt_str.push_str(&day_tok.text);
1787
1788        // Check for T (time component)
1789        if self.at(&TokenKind::Identifier)? {
1790            let peeked = self.peek()?;
1791            if peeked.text.len() >= 2
1792                && (peeked.text.starts_with('T') || peeked.text.starts_with('t'))
1793            {
1794                // The lexer may have tokenized T14 as a single identifier
1795                let t_tok = self.next()?;
1796                dt_str.push_str(&t_tok.text);
1797
1798                // Consume :MM
1799                if self.at(&TokenKind::Colon)? {
1800                    self.next()?;
1801                    dt_str.push(':');
1802                    let min_tok = self.next()?;
1803                    dt_str.push_str(&min_tok.text);
1804
1805                    // Consume :SS and optional fractional second
1806                    if self.at(&TokenKind::Colon)? {
1807                        self.next()?;
1808                        dt_str.push(':');
1809                        let sec_tok = self.next()?;
1810                        dt_str.push_str(&sec_tok.text);
1811
1812                        // Check for fractional second .NNNNNN
1813                        if self.at(&TokenKind::Dot)? {
1814                            self.next()?;
1815                            dt_str.push('.');
1816                            let frac_tok = self.expect(&TokenKind::NumberLit)?;
1817                            dt_str.push_str(&frac_tok.text);
1818                        }
1819                    }
1820                }
1821
1822                // Check for timezone
1823                self.try_consume_timezone(&mut dt_str)?;
1824            }
1825        }
1826
1827        if let Ok(dtv) = dt_str.parse::<crate::literals::DateTimeValue>() {
1828            return Ok(Value::Date(dtv));
1829        }
1830
1831        Err(Error::parsing(
1832            format!("Invalid date/time format: '{}'", dt_str),
1833            self.make_source(start_span),
1834            None::<String>,
1835        ))
1836    }
1837
1838    fn try_consume_timezone(&mut self, dt_str: &mut String) -> Result<(), Error> {
1839        // Z timezone
1840        if self.at(&TokenKind::Identifier)? {
1841            let peeked = self.peek()?;
1842            if (peeked.text == "Z" || peeked.text == "z") && peeked.span.start == self.last_span.end
1843            {
1844                let z_tok = self.next()?;
1845                dt_str.push_str(&z_tok.text);
1846                return Ok(());
1847            }
1848        }
1849
1850        // +HH:MM or -HH:MM, only when attached directly to the preceding token.
1851        if self.at(&TokenKind::Plus)? || self.at(&TokenKind::Minus)? {
1852            let mut lookahead = self.lexer.clone();
1853            let sign_tok = lookahead.next_token()?;
1854            let hour_tok = lookahead.next_token()?;
1855            let colon_tok = lookahead.next_token()?;
1856            let minute_tok = lookahead.next_token()?;
1857
1858            let attached = sign_tok.span.start == self.last_span.end;
1859            let is_timezone_shape = hour_tok.kind == TokenKind::NumberLit
1860                && colon_tok.kind == TokenKind::Colon
1861                && minute_tok.kind == TokenKind::NumberLit;
1862
1863            if attached && is_timezone_shape {
1864                let sign_tok = self.next()?;
1865                dt_str.push_str(&sign_tok.text);
1866                let hour_tok = self.expect(&TokenKind::NumberLit)?;
1867                dt_str.push_str(&hour_tok.text);
1868                self.expect(&TokenKind::Colon)?;
1869                dt_str.push(':');
1870                let min_tok = self.expect(&TokenKind::NumberLit)?;
1871                dt_str.push_str(&min_tok.text);
1872            }
1873        }
1874
1875        Ok(())
1876    }
1877
1878    fn try_parse_time_literal(
1879        &mut self,
1880        hour_text: String,
1881        start_span: Span,
1882    ) -> Result<Value, Error> {
1883        let mut time_str = hour_text;
1884
1885        // Consume :MM
1886        self.expect(&TokenKind::Colon)?;
1887        time_str.push(':');
1888        let min_tok = self.expect(&TokenKind::NumberLit)?;
1889        time_str.push_str(&min_tok.text);
1890
1891        // Optional :SS
1892        if self.at(&TokenKind::Colon)? {
1893            self.next()?;
1894            time_str.push(':');
1895            let sec_tok = self.expect(&TokenKind::NumberLit)?;
1896            time_str.push_str(&sec_tok.text);
1897
1898            // Optional fractional second .NNNNNN
1899            if self.at(&TokenKind::Dot)? {
1900                self.next()?;
1901                time_str.push('.');
1902                let frac_tok = self.expect(&TokenKind::NumberLit)?;
1903                time_str.push_str(&frac_tok.text);
1904            }
1905        }
1906
1907        // Try timezone
1908        self.try_consume_timezone(&mut time_str)?;
1909
1910        if let Ok(t) = time_str.parse::<TimeValue>() {
1911            return Ok(Value::Time(TimeValue {
1912                hour: t.hour,
1913                minute: t.minute,
1914                second: t.second,
1915                microsecond: t.microsecond,
1916                timezone: t.timezone,
1917            }));
1918        }
1919
1920        Err(Error::parsing(
1921            format!("Invalid time format: '{}'", time_str),
1922            self.make_source(start_span),
1923            None::<String>,
1924        ))
1925    }
1926
1927    // ========================================================================
1928    // Expression parsing (Pratt parser / precedence climbing)
1929    // ========================================================================
1930
1931    fn new_expression(
1932        &mut self,
1933        kind: ExpressionKind,
1934        source: Source,
1935    ) -> Result<Expression, Error> {
1936        self.expression_count += 1;
1937        if self.expression_count > self.max_expression_count {
1938            return Err(Error::resource_limit_exceeded(
1939                "max_expression_count",
1940                self.max_expression_count.to_string(),
1941                self.expression_count.to_string(),
1942                "Split logic into multiple rules to reduce expression count",
1943                Some(source),
1944                None,
1945                None,
1946            ));
1947        }
1948        Ok(Expression::new(kind, source))
1949    }
1950
1951    fn check_depth(&mut self) -> Result<(), Error> {
1952        if let Err(actual) = self.depth_tracker.push_depth() {
1953            let span = self.peek()?.span.clone();
1954            self.depth_tracker.pop_depth();
1955            return Err(Error::resource_limit_exceeded(
1956                "max_expression_depth",
1957                self.depth_tracker.max_depth().to_string(),
1958                actual.to_string(),
1959                "Simplify nested expressions or break into separate rules",
1960                Some(self.make_source(span)),
1961                None,
1962                None,
1963            ));
1964        }
1965        Ok(())
1966    }
1967
1968    fn parse_expression(&mut self) -> Result<Expression, Error> {
1969        self.check_depth()?;
1970        let result = self.parse_and_expression();
1971        self.depth_tracker.pop_depth();
1972        result
1973    }
1974
1975    fn parse_and_expression(&mut self) -> Result<Expression, Error> {
1976        let start_span = self.peek()?.span.clone();
1977        let mut left = self.parse_and_operand()?;
1978
1979        while self.at(&TokenKind::And)? {
1980            self.next()?; // consume 'and'
1981            let right = self.parse_and_operand()?;
1982            let span = self.span_covering(
1983                &start_span,
1984                &right
1985                    .source_location
1986                    .as_ref()
1987                    .map(|s| s.span.clone())
1988                    .unwrap_or_else(|| start_span.clone()),
1989            );
1990            left = self.new_expression(
1991                ExpressionKind::LogicalAnd(Arc::new(left), Arc::new(right)),
1992                self.make_source(span),
1993            )?;
1994        }
1995
1996        Ok(left)
1997    }
1998
1999    fn at_bare_veto_token(&mut self) -> Result<bool, Error> {
2000        if !self.at(&TokenKind::Veto)? {
2001            return Ok(false);
2002        }
2003        let checkpoint = self.checkpoint();
2004        self.next()?;
2005        let bare = !self.at(&TokenKind::StringLit)?;
2006        self.restore(checkpoint);
2007        Ok(bare)
2008    }
2009
2010    fn at_bare_veto_followed_by_is(&mut self) -> Result<bool, Error> {
2011        if !self.at_bare_veto_token()? {
2012            return Ok(false);
2013        }
2014        let checkpoint = self.checkpoint();
2015        self.next()?;
2016        let followed = self.at(&TokenKind::Is)?;
2017        self.restore(checkpoint);
2018        Ok(followed)
2019    }
2020
2021    fn at_not_bare_veto_followed_by_is(&mut self) -> Result<bool, Error> {
2022        if !self.at(&TokenKind::Not)? {
2023            return Ok(false);
2024        }
2025        let checkpoint = self.checkpoint();
2026        self.next()?;
2027        if !self.at(&TokenKind::Veto)? {
2028            self.restore(checkpoint);
2029            return Ok(false);
2030        }
2031        self.next()?;
2032        if self.at(&TokenKind::StringLit)? {
2033            self.restore(checkpoint);
2034            return Ok(false);
2035        }
2036        let followed = self.at(&TokenKind::Is)?;
2037        self.restore(checkpoint);
2038        Ok(followed)
2039    }
2040
2041    fn wrap_result_is_veto_expression(
2042        &mut self,
2043        operand: Expression,
2044        operator_is_not: bool,
2045        keyword_was_negated: bool,
2046        start_span: Span,
2047    ) -> Result<Expression, Error> {
2048        let negate = operator_is_not ^ keyword_was_negated;
2049        let end_span = operand
2050            .source_location
2051            .as_ref()
2052            .map(|source| source.span.clone())
2053            .unwrap_or_else(|| start_span.clone());
2054        let span = self.span_covering(&start_span, &end_span);
2055        let core = self.new_expression(
2056            ExpressionKind::ResultIsVeto(Arc::new(operand)),
2057            self.make_source(span.clone()),
2058        )?;
2059        if negate {
2060            self.new_expression(
2061                ExpressionKind::LogicalNegation(Arc::new(core), NegationType::Not),
2062                self.make_source(span),
2063            )
2064        } else {
2065            Ok(core)
2066        }
2067    }
2068
2069    fn parse_veto_status_lhs_is_comparison(&mut self) -> Result<Expression, Error> {
2070        let start_span = self.peek()?.span.clone();
2071        let keyword_was_negated = if self.at(&TokenKind::Not)? {
2072            self.next()?;
2073            true
2074        } else {
2075            false
2076        };
2077        self.expect(&TokenKind::Veto)?;
2078        if self.at(&TokenKind::StringLit)? {
2079            let tok = self.peek()?.clone();
2080            return Err(self.error_at_token(
2081                &tok,
2082                "veto with a message is only valid as a rule or unless result, not in `is veto` comparisons",
2083            ));
2084        }
2085        let operator = self.parse_comparison_operator()?;
2086        let operator_is_not = matches!(operator, ComparisonComputation::IsNot);
2087        if !matches!(
2088            operator,
2089            ComparisonComputation::Is | ComparisonComputation::IsNot
2090        ) {
2091            let tok = self.peek()?.clone();
2092            return Err(self.error_at_token(
2093                &tok,
2094                "Expected `is` or `is not` after `veto` in a veto-status comparison",
2095            ));
2096        }
2097        let operand = self.parse_range_expression()?;
2098        self.wrap_result_is_veto_expression(
2099            operand,
2100            operator_is_not,
2101            keyword_was_negated,
2102            start_span,
2103        )
2104    }
2105
2106    fn parse_and_operand(&mut self) -> Result<Expression, Error> {
2107        if self.at_not_bare_veto_followed_by_is()? || self.at_bare_veto_followed_by_is()? {
2108            return self.parse_veto_status_lhs_is_comparison();
2109        }
2110
2111        // not expression
2112        if self.at(&TokenKind::Not)? {
2113            return self.parse_not_expression();
2114        }
2115
2116        // repository_with_suffix: repository_expression followed by optional suffix
2117        self.parse_repository_with_suffix()
2118    }
2119
2120    fn parse_not_expression(&mut self) -> Result<Expression, Error> {
2121        let not_tok = self.expect(&TokenKind::Not)?;
2122        let start_span = not_tok.span.clone();
2123
2124        self.check_depth()?;
2125        let operand = self.parse_and_operand()?;
2126        self.depth_tracker.pop_depth();
2127
2128        let end_span = operand
2129            .source_location
2130            .as_ref()
2131            .map(|s| s.span.clone())
2132            .unwrap_or_else(|| start_span.clone());
2133        let span = self.span_covering(&start_span, &end_span);
2134
2135        self.new_expression(
2136            ExpressionKind::LogicalNegation(Arc::new(operand), NegationType::Not),
2137            self.make_source(span),
2138        )
2139    }
2140
2141    fn parse_repository_with_suffix(&mut self) -> Result<Expression, Error> {
2142        let start_span = self.peek()?.span.clone();
2143        let repository = self.parse_range_expression()?;
2144        self.continue_repository_operand(repository, start_span)
2145    }
2146
2147    /// Postfix suffixes on a completed repository/range expression (`in`, calendar, comparison, `as`).
2148    fn continue_repository_operand(
2149        &mut self,
2150        mut expr: Expression,
2151        start_span: Span,
2152    ) -> Result<Expression, Error> {
2153        loop {
2154            let peeked = self.peek()?;
2155
2156            if is_comparison_operator(&peeked.kind) {
2157                return self.parse_comparison_suffix(expr, start_span);
2158            }
2159
2160            if peeked.kind == TokenKind::Not {
2161                expr = self.parse_not_in_calendar_suffix(expr, start_span.clone())?;
2162                continue;
2163            }
2164
2165            if peeked.kind == TokenKind::In {
2166                expr = self.parse_in_suffix(expr, start_span.clone())?;
2167                continue;
2168            }
2169
2170            if peeked.kind == TokenKind::As {
2171                expr = self.parse_as_chain(expr, start_span.clone())?;
2172                continue;
2173            }
2174
2175            break;
2176        }
2177
2178        if self.at_expression_suffix_end()? {
2179            return Ok(expr);
2180        }
2181
2182        let tok = self.peek()?.clone();
2183        Err(self.error_at_token(
2184            &tok,
2185            format!("Unexpected token '{}' after expression", tok.text),
2186        ))
2187    }
2188
2189    fn parse_comparison_suffix(
2190        &mut self,
2191        left: Expression,
2192        start_span: Span,
2193    ) -> Result<Expression, Error> {
2194        let operator = self.parse_comparison_operator()?;
2195        let operator_is_not = matches!(operator, ComparisonComputation::IsNot);
2196
2197        if matches!(
2198            operator,
2199            ComparisonComputation::Is | ComparisonComputation::IsNot
2200        ) && self.at_bare_veto_token()?
2201        {
2202            self.expect(&TokenKind::Veto)?;
2203            if self.at(&TokenKind::StringLit)? {
2204                let tok = self.peek()?.clone();
2205                return Err(self.error_at_token(
2206                    &tok,
2207                    "veto with a message is only valid as a rule or unless result, not in `is veto` comparisons",
2208                ));
2209            }
2210            return self.wrap_result_is_veto_expression(left, operator_is_not, false, start_span);
2211        }
2212
2213        // Right side can be: not_expr | range/repository expression (term-level `as` included)
2214        let right = if self.at(&TokenKind::Not)? {
2215            self.parse_not_expression()?
2216        } else {
2217            self.parse_range_expression()?
2218        };
2219
2220        let end_span = right
2221            .source_location
2222            .as_ref()
2223            .map(|s| s.span.clone())
2224            .unwrap_or_else(|| start_span.clone());
2225        let span = self.span_covering(&start_span, &end_span);
2226
2227        self.new_expression(
2228            ExpressionKind::Comparison(Arc::new(left), operator, Arc::new(right)),
2229            self.make_source(span),
2230        )
2231    }
2232
2233    fn parse_comparison_operator(&mut self) -> Result<ComparisonComputation, Error> {
2234        let tok = self.next()?;
2235        match tok.kind {
2236            TokenKind::Gt => Ok(ComparisonComputation::GreaterThan),
2237            TokenKind::Lt => Ok(ComparisonComputation::LessThan),
2238            TokenKind::Gte => Ok(ComparisonComputation::GreaterThanOrEqual),
2239            TokenKind::Lte => Ok(ComparisonComputation::LessThanOrEqual),
2240            TokenKind::Is => {
2241                // Check for "is not"
2242                if self.at(&TokenKind::Not)? {
2243                    self.next()?; // consume 'not'
2244                    Ok(ComparisonComputation::IsNot)
2245                } else {
2246                    Ok(ComparisonComputation::Is)
2247                }
2248            }
2249            _ => Err(self.error_at_token(
2250                &tok,
2251                format!("Expected a comparison operator, found {}", tok.kind),
2252            )),
2253        }
2254    }
2255
2256    fn parse_not_in_calendar_suffix(
2257        &mut self,
2258        repository: Expression,
2259        start_span: Span,
2260    ) -> Result<Expression, Error> {
2261        self.expect(&TokenKind::Not)?;
2262        self.expect(&TokenKind::In)?;
2263        self.expect_calendar_period_marker()?;
2264        let unit = self.parse_calendar_unit()?;
2265        let end = self.peek()?.span.clone();
2266        let span = self.span_covering(&start_span, &end);
2267        self.new_expression(
2268            ExpressionKind::DateCalendar(DateCalendarKind::NotIn, unit, Arc::new(repository)),
2269            self.make_source(span),
2270        )
2271    }
2272
2273    fn parse_in_suffix(
2274        &mut self,
2275        repository: Expression,
2276        start_span: Span,
2277    ) -> Result<Expression, Error> {
2278        self.expect(&TokenKind::In)?;
2279
2280        let peeked = self.peek()?;
2281
2282        // "in past calendar <unit>" or "in future calendar <unit>"
2283        if peeked.kind == TokenKind::Past || peeked.kind == TokenKind::Future {
2284            let direction = self.next()?;
2285            let rel_kind = if direction.kind == TokenKind::Past {
2286                DateRelativeKind::InPast
2287            } else {
2288                DateRelativeKind::InFuture
2289            };
2290
2291            // Check for "calendar" keyword
2292            if self.at_calendar_period_marker()? {
2293                self.next_calendar_period_marker()?;
2294                let cal_kind = if direction.kind == TokenKind::Past {
2295                    DateCalendarKind::Past
2296                } else {
2297                    DateCalendarKind::Future
2298                };
2299                let unit = self.parse_calendar_unit()?;
2300                let end = self.peek()?.span.clone();
2301                let span = self.span_covering(&start_span, &end);
2302                return self.new_expression(
2303                    ExpressionKind::DateCalendar(cal_kind, unit, Arc::new(repository)),
2304                    self.make_source(span),
2305                );
2306            }
2307
2308            if self.at(&TokenKind::And)?
2309                || self.at(&TokenKind::Unless)?
2310                || self.at(&TokenKind::Then)?
2311                || self.at(&TokenKind::RParen)?
2312                || self.at(&TokenKind::Eof)?
2313                || is_comparison_operator(&self.peek()?.kind)
2314            {
2315                let end = self.peek()?.span.clone();
2316                let span = self.span_covering(&start_span, &end);
2317                return self.new_expression(
2318                    ExpressionKind::DateRelative(rel_kind, Arc::new(repository)),
2319                    self.make_source(span),
2320                );
2321            }
2322
2323            let offset = self.parse_repository_expression()?;
2324            let offset_end_span = offset
2325                .source_location
2326                .as_ref()
2327                .map(|s| s.span.clone())
2328                .unwrap_or_else(|| start_span.clone());
2329            let range = self.new_expression(
2330                ExpressionKind::PastFutureRange(rel_kind, Arc::new(offset)),
2331                self.make_source(self.span_covering(&direction.span, &offset_end_span)),
2332            )?;
2333            let span = self.span_covering(&start_span, &offset_end_span);
2334            return self.new_expression(
2335                ExpressionKind::RangeContainment(Arc::new(repository), Arc::new(range)),
2336                self.make_source(span),
2337            );
2338        }
2339
2340        // "in calendar <unit>"
2341        if token_is_calendar_period_marker(peeked) {
2342            self.next_calendar_period_marker()?;
2343            let unit = self.parse_calendar_unit()?;
2344            let end = self.peek()?.span.clone();
2345            let span = self.span_covering(&start_span, &end);
2346            return self.new_expression(
2347                ExpressionKind::DateCalendar(DateCalendarKind::Current, unit, Arc::new(repository)),
2348                self.make_source(span),
2349            );
2350        }
2351
2352        let range = self.parse_range_expression()?;
2353        let end_span = range
2354            .source_location
2355            .as_ref()
2356            .map(|s| s.span.clone())
2357            .unwrap_or_else(|| start_span.clone());
2358        let span = self.span_covering(&start_span, &end_span);
2359        self.new_expression(
2360            ExpressionKind::RangeContainment(Arc::new(repository), Arc::new(range)),
2361            self.make_source(span),
2362        )
2363    }
2364
2365    fn parse_as_chain(
2366        &mut self,
2367        mut expr: Expression,
2368        start_span: Span,
2369    ) -> Result<Expression, Error> {
2370        while self.at(&TokenKind::As)? {
2371            self.expect(&TokenKind::As)?;
2372            let target_tok = self.next()?;
2373            let target = if matches!(target_tok.kind, TokenKind::Permille) {
2374                ConversionTarget::Unit {
2375                    unit_name: "permille".to_string(),
2376                }
2377            } else if let Some(primitive) = token_kind_to_primitive(&target_tok.kind) {
2378                ConversionTarget::Type(primitive)
2379            } else if can_be_label(&target_tok.kind) {
2380                let mut unit_path = target_tok.text.clone();
2381                let mut end_span = target_tok.span.clone();
2382                while self.at(&TokenKind::Dot)? {
2383                    self.next()?;
2384                    let seg = self.next()?;
2385                    if !can_be_label(&seg.kind) {
2386                        return Err(self.error_at_token(
2387                            &seg,
2388                            format!("Expected a unit path segment after '.', found {}", seg.kind),
2389                        ));
2390                    }
2391                    unit_path.push('.');
2392                    unit_path.push_str(&seg.text);
2393                    end_span = seg.span.clone();
2394                }
2395                let target = ConversionTarget::Unit {
2396                    unit_name: unit_path,
2397                };
2398                expr = self.new_expression(
2399                    ExpressionKind::UnitConversion(Arc::new(expr), target),
2400                    self.make_source(self.span_covering(&start_span, &end_span)),
2401                )?;
2402                continue;
2403            } else {
2404                return Err(self.error_at_token(
2405                    &target_tok,
2406                    format!(
2407                        "Expected a type keyword or unit name after 'as', found {}",
2408                        target_tok.kind
2409                    ),
2410                ));
2411            };
2412            expr = self.new_expression(
2413                ExpressionKind::UnitConversion(Arc::new(expr), target),
2414                self.make_source(self.span_covering(&start_span, &target_tok.span)),
2415            )?;
2416        }
2417        Ok(expr)
2418    }
2419
2420    fn is_plain_number_literal(expr: &Expression) -> bool {
2421        matches!(expr.kind, ExpressionKind::Literal(Value::Number(_)))
2422    }
2423
2424    fn is_unit_conversion(expr: &Expression) -> bool {
2425        matches!(expr.kind, ExpressionKind::UnitConversion(..))
2426    }
2427
2428    /// True when the next token can follow a completed suffix expression (no further operands).
2429    ///
2430    /// Must include every token that can start the next spec-body item, a new `spec`/`repo`,
2431    /// or end the file. See `parse_unit_conversion_before_expression_boundaries` in `parsing/mod.rs`.
2432    fn at_expression_suffix_end(&mut self) -> Result<bool, Error> {
2433        Ok(self.at(&TokenKind::And)?
2434            || self.at(&TokenKind::Unless)?
2435            || self.at(&TokenKind::Then)?
2436            || self.at(&TokenKind::RParen)?
2437            || self.at(&TokenKind::Eof)?
2438            || self.at(&TokenKind::Spec)?
2439            || self.at(&TokenKind::Repo)?
2440            || self.at(&TokenKind::Uses)?
2441            || is_spec_body_keyword(&self.peek()?.kind))
2442    }
2443
2444    fn parse_calendar_unit(&mut self) -> Result<CalendarPeriodUnit, Error> {
2445        let tok = self.next()?;
2446        if let Some(unit) = CalendarPeriodUnit::from_keyword(&tok.text) {
2447            return Ok(unit);
2448        }
2449        Err(self.error_at_token(
2450            &tok,
2451            format!("Expected 'year', 'month', or 'week', found '{}'", tok.text),
2452        ))
2453    }
2454
2455    // ========================================================================
2456    // Arithmetic expressions (precedence climbing)
2457    // ========================================================================
2458
2459    fn parse_range_expression(&mut self) -> Result<Expression, Error> {
2460        self.parse_repository_expression()
2461    }
2462
2463    /// Atom or range-typed value: `...` binds before `^`, `*`, `/`, `%` on the same operand.
2464    /// Both endpoints use [`Self::parse_range_ellipsis_bound`] (`+`/`-` only) so
2465    /// `now - 7 day...now` and `start...start + length` are valid, and
2466    /// `rate * period_start...period_end` keeps `*` outside the range.
2467    fn parse_range_operand(&mut self) -> Result<Expression, Error> {
2468        let start_span = self.peek()?.span.clone();
2469        let checkpoint = self.checkpoint();
2470        let left = self.parse_range_ellipsis_bound()?;
2471        if !self.at(&TokenKind::Ellipsis)? {
2472            self.restore(checkpoint);
2473            return self.parse_factor();
2474        }
2475
2476        self.next()?;
2477        let right = self.parse_range_ellipsis_bound()?;
2478        let end_span = right
2479            .source_location
2480            .as_ref()
2481            .map(|s| s.span.clone())
2482            .unwrap_or_else(|| start_span.clone());
2483        let span = self.span_covering(&start_span, &end_span);
2484        self.new_expression(
2485            ExpressionKind::RangeLiteral(Arc::new(left), Arc::new(right)),
2486            self.make_source(span),
2487        )
2488    }
2489
2490    /// One side of `...`: `+`/`-` between powers only (no `*`/`/`/`%` — those bind outside the range).
2491    fn parse_range_ellipsis_bound(&mut self) -> Result<Expression, Error> {
2492        let start_span = self.peek()?.span.clone();
2493        let mut left = self.parse_power_for_range_bound()?;
2494
2495        while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
2496            let op_tok = self.next()?;
2497            let operation = match op_tok.kind {
2498                TokenKind::Plus => ArithmeticComputation::Add,
2499                TokenKind::Minus => ArithmeticComputation::Subtract,
2500                _ => unreachable!("BUG: only + and - should reach here"),
2501            };
2502
2503            let right = self.parse_power_for_range_bound()?;
2504            let end_span = right
2505                .source_location
2506                .as_ref()
2507                .map(|s| s.span.clone())
2508                .unwrap_or_else(|| start_span.clone());
2509            let span = self.span_covering(&start_span, &end_span);
2510
2511            left = self.new_expression(
2512                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2513                self.make_source(span),
2514            )?;
2515        }
2516
2517        Ok(left)
2518    }
2519
2520    fn parse_power_for_range_bound(&mut self) -> Result<Expression, Error> {
2521        let start_span = self.peek()?.span.clone();
2522        let left = self.parse_factor()?;
2523
2524        if self.at(&TokenKind::Caret)? {
2525            self.next()?;
2526            self.check_depth()?;
2527            let right = self.parse_power_for_range_bound()?;
2528            self.depth_tracker.pop_depth();
2529            let end_span = right
2530                .source_location
2531                .as_ref()
2532                .map(|s| s.span.clone())
2533                .unwrap_or_else(|| start_span.clone());
2534            let span = self.span_covering(&start_span, &end_span);
2535
2536            return self.new_expression(
2537                ExpressionKind::Arithmetic(
2538                    Arc::new(left),
2539                    ArithmeticComputation::Power,
2540                    Arc::new(right),
2541                ),
2542                self.make_source(span),
2543            );
2544        }
2545
2546        Ok(left)
2547    }
2548
2549    fn parse_repository_expression(&mut self) -> Result<Expression, Error> {
2550        let start_span = self.peek()?.span.clone();
2551        let mut left = self.parse_term()?;
2552
2553        while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
2554            // Check if this minus is really a binary operator or could be part of something else
2555            // In "X not in calendar year", we don't want to consume "not" as an operator
2556            let op_tok = self.next()?;
2557            let operation = match op_tok.kind {
2558                TokenKind::Plus => ArithmeticComputation::Add,
2559                TokenKind::Minus => ArithmeticComputation::Subtract,
2560                _ => unreachable!("BUG: only + and - should reach here"),
2561            };
2562
2563            let right = self.parse_term()?;
2564            if Self::is_plain_number_literal(&left) && Self::is_unit_conversion(&right) {
2565                let source = right
2566                    .source_location
2567                    .clone()
2568                    .unwrap_or_else(|| self.make_source(start_span.clone()));
2569                return Err(Error::parsing(
2570                    "Cannot add a plain number to a converted value; convert each operand before \
2571                     '+' (e.g. '5 as usd + c as usd')",
2572                    source,
2573                    None::<String>,
2574                ));
2575            }
2576
2577            let end_span = right
2578                .source_location
2579                .as_ref()
2580                .map(|s| s.span.clone())
2581                .unwrap_or_else(|| start_span.clone());
2582            let span = self.span_covering(&start_span, &end_span);
2583
2584            left = self.new_expression(
2585                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2586                self.make_source(span),
2587            )?;
2588        }
2589
2590        Ok(left)
2591    }
2592
2593    fn parse_term(&mut self) -> Result<Expression, Error> {
2594        self.parse_term_with_as(true)
2595    }
2596
2597    fn parse_term_with_as(&mut self, allow_as: bool) -> Result<Expression, Error> {
2598        let start_span = self.peek()?.span.clone();
2599        let mut left = self.parse_power()?;
2600        if allow_as {
2601            left = self.parse_as_chain(left, start_span.clone())?;
2602        }
2603
2604        while self.at_any(&[TokenKind::Star, TokenKind::Slash, TokenKind::Percent])? {
2605            // Be careful: % could be a percent literal suffix (e.g. 50%)
2606            // But here in term context, it's modulo since we already parsed the number
2607            let op_tok = self.next()?;
2608            let operation = match op_tok.kind {
2609                TokenKind::Star => ArithmeticComputation::Multiply,
2610                TokenKind::Slash => ArithmeticComputation::Divide,
2611                TokenKind::Percent => ArithmeticComputation::Modulo,
2612                _ => unreachable!("BUG: only *, /, % should reach here"),
2613            };
2614
2615            let right_start_span = self.peek()?.span.clone();
2616            let mut right = self.parse_power()?;
2617            if allow_as {
2618                right = self.parse_as_chain(right, right_start_span)?;
2619            }
2620            let end_span = right
2621                .source_location
2622                .as_ref()
2623                .map(|s| s.span.clone())
2624                .unwrap_or_else(|| start_span.clone());
2625            let span = self.span_covering(&start_span, &end_span);
2626
2627            left = self.new_expression(
2628                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2629                self.make_source(span),
2630            )?;
2631        }
2632
2633        Ok(left)
2634    }
2635
2636    fn parse_power(&mut self) -> Result<Expression, Error> {
2637        let start_span = self.peek()?.span.clone();
2638        let left = self.parse_range_operand()?;
2639
2640        if self.at(&TokenKind::Caret)? {
2641            self.next()?;
2642            self.check_depth()?;
2643            let right = self.parse_power()?;
2644            self.depth_tracker.pop_depth();
2645            let end_span = right
2646                .source_location
2647                .as_ref()
2648                .map(|s| s.span.clone())
2649                .unwrap_or_else(|| start_span.clone());
2650            let span = self.span_covering(&start_span, &end_span);
2651
2652            return self.new_expression(
2653                ExpressionKind::Arithmetic(
2654                    Arc::new(left),
2655                    ArithmeticComputation::Power,
2656                    Arc::new(right),
2657                ),
2658                self.make_source(span),
2659            );
2660        }
2661
2662        Ok(left)
2663    }
2664
2665    fn parse_factor(&mut self) -> Result<Expression, Error> {
2666        let peeked = self.peek()?;
2667        let start_span = peeked.span.clone();
2668
2669        if peeked.kind == TokenKind::Minus {
2670            self.next()?;
2671            let operand = self.parse_primary_or_math()?;
2672            let end_span = operand
2673                .source_location
2674                .as_ref()
2675                .map(|s| s.span.clone())
2676                .unwrap_or_else(|| start_span.clone());
2677            let span = self.span_covering(&start_span, &end_span);
2678
2679            if let ExpressionKind::Literal(value) = &operand.kind {
2680                if let Ok(negated) = try_negate_numeric_literal(value.clone()) {
2681                    return self
2682                        .new_expression(ExpressionKind::Literal(negated), self.make_source(span));
2683                }
2684            }
2685
2686            let zero = self.new_expression(
2687                ExpressionKind::Literal(Value::Number(Decimal::ZERO)),
2688                self.make_source(start_span),
2689            )?;
2690            return self.new_expression(
2691                ExpressionKind::Arithmetic(
2692                    Arc::new(zero),
2693                    ArithmeticComputation::Subtract,
2694                    Arc::new(operand),
2695                ),
2696                self.make_source(span),
2697            );
2698        }
2699
2700        if peeked.kind == TokenKind::Plus {
2701            self.next()?;
2702            return self.parse_primary_or_math();
2703        }
2704
2705        self.parse_primary_or_math()
2706    }
2707
2708    fn parse_primary_or_math(&mut self) -> Result<Expression, Error> {
2709        let peeked = self.peek()?;
2710
2711        // Math functions
2712        if is_math_function(&peeked.kind) {
2713            return self.parse_math_function();
2714        }
2715
2716        self.parse_primary()
2717    }
2718
2719    fn parse_math_function(&mut self) -> Result<Expression, Error> {
2720        let func_tok = self.next()?;
2721        let start_span = func_tok.span.clone();
2722
2723        let operator = match func_tok.kind {
2724            TokenKind::Sqrt => MathematicalComputation::Sqrt,
2725            TokenKind::Sin => MathematicalComputation::Sin,
2726            TokenKind::Cos => MathematicalComputation::Cos,
2727            TokenKind::Tan => MathematicalComputation::Tan,
2728            TokenKind::Asin => MathematicalComputation::Asin,
2729            TokenKind::Acos => MathematicalComputation::Acos,
2730            TokenKind::Atan => MathematicalComputation::Atan,
2731            TokenKind::Log => MathematicalComputation::Log,
2732            TokenKind::Exp => MathematicalComputation::Exp,
2733            TokenKind::Abs => MathematicalComputation::Abs,
2734            TokenKind::Floor => MathematicalComputation::Floor,
2735            TokenKind::Ceil => MathematicalComputation::Ceil,
2736            TokenKind::Round => MathematicalComputation::Round,
2737            _ => unreachable!("BUG: only math functions should reach here"),
2738        };
2739
2740        self.check_depth()?;
2741        let operand = self.parse_repository_expression()?;
2742        self.depth_tracker.pop_depth();
2743
2744        let end_span = operand
2745            .source_location
2746            .as_ref()
2747            .map(|s| s.span.clone())
2748            .unwrap_or_else(|| start_span.clone());
2749        let span = self.span_covering(&start_span, &end_span);
2750
2751        self.new_expression(
2752            ExpressionKind::MathematicalComputation(operator, Arc::new(operand)),
2753            self.make_source(span),
2754        )
2755    }
2756
2757    fn parse_primary(&mut self) -> Result<Expression, Error> {
2758        let peeked = self.peek()?;
2759        let start_span = peeked.span.clone();
2760
2761        match &peeked.kind {
2762            // Parenthesized expression
2763            TokenKind::LParen => {
2764                self.next()?; // consume (
2765                let inner = self.parse_expression()?;
2766                self.expect(&TokenKind::RParen)?;
2767                Ok(inner)
2768            }
2769
2770            // Now keyword
2771            TokenKind::Now => {
2772                let tok = self.next()?;
2773                self.new_expression(ExpressionKind::Now, self.make_source(tok.span))
2774            }
2775
2776            TokenKind::Past | TokenKind::Future => {
2777                let tok = self.next()?;
2778                let kind = if tok.kind == TokenKind::Past {
2779                    DateRelativeKind::InPast
2780                } else {
2781                    DateRelativeKind::InFuture
2782                };
2783                let offset = self.parse_repository_expression()?;
2784                let span = self.span_covering(
2785                    &start_span,
2786                    &offset
2787                        .source_location
2788                        .as_ref()
2789                        .map(|s| s.span.clone())
2790                        .unwrap_or(start_span.clone()),
2791                );
2792                self.new_expression(
2793                    ExpressionKind::PastFutureRange(kind, Arc::new(offset)),
2794                    self.make_source(span),
2795                )
2796            }
2797
2798            // String literal
2799            TokenKind::StringLit => {
2800                let tok = self.next()?;
2801                let content = unquote_string(&tok.text);
2802                self.new_expression(
2803                    ExpressionKind::Literal(Value::Text(content)),
2804                    self.make_source(tok.span),
2805                )
2806            }
2807
2808            // Boolean literals
2809            k if is_boolean_keyword(k) => {
2810                let tok = self.next()?;
2811                self.new_expression(
2812                    ExpressionKind::Literal(Value::Boolean(token_kind_to_boolean_value(&tok.kind))),
2813                    self.make_source(tok.span),
2814                )
2815            }
2816
2817            // Number literal (could be: plain number, date, time, duration, percent, unit)
2818            TokenKind::NumberLit => self.parse_number_expression(),
2819
2820            // Reference (identifier, type keyword)
2821            k if can_be_label(k) => {
2822                let reference = self.parse_expression_reference()?;
2823                let span = self.span_covering(&start_span, &self.last_span);
2824                self.new_expression(ExpressionKind::Reference(reference), self.make_source(span))
2825            }
2826
2827            _ => {
2828                let tok = self.next()?;
2829                Err(self.error_at_token(
2830                    &tok,
2831                    format!("Expected an expression, found '{}'", tok.text),
2832                ))
2833            }
2834        }
2835    }
2836
2837    fn parse_number_expression(&mut self) -> Result<Expression, Error> {
2838        let num_tok = self.next()?;
2839        let num_text = num_tok.text.clone();
2840        let start_span = num_tok.span.clone();
2841
2842        // Check if this is a date literal (YYYY-MM-DD)
2843        if num_text.len() == 4
2844            && num_text.chars().all(|c| c.is_ascii_digit())
2845            && self.at(&TokenKind::Minus)?
2846        {
2847            // Peek further: if next-next is a number, this is likely a date
2848            // We need to be careful: "2024 - 5" is arithmetic, "2024-01-15" is a date
2849            // Date format requires: YYYY-MM-DD where MM and DD are 2 digits
2850            // This is ambiguous at the token level. Let's check if the pattern matches.
2851            // Since dates use -NN- pattern and arithmetic uses - N pattern (with spaces),
2852            // we can use the span positions to disambiguate.
2853            let minus_span = self.peek()?.span.clone();
2854            // If minus is immediately adjacent to the number (no space), it's a date
2855            if minus_span.start == start_span.end {
2856                let value = self.parse_date_literal(num_text, start_span.clone())?;
2857                return self
2858                    .new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
2859            }
2860        }
2861
2862        // Check for time literal (HH:MM:SS)
2863        if num_text.len() == 2
2864            && num_text.chars().all(|c| c.is_ascii_digit())
2865            && self.at(&TokenKind::Colon)?
2866        {
2867            let colon_span = self.peek()?.span.clone();
2868            if colon_span.start == start_span.end {
2869                let value = self.try_parse_time_literal(num_text, start_span.clone())?;
2870                return self
2871                    .new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
2872            }
2873        }
2874
2875        // Check for %% (permille)
2876        if self.at(&TokenKind::PercentPercent)? {
2877            let pp_tok = self.next()?;
2878            if let Ok(next_peek) = self.peek() {
2879                if next_peek.kind == TokenKind::NumberLit {
2880                    return Err(self.error_at_token(
2881                        &pp_tok,
2882                        "Permille literal cannot be followed by a digit",
2883                    ));
2884                }
2885            }
2886            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2887            return self.new_expression(
2888                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
2889                self.make_source(start_span),
2890            );
2891        }
2892
2893        // Check for % (percent)
2894        if self.at(&TokenKind::Percent)? {
2895            let pct_span = self.peek()?.span.clone();
2896            // Only consume % if it's directly adjacent (no space) for the shorthand syntax
2897            // Or if it's "50 %" (space separated is also valid per the grammar)
2898            let pct_tok = self.next()?;
2899            if let Ok(next_peek) = self.peek() {
2900                if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
2901                    return Err(self.error_at_token(
2902                        &pct_tok,
2903                        "Percent literal cannot be followed by a digit",
2904                    ));
2905                }
2906            }
2907            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2908            return self.new_expression(
2909                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "percent".to_string())),
2910                self.make_source(self.span_covering(&start_span, &pct_span)),
2911            );
2912        }
2913
2914        // Check for "permille" keyword
2915        if self.at(&TokenKind::Permille)? {
2916            self.next()?;
2917            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2918            return self.new_expression(
2919                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
2920                self.make_source(start_span),
2921            );
2922        }
2923
2924        if can_be_label(&self.peek()?.kind) {
2925            let (unit_path, end_span) = self.parse_unit_path()?;
2926            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2927            return self.new_expression(
2928                ExpressionKind::Literal(Value::NumberWithUnit(decimal, unit_path)),
2929                self.make_source(self.span_covering(&start_span, &end_span)),
2930            );
2931        }
2932
2933        // Plain number
2934        let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2935        self.new_expression(
2936            ExpressionKind::Literal(Value::Number(decimal)),
2937            self.make_source(start_span),
2938        )
2939    }
2940
2941    fn parse_expression_reference(&mut self) -> Result<Reference, Error> {
2942        let mut segments = Vec::new();
2943
2944        let first = self.next()?;
2945        segments.push(first.text.clone());
2946
2947        while self.at(&TokenKind::Dot)? {
2948            self.next()?; // consume .
2949            let seg = self.next()?;
2950            if !can_be_label(&seg.kind) {
2951                return Err(self.error_at_token(
2952                    &seg,
2953                    format!("Expected an identifier after '.', found {}", seg.kind),
2954                ));
2955            }
2956            segments.push(seg.text.clone());
2957        }
2958
2959        Ok(Reference::from_path(segments))
2960    }
2961}
2962
2963// ============================================================================
2964// Helper functions
2965// ============================================================================
2966
2967fn unquote_string(s: &str) -> String {
2968    if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
2969        s[1..s.len() - 1].to_string()
2970    } else {
2971        s.to_string()
2972    }
2973}
2974
2975fn parse_decimal_string(text: &str, span: &Span, parser: &Parser) -> Result<Decimal, Error> {
2976    text.parse::<crate::literals::NumberLiteral>()
2977        .map(|parsed| parsed.0)
2978        .map_err(|message| {
2979            Error::parsing(message, parser.make_source(span.clone()), None::<String>)
2980        })
2981}
2982
2983/// Negate a numeric literal value. Returns `Err(value)` when the value is not a number.
2984fn try_negate_numeric_literal(value: Value) -> Result<Value, Value> {
2985    match value {
2986        Value::Number(d) => Ok(Value::Number(-d)),
2987        Value::NumberWithUnit(d, unit) => Ok(Value::NumberWithUnit(-d, unit)),
2988        other => Err(other),
2989    }
2990}
2991
2992fn is_comparison_operator(kind: &TokenKind) -> bool {
2993    matches!(
2994        kind,
2995        TokenKind::Gt | TokenKind::Lt | TokenKind::Gte | TokenKind::Lte | TokenKind::Is
2996    )
2997}
2998
2999// Helper trait for TokenKind
3000impl TokenKind {
3001    fn is_identifier_like(&self) -> bool {
3002        matches!(self, TokenKind::Identifier)
3003            || can_be_label(self)
3004            || is_boolean_keyword(self)
3005            || is_math_function(self)
3006    }
3007}