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