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