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            return Err(self.error_at_token(
1248                &name_tok,
1249                format!("Expected a command name, found {}", name_tok.kind),
1250            ));
1251        }
1252        let cmd = try_parse_type_constraint_command(&name_tok.text).ok_or_else(|| {
1253            self.error_at_token(
1254                &name_tok,
1255                format!(
1256                    "Unknown constraint command '{}'. Valid commands: help, suggest, fill, unit, trait, minimum, maximum, decimals, option, options, length",
1257                    name_tok.text
1258                ),
1259            )
1260        })?;
1261
1262        match cmd.continuation_shape() {
1263            ContinuationShape::Assignment => {
1264                let (args, deprecated_without_colon) = self.parse_unit_as_assignment()?;
1265                Ok((cmd, args, deprecated_without_colon))
1266            }
1267            ContinuationShape::SpaceSeparated => {
1268                Ok((cmd, self.parse_generic_command_args()?, false))
1269            }
1270        }
1271    }
1272
1273    /// Shared spine for assignment continuations (`with`, `unit`): `key: value` or deprecated space.
1274    fn parse_assignment_after_key<T>(
1275        &mut self,
1276        parse_value: impl FnOnce(&mut Self) -> Result<T, Error>,
1277        assignment_context: &str,
1278        allow_deprecated_without_colon: bool,
1279    ) -> Result<(T, bool), Error> {
1280        if self.at(&TokenKind::Colon)? {
1281            self.next()?;
1282            let value = parse_value(self)?;
1283            return Ok((value, false));
1284        }
1285        if allow_deprecated_without_colon {
1286            let value = parse_value(self)?;
1287            return Ok((value, true));
1288        }
1289        let tok = self.peek()?.clone();
1290        Err(self.error_at_token_with_suggestion(
1291            &tok,
1292            format!("Expected `:` after `{assignment_context}` key."),
1293            format!("Use `-> {assignment_context} <key>: <value>`."),
1294        ))
1295    }
1296
1297    fn parse_unit_as_assignment(&mut self) -> Result<(Vec<CommandArg>, bool), Error> {
1298        if self.at_command_terminator()? {
1299            return Ok((Vec::new(), false));
1300        }
1301
1302        let peek_kind = self.peek()?.kind.clone();
1303        if !can_be_label(&peek_kind) {
1304            return Ok((Vec::new(), false));
1305        }
1306
1307        let unit_name_tok = self.next()?;
1308        let unit_name_arg = CommandArg::Label(unit_name_tok.text.clone());
1309
1310        let (unit_arg, deprecated_without_colon) =
1311            self.parse_assignment_after_key(|parser| parser.parse_unit_payload(), "unit", true)?;
1312
1313        Ok((
1314            vec![unit_name_arg, CommandArg::UnitExpr(unit_arg)],
1315            deprecated_without_colon,
1316        ))
1317    }
1318
1319    /// Value side of `-> unit <name>: <payload>` (factor, compound, or prefix + compound).
1320    fn parse_unit_payload(&mut self) -> Result<UnitArg, Error> {
1321        if self.at_command_terminator()? {
1322            let tok = self.peek()?.clone();
1323            return Err(self.error_at_token_with_suggestion(
1324                &tok,
1325                "Expected a unit conversion factor or compound unit expression after `:`.",
1326                "Use `-> unit <name>: <factor>` or `-> unit <name>: <compound>` (e.g. `unit eur: 1.00`, `unit eur_per_hour: eur/hour`).",
1327            ));
1328        }
1329
1330        let numeric_prefix: Option<Decimal> = if self.at(&TokenKind::NumberLit)? {
1331            let num_tok = self.next()?;
1332            Some(parse_decimal_string(&num_tok.text, &num_tok.span, self)?)
1333        } else {
1334            None
1335        };
1336
1337        let peek_kind_after_prefix = self.peek()?.kind.clone();
1338        let has_compound_expr =
1339            can_be_label(&peek_kind_after_prefix) && !self.at_command_terminator()?;
1340
1341        if has_compound_expr {
1342            let factors = self.parse_unit_factors()?;
1343            let prefix = numeric_prefix.unwrap_or(Decimal::ONE);
1344            Ok(UnitArg::Expr(prefix, factors))
1345        } else if let Some(factor) = numeric_prefix {
1346            Ok(UnitArg::Factor(factor))
1347        } else {
1348            let tok = self.peek()?.clone();
1349            Err(self.error_at_token_with_suggestion(
1350                &tok,
1351                "Expected a unit conversion factor or compound unit expression after `:`.",
1352                "Use `-> unit <name>: <factor>` or `-> unit <name>: <compound>` (e.g. `unit eur: 1.00`, `unit eur_per_hour: eur/hour`).",
1353            ))
1354        }
1355    }
1356
1357    /// Parse arguments for a generic (non-unit) constraint command.
1358    fn parse_generic_command_args(&mut self) -> Result<Vec<CommandArg>, Error> {
1359        let mut args = Vec::new();
1360        loop {
1361            if self.at(&TokenKind::Arrow)?
1362                || self.at(&TokenKind::Eof)?
1363                || is_spec_body_keyword(&self.peek()?.kind)
1364                || self.at(&TokenKind::Spec)?
1365            {
1366                break;
1367            }
1368
1369            let peek_kind = self.peek()?.kind.clone();
1370            match peek_kind {
1371                TokenKind::NumberLit
1372                | TokenKind::Minus
1373                | TokenKind::Plus
1374                | TokenKind::StringLit => {
1375                    let value = self.parse_literal_value()?;
1376                    args.push(CommandArg::Literal(value));
1377                }
1378                ref k if is_boolean_keyword(k) => {
1379                    let value = self.parse_literal_value()?;
1380                    args.push(CommandArg::Literal(value));
1381                }
1382                ref k if can_be_label(k) => {
1383                    let tok = self.next()?;
1384                    args.push(CommandArg::Label(tok.text));
1385                }
1386                _ => break,
1387            }
1388        }
1389        Ok(args)
1390    }
1391
1392    fn parse_scalar_literal_value(&mut self) -> Result<Value, Error> {
1393        self.parse_scalar_literal_value_with_context("value (number, text, boolean, date, etc.)")
1394    }
1395
1396    fn parse_scalar_literal_value_with_context(&mut self, expected: &str) -> Result<Value, Error> {
1397        let peeked = self.peek()?;
1398        match &peeked.kind {
1399            TokenKind::StringLit => {
1400                let tok = self.next()?;
1401                Ok(Value::Text(tok.text))
1402            }
1403            k if is_boolean_keyword(k) => {
1404                let tok = self.next()?;
1405                Ok(Value::Boolean(token_kind_to_boolean_value(&tok.kind)))
1406            }
1407            TokenKind::NumberLit => self.parse_number_literal(),
1408            TokenKind::Minus | TokenKind::Plus => self.parse_signed_number_literal(),
1409            _ => {
1410                let tok = self.next()?;
1411                Err(self
1412                    .error_at_token(&tok, format!("Expected a {expected}, found '{}'", tok.text)))
1413            }
1414        }
1415    }
1416
1417    /// Returns true when the current token ends the current command argument list.
1418    fn at_command_terminator(&mut self) -> Result<bool, Error> {
1419        if self.at(&TokenKind::Arrow)? || self.at(&TokenKind::Eof)? || self.at(&TokenKind::Spec)? {
1420            return Ok(true);
1421        }
1422        Ok(is_spec_body_keyword(&self.peek()?.kind))
1423    }
1424
1425    /// Parse a sequence of `<measure_ref>[^[-]<integer>]` terms joined by `*` or `/`.
1426    ///
1427    /// `*` is explicit multiplication and resets to numerator mode.
1428    /// `/` switches to denominator mode for all subsequent factors until the next `*`.
1429    /// Exponents in denominator mode are negated.
1430    /// An explicit `^<integer>` overrides the default (±1), still relative to the current mode.
1431    ///
1432    /// Space has no meaning in Lemma — `kg * m / s^2` and `kg*m/s^2` are identical.
1433    /// Space-adjacent labels without an intervening `*` or `/` end the expression;
1434    /// the bare label belongs to the next syntactic element.
1435    ///
1436    /// Examples:
1437    /// - `meter/second`            → `[{meter, +1}, {second, -1}]`
1438    /// - `meter/second^2`          → `[{meter, +1}, {second, -2}]`
1439    /// - `kg * meter / second^2`   → `[{kg, +1}, {meter, +1}, {second, -2}]`
1440    /// - `meter / second * kg`     → `[{meter, +1}, {second, -1}, {kg, +1}]`
1441    fn parse_unit_factors(&mut self) -> Result<Vec<UnitFactor>, Error> {
1442        let mut factors: Vec<UnitFactor> = Vec::new();
1443        let mut denominator_mode = false;
1444        // Tracks whether the loop is sitting immediately after an operator (* or /).
1445        // On the first iteration there is an implicit "we just started", so we allow
1446        // the first label without a preceding operator.
1447        let mut operator_just_consumed = true;
1448
1449        loop {
1450            if self.at_command_terminator()? {
1451                if !operator_just_consumed {
1452                    break;
1453                }
1454                // An operator was consumed but no label followed — that is a parse error
1455                // only if we already emitted at least one factor (dangling operator).
1456                // If factors is empty the caller will handle the missing expression.
1457                break;
1458            }
1459
1460            // `*` — explicit multiplication; reset to numerator mode.
1461            if self.at(&TokenKind::Star)? {
1462                if operator_just_consumed && !factors.is_empty() {
1463                    let bad_tok = self.next()?;
1464                    return Err(self.error_at_token(
1465                        &bad_tok,
1466                        "Unexpected '*' in unit expression: two consecutive operators".to_string(),
1467                    ));
1468                }
1469                self.next()?;
1470                denominator_mode = false;
1471                operator_just_consumed = true;
1472                continue;
1473            }
1474
1475            // `/` — switch to denominator mode.
1476            if self.at(&TokenKind::Slash)? {
1477                if operator_just_consumed && !factors.is_empty() {
1478                    let bad_tok = self.next()?;
1479                    return Err(self.error_at_token(
1480                        &bad_tok,
1481                        "Unexpected '/' in unit expression: two consecutive operators".to_string(),
1482                    ));
1483                }
1484                self.next()?;
1485                denominator_mode = true;
1486                operator_just_consumed = true;
1487                continue;
1488            }
1489
1490            // A label is a measure reference.
1491            let peek_kind = self.peek()?.kind.clone();
1492            if !can_be_label(&peek_kind) {
1493                break;
1494            }
1495
1496            // A label without a preceding operator ends the expression.
1497            // The first factor is permitted without an operator (operator_just_consumed starts true).
1498            if !operator_just_consumed {
1499                break;
1500            }
1501            operator_just_consumed = false;
1502
1503            let (measure_ref, _end_span) = self.parse_unit_path()?;
1504
1505            // Optional exponent: `^` followed by an optional `-` and an integer.
1506            let explicit_exp: Option<i32> = if self.at(&TokenKind::Caret)? {
1507                self.next()?; // consume `^`
1508
1509                let negative = if self.at(&TokenKind::Minus)? {
1510                    self.next()?; // consume `-`
1511                    true
1512                } else {
1513                    false
1514                };
1515
1516                if !self.at(&TokenKind::NumberLit)? {
1517                    let bad_tok = self.next()?;
1518                    return Err(self.error_at_token(
1519                        &bad_tok,
1520                        format!(
1521                            "Expected an integer exponent after '^' in unit expression, found {}",
1522                            bad_tok.kind
1523                        ),
1524                    ));
1525                }
1526
1527                let exp_tok = self.next()?;
1528                let raw: i32 = exp_tok.text.parse::<i32>().map_err(|_| {
1529                    self.error_at_token(
1530                        &exp_tok,
1531                        format!(
1532                            "Exponent '{}' is not a valid integer in unit expression",
1533                            exp_tok.text
1534                        ),
1535                    )
1536                })?;
1537
1538                if raw == 0 {
1539                    return Err(self.error_at_token(
1540                        &exp_tok,
1541                        "Exponent cannot be zero in a unit expression".to_string(),
1542                    ));
1543                }
1544
1545                Some(if negative { -raw } else { raw })
1546            } else {
1547                None
1548            };
1549
1550            // Apply denominator mode: negate the exponent (or its default) when in denominator.
1551            let final_exp = match (explicit_exp, denominator_mode) {
1552                (Some(exponent), true) => -exponent,
1553                (Some(exponent), false) => exponent,
1554                (None, true) => -1,
1555                (None, false) => 1,
1556            };
1557
1558            factors.push(UnitFactor {
1559                measure_ref,
1560                exp: final_exp,
1561            });
1562        }
1563
1564        Ok(factors)
1565    }
1566
1567    // ========================================================================
1568    // Meta parsing
1569    // ========================================================================
1570
1571    fn parse_meta(&mut self) -> Result<MetaField, Error> {
1572        let meta_tok = self.expect(&TokenKind::Meta)?;
1573        let start_span = meta_tok.span.clone();
1574
1575        let key_tok = self.next()?;
1576        let key = key_tok.text.clone();
1577
1578        self.expect(&TokenKind::Colon)?;
1579
1580        let value = self.parse_meta_literal_value()?;
1581
1582        let span = self.span_covering(&start_span, &self.last_span);
1583
1584        Ok(MetaField {
1585            key,
1586            value,
1587            source_location: self.make_source(span),
1588        })
1589    }
1590
1591    // ========================================================================
1592    // Literal value parsing
1593    // ========================================================================
1594
1595    fn parse_meta_literal_value(&mut self) -> Result<Value, Error> {
1596        if self.at(&TokenKind::Ellipsis)? {
1597            let tok = self.peek()?.clone();
1598            return Err(self.error_at_token(&tok, "Expected a meta literal, found a range"));
1599        }
1600        self.parse_scalar_literal_value_with_context("meta literal")
1601    }
1602
1603    fn parse_literal_value(&mut self) -> Result<Value, Error> {
1604        let left = self.parse_scalar_literal_value()?;
1605        if self.at(&TokenKind::Ellipsis)? {
1606            self.next()?;
1607            let right = self.parse_scalar_literal_value()?;
1608            Ok(Value::Range(Box::new(left), Box::new(right)))
1609        } else {
1610            Ok(left)
1611        }
1612    }
1613
1614    fn parse_signed_number_literal(&mut self) -> Result<Value, Error> {
1615        let sign_tok = self.next()?;
1616        let sign_span = sign_tok.span.clone();
1617        let is_negative = sign_tok.kind == TokenKind::Minus;
1618
1619        if !self.at(&TokenKind::NumberLit)? {
1620            let tok = self.peek()?.clone();
1621            return Err(self.error_at_token(
1622                &tok,
1623                format!(
1624                    "Expected a number after '{}', found '{}'",
1625                    sign_tok.text, tok.text
1626                ),
1627            ));
1628        }
1629
1630        let value = self.parse_number_literal()?;
1631        if !is_negative {
1632            return Ok(value);
1633        }
1634        match try_negate_numeric_literal(value) {
1635            Ok(negated) => Ok(negated),
1636            Err(other) => Err(Error::parsing(
1637                format!("Cannot negate this value: {}", other),
1638                self.make_source(sign_span),
1639                None::<String>,
1640            )),
1641        }
1642    }
1643
1644    fn parse_number_literal(&mut self) -> Result<Value, Error> {
1645        let num_tok = self.next()?;
1646        let num_text = &num_tok.text;
1647        let num_span = num_tok.span.clone();
1648
1649        // Check if followed by - which could make it a date (YYYY-MM-DD)
1650        if num_text.len() == 4
1651            && num_text.chars().all(|c| c.is_ascii_digit())
1652            && self.at(&TokenKind::Minus)?
1653        {
1654            return self.parse_date_literal(num_text.clone(), num_span);
1655        }
1656
1657        // Check what follows the number
1658        let peeked = self.peek()?;
1659
1660        // Number followed by : could be a time literal (HH:MM:SS)
1661        if num_text.len() == 2
1662            && num_text.chars().all(|c| c.is_ascii_digit())
1663            && peeked.kind == TokenKind::Colon
1664        {
1665            // Only if we're in a data value context... this is ambiguous.
1666            // Time literals look like: 14:30:00 or 14:30
1667            // But we might also have "rule x: expr" where : is assignment.
1668            // The grammar handles this at the grammar level. For us,
1669            // we need to check if the context is right.
1670            // Let's try to parse as time if the following pattern matches.
1671            return self.try_parse_time_literal(num_text.clone(), num_span);
1672        }
1673
1674        // Check for %% (permille) - must be before %
1675        if peeked.kind == TokenKind::PercentPercent {
1676            let pp_tok = self.next()?;
1677            // Check it's not followed by a digit
1678            if let Ok(next_peek) = self.peek() {
1679                if next_peek.kind == TokenKind::NumberLit {
1680                    return Err(self.error_at_token(
1681                        &pp_tok,
1682                        "Permille literal cannot be followed by a digit",
1683                    ));
1684                }
1685            }
1686            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1687            return Ok(Value::NumberWithUnit(decimal, "permille".to_string()));
1688        }
1689
1690        // Check for % (percent)
1691        if peeked.kind == TokenKind::Percent {
1692            let pct_tok = self.next()?;
1693            // Check it's not followed by a digit or another %
1694            if let Ok(next_peek) = self.peek() {
1695                if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
1696                    return Err(self.error_at_token(
1697                        &pct_tok,
1698                        "Percent literal cannot be followed by a digit",
1699                    ));
1700                }
1701            }
1702            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1703            return Ok(Value::NumberWithUnit(decimal, "percent".to_string()));
1704        }
1705
1706        // Check for "permille" keyword
1707        if peeked.kind == TokenKind::Permille {
1708            self.next()?; // consume "permille"
1709            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1710            return Ok(Value::NumberWithUnit(decimal, "permille".to_string()));
1711        }
1712
1713        if can_be_label(&peeked.kind) {
1714            let (unit_path, _end_span) = self.parse_unit_path()?;
1715            let decimal = parse_decimal_string(num_text, &num_span, self)?;
1716            return Ok(Value::NumberWithUnit(decimal, unit_path));
1717        }
1718
1719        // Plain number
1720        let decimal = parse_decimal_string(num_text, &num_span, self)?;
1721        Ok(Value::Number(decimal))
1722    }
1723
1724    /// One or more labels separated by `.` (bare unit or qualified unit path).
1725    fn parse_unit_path(&mut self) -> Result<(String, Span), Error> {
1726        let first = self.next()?;
1727        if !can_be_label(&first.kind) {
1728            return Err(self.error_at_token(
1729                &first,
1730                format!("Expected a unit name, found {}", first.kind),
1731            ));
1732        }
1733        let mut path = first.text.clone();
1734        let mut end_span = first.span.clone();
1735        while self.at(&TokenKind::Dot)? {
1736            self.next()?;
1737            let seg = self.next()?;
1738            if !can_be_label(&seg.kind) {
1739                return Err(self.error_at_token(
1740                    &seg,
1741                    format!("Expected a unit path segment after '.', found {}", seg.kind),
1742                ));
1743            }
1744            path.push('.');
1745            path.push_str(&seg.text);
1746            end_span = seg.span.clone();
1747        }
1748        Ok((path, end_span))
1749    }
1750
1751    fn parse_date_literal(&mut self, year_text: String, start_span: Span) -> Result<Value, Error> {
1752        let mut dt_str = year_text;
1753
1754        // Consume -MM
1755        self.expect(&TokenKind::Minus)?;
1756        dt_str.push('-');
1757        let month_tok = self.expect(&TokenKind::NumberLit)?;
1758        dt_str.push_str(&month_tok.text);
1759
1760        // Consume -DD
1761        self.expect(&TokenKind::Minus)?;
1762        dt_str.push('-');
1763        let day_tok = self.expect(&TokenKind::NumberLit)?;
1764        dt_str.push_str(&day_tok.text);
1765
1766        // Check for T (time component)
1767        if self.at(&TokenKind::Identifier)? {
1768            let peeked = self.peek()?;
1769            if peeked.text.len() >= 2
1770                && (peeked.text.starts_with('T') || peeked.text.starts_with('t'))
1771            {
1772                // The lexer may have tokenized T14 as a single identifier
1773                let t_tok = self.next()?;
1774                dt_str.push_str(&t_tok.text);
1775
1776                // Consume :MM
1777                if self.at(&TokenKind::Colon)? {
1778                    self.next()?;
1779                    dt_str.push(':');
1780                    let min_tok = self.next()?;
1781                    dt_str.push_str(&min_tok.text);
1782
1783                    // Consume :SS and optional fractional second
1784                    if self.at(&TokenKind::Colon)? {
1785                        self.next()?;
1786                        dt_str.push(':');
1787                        let sec_tok = self.next()?;
1788                        dt_str.push_str(&sec_tok.text);
1789
1790                        // Check for fractional second .NNNNNN
1791                        if self.at(&TokenKind::Dot)? {
1792                            self.next()?;
1793                            dt_str.push('.');
1794                            let frac_tok = self.expect(&TokenKind::NumberLit)?;
1795                            dt_str.push_str(&frac_tok.text);
1796                        }
1797                    }
1798                }
1799
1800                // Check for timezone
1801                self.try_consume_timezone(&mut dt_str)?;
1802            }
1803        }
1804
1805        if let Ok(dtv) = dt_str.parse::<crate::literals::DateTimeValue>() {
1806            return Ok(Value::Date(dtv));
1807        }
1808
1809        Err(Error::parsing(
1810            format!("Invalid date/time format: '{}'", dt_str),
1811            self.make_source(start_span),
1812            None::<String>,
1813        ))
1814    }
1815
1816    fn try_consume_timezone(&mut self, dt_str: &mut String) -> Result<(), Error> {
1817        // Z timezone
1818        if self.at(&TokenKind::Identifier)? {
1819            let peeked = self.peek()?;
1820            if (peeked.text == "Z" || peeked.text == "z") && peeked.span.start == self.last_span.end
1821            {
1822                let z_tok = self.next()?;
1823                dt_str.push_str(&z_tok.text);
1824                return Ok(());
1825            }
1826        }
1827
1828        // +HH:MM or -HH:MM, only when attached directly to the preceding token.
1829        if self.at(&TokenKind::Plus)? || self.at(&TokenKind::Minus)? {
1830            let mut lookahead = self.lexer.clone();
1831            let sign_tok = lookahead.next_token()?;
1832            let hour_tok = lookahead.next_token()?;
1833            let colon_tok = lookahead.next_token()?;
1834            let minute_tok = lookahead.next_token()?;
1835
1836            let attached = sign_tok.span.start == self.last_span.end;
1837            let is_timezone_shape = hour_tok.kind == TokenKind::NumberLit
1838                && colon_tok.kind == TokenKind::Colon
1839                && minute_tok.kind == TokenKind::NumberLit;
1840
1841            if attached && is_timezone_shape {
1842                let sign_tok = self.next()?;
1843                dt_str.push_str(&sign_tok.text);
1844                let hour_tok = self.expect(&TokenKind::NumberLit)?;
1845                dt_str.push_str(&hour_tok.text);
1846                self.expect(&TokenKind::Colon)?;
1847                dt_str.push(':');
1848                let min_tok = self.expect(&TokenKind::NumberLit)?;
1849                dt_str.push_str(&min_tok.text);
1850            }
1851        }
1852
1853        Ok(())
1854    }
1855
1856    fn try_parse_time_literal(
1857        &mut self,
1858        hour_text: String,
1859        start_span: Span,
1860    ) -> Result<Value, Error> {
1861        let mut time_str = hour_text;
1862
1863        // Consume :MM
1864        self.expect(&TokenKind::Colon)?;
1865        time_str.push(':');
1866        let min_tok = self.expect(&TokenKind::NumberLit)?;
1867        time_str.push_str(&min_tok.text);
1868
1869        // Optional :SS
1870        if self.at(&TokenKind::Colon)? {
1871            self.next()?;
1872            time_str.push(':');
1873            let sec_tok = self.expect(&TokenKind::NumberLit)?;
1874            time_str.push_str(&sec_tok.text);
1875
1876            // Optional fractional second .NNNNNN
1877            if self.at(&TokenKind::Dot)? {
1878                self.next()?;
1879                time_str.push('.');
1880                let frac_tok = self.expect(&TokenKind::NumberLit)?;
1881                time_str.push_str(&frac_tok.text);
1882            }
1883        }
1884
1885        // Try timezone
1886        self.try_consume_timezone(&mut time_str)?;
1887
1888        if let Ok(t) = time_str.parse::<TimeValue>() {
1889            return Ok(Value::Time(TimeValue {
1890                hour: t.hour,
1891                minute: t.minute,
1892                second: t.second,
1893                microsecond: t.microsecond,
1894                timezone: t.timezone,
1895            }));
1896        }
1897
1898        Err(Error::parsing(
1899            format!("Invalid time format: '{}'", time_str),
1900            self.make_source(start_span),
1901            None::<String>,
1902        ))
1903    }
1904
1905    // ========================================================================
1906    // Expression parsing (Pratt parser / precedence climbing)
1907    // ========================================================================
1908
1909    fn new_expression(
1910        &mut self,
1911        kind: ExpressionKind,
1912        source: Source,
1913    ) -> Result<Expression, Error> {
1914        self.expression_count += 1;
1915        if self.expression_count > self.max_expression_count {
1916            return Err(Error::resource_limit_exceeded(
1917                "max_expression_count",
1918                self.max_expression_count.to_string(),
1919                self.expression_count.to_string(),
1920                "Split logic into multiple rules to reduce expression count",
1921                Some(source),
1922                None,
1923                None,
1924            ));
1925        }
1926        Ok(Expression::new(kind, source))
1927    }
1928
1929    fn check_depth(&mut self) -> Result<(), Error> {
1930        if let Err(actual) = self.depth_tracker.push_depth() {
1931            let span = self.peek()?.span.clone();
1932            self.depth_tracker.pop_depth();
1933            return Err(Error::resource_limit_exceeded(
1934                "max_expression_depth",
1935                self.depth_tracker.max_depth().to_string(),
1936                actual.to_string(),
1937                "Simplify nested expressions or break into separate rules",
1938                Some(self.make_source(span)),
1939                None,
1940                None,
1941            ));
1942        }
1943        Ok(())
1944    }
1945
1946    fn parse_expression(&mut self) -> Result<Expression, Error> {
1947        self.check_depth()?;
1948        let result = self.parse_and_expression();
1949        self.depth_tracker.pop_depth();
1950        result
1951    }
1952
1953    fn parse_and_expression(&mut self) -> Result<Expression, Error> {
1954        let start_span = self.peek()?.span.clone();
1955        let mut left = self.parse_and_operand()?;
1956
1957        while self.at(&TokenKind::And)? {
1958            self.next()?; // consume 'and'
1959            let right = self.parse_and_operand()?;
1960            let span = self.span_covering(
1961                &start_span,
1962                &right
1963                    .source_location
1964                    .as_ref()
1965                    .map(|s| s.span.clone())
1966                    .unwrap_or_else(|| start_span.clone()),
1967            );
1968            left = self.new_expression(
1969                ExpressionKind::LogicalAnd(Arc::new(left), Arc::new(right)),
1970                self.make_source(span),
1971            )?;
1972        }
1973
1974        Ok(left)
1975    }
1976
1977    fn at_bare_veto_token(&mut self) -> Result<bool, Error> {
1978        if !self.at(&TokenKind::Veto)? {
1979            return Ok(false);
1980        }
1981        let checkpoint = self.checkpoint();
1982        self.next()?;
1983        let bare = !self.at(&TokenKind::StringLit)?;
1984        self.restore(checkpoint);
1985        Ok(bare)
1986    }
1987
1988    fn at_bare_veto_followed_by_is(&mut self) -> Result<bool, Error> {
1989        if !self.at_bare_veto_token()? {
1990            return Ok(false);
1991        }
1992        let checkpoint = self.checkpoint();
1993        self.next()?;
1994        let followed = self.at(&TokenKind::Is)?;
1995        self.restore(checkpoint);
1996        Ok(followed)
1997    }
1998
1999    fn at_not_bare_veto_followed_by_is(&mut self) -> Result<bool, Error> {
2000        if !self.at(&TokenKind::Not)? {
2001            return Ok(false);
2002        }
2003        let checkpoint = self.checkpoint();
2004        self.next()?;
2005        if !self.at(&TokenKind::Veto)? {
2006            self.restore(checkpoint);
2007            return Ok(false);
2008        }
2009        self.next()?;
2010        if self.at(&TokenKind::StringLit)? {
2011            self.restore(checkpoint);
2012            return Ok(false);
2013        }
2014        let followed = self.at(&TokenKind::Is)?;
2015        self.restore(checkpoint);
2016        Ok(followed)
2017    }
2018
2019    fn wrap_result_is_veto_expression(
2020        &mut self,
2021        operand: Expression,
2022        operator_is_not: bool,
2023        keyword_was_negated: bool,
2024        start_span: Span,
2025    ) -> Result<Expression, Error> {
2026        let negate = operator_is_not ^ keyword_was_negated;
2027        let end_span = operand
2028            .source_location
2029            .as_ref()
2030            .map(|source| source.span.clone())
2031            .unwrap_or_else(|| start_span.clone());
2032        let span = self.span_covering(&start_span, &end_span);
2033        let core = self.new_expression(
2034            ExpressionKind::ResultIsVeto(Arc::new(operand)),
2035            self.make_source(span.clone()),
2036        )?;
2037        if negate {
2038            self.new_expression(
2039                ExpressionKind::LogicalNegation(Arc::new(core), NegationType::Not),
2040                self.make_source(span),
2041            )
2042        } else {
2043            Ok(core)
2044        }
2045    }
2046
2047    fn parse_veto_status_lhs_is_comparison(&mut self) -> Result<Expression, Error> {
2048        let start_span = self.peek()?.span.clone();
2049        let keyword_was_negated = if self.at(&TokenKind::Not)? {
2050            self.next()?;
2051            true
2052        } else {
2053            false
2054        };
2055        self.expect(&TokenKind::Veto)?;
2056        if self.at(&TokenKind::StringLit)? {
2057            let tok = self.peek()?.clone();
2058            return Err(self.error_at_token(
2059                &tok,
2060                "veto with a message is only valid as a rule or unless result, not in `is veto` comparisons",
2061            ));
2062        }
2063        let operator = self.parse_comparison_operator()?;
2064        let operator_is_not = matches!(operator, ComparisonComputation::IsNot);
2065        if !matches!(
2066            operator,
2067            ComparisonComputation::Is | ComparisonComputation::IsNot
2068        ) {
2069            let tok = self.peek()?.clone();
2070            return Err(self.error_at_token(
2071                &tok,
2072                "Expected `is` or `is not` after `veto` in a veto-status comparison",
2073            ));
2074        }
2075        let operand = self.parse_range_expression()?;
2076        self.wrap_result_is_veto_expression(
2077            operand,
2078            operator_is_not,
2079            keyword_was_negated,
2080            start_span,
2081        )
2082    }
2083
2084    fn parse_and_operand(&mut self) -> Result<Expression, Error> {
2085        if self.at_not_bare_veto_followed_by_is()? || self.at_bare_veto_followed_by_is()? {
2086            return self.parse_veto_status_lhs_is_comparison();
2087        }
2088
2089        // not expression
2090        if self.at(&TokenKind::Not)? {
2091            return self.parse_not_expression();
2092        }
2093
2094        // repository_with_suffix: repository_expression followed by optional suffix
2095        self.parse_repository_with_suffix()
2096    }
2097
2098    fn parse_not_expression(&mut self) -> Result<Expression, Error> {
2099        let not_tok = self.expect(&TokenKind::Not)?;
2100        let start_span = not_tok.span.clone();
2101
2102        self.check_depth()?;
2103        let operand = self.parse_and_operand()?;
2104        self.depth_tracker.pop_depth();
2105
2106        let end_span = operand
2107            .source_location
2108            .as_ref()
2109            .map(|s| s.span.clone())
2110            .unwrap_or_else(|| start_span.clone());
2111        let span = self.span_covering(&start_span, &end_span);
2112
2113        self.new_expression(
2114            ExpressionKind::LogicalNegation(Arc::new(operand), NegationType::Not),
2115            self.make_source(span),
2116        )
2117    }
2118
2119    fn parse_repository_with_suffix(&mut self) -> Result<Expression, Error> {
2120        let start_span = self.peek()?.span.clone();
2121        let repository = self.parse_range_expression()?;
2122        self.continue_repository_operand(repository, start_span)
2123    }
2124
2125    /// Postfix suffixes on a completed repository/range expression (`in`, calendar, comparison, `as`).
2126    fn continue_repository_operand(
2127        &mut self,
2128        mut expr: Expression,
2129        start_span: Span,
2130    ) -> Result<Expression, Error> {
2131        loop {
2132            let peeked = self.peek()?;
2133
2134            if is_comparison_operator(&peeked.kind) {
2135                return self.parse_comparison_suffix(expr, start_span);
2136            }
2137
2138            if peeked.kind == TokenKind::Not {
2139                expr = self.parse_not_in_calendar_suffix(expr, start_span.clone())?;
2140                continue;
2141            }
2142
2143            if peeked.kind == TokenKind::In {
2144                expr = self.parse_in_suffix(expr, start_span.clone())?;
2145                continue;
2146            }
2147
2148            if peeked.kind == TokenKind::As {
2149                expr = self.parse_as_chain(expr, start_span.clone())?;
2150                continue;
2151            }
2152
2153            break;
2154        }
2155
2156        if self.at_expression_suffix_end()? {
2157            return Ok(expr);
2158        }
2159
2160        let tok = self.peek()?.clone();
2161        Err(self.error_at_token(
2162            &tok,
2163            format!("Unexpected token '{}' after expression", tok.text),
2164        ))
2165    }
2166
2167    fn parse_comparison_suffix(
2168        &mut self,
2169        left: Expression,
2170        start_span: Span,
2171    ) -> Result<Expression, Error> {
2172        let operator = self.parse_comparison_operator()?;
2173        let operator_is_not = matches!(operator, ComparisonComputation::IsNot);
2174
2175        if matches!(
2176            operator,
2177            ComparisonComputation::Is | ComparisonComputation::IsNot
2178        ) && self.at_bare_veto_token()?
2179        {
2180            self.expect(&TokenKind::Veto)?;
2181            if self.at(&TokenKind::StringLit)? {
2182                let tok = self.peek()?.clone();
2183                return Err(self.error_at_token(
2184                    &tok,
2185                    "veto with a message is only valid as a rule or unless result, not in `is veto` comparisons",
2186                ));
2187            }
2188            return self.wrap_result_is_veto_expression(left, operator_is_not, false, start_span);
2189        }
2190
2191        // Right side can be: not_expr | range/repository expression (term-level `as` included)
2192        let right = if self.at(&TokenKind::Not)? {
2193            self.parse_not_expression()?
2194        } else {
2195            self.parse_range_expression()?
2196        };
2197
2198        let end_span = right
2199            .source_location
2200            .as_ref()
2201            .map(|s| s.span.clone())
2202            .unwrap_or_else(|| start_span.clone());
2203        let span = self.span_covering(&start_span, &end_span);
2204
2205        self.new_expression(
2206            ExpressionKind::Comparison(Arc::new(left), operator, Arc::new(right)),
2207            self.make_source(span),
2208        )
2209    }
2210
2211    fn parse_comparison_operator(&mut self) -> Result<ComparisonComputation, Error> {
2212        let tok = self.next()?;
2213        match tok.kind {
2214            TokenKind::Gt => Ok(ComparisonComputation::GreaterThan),
2215            TokenKind::Lt => Ok(ComparisonComputation::LessThan),
2216            TokenKind::Gte => Ok(ComparisonComputation::GreaterThanOrEqual),
2217            TokenKind::Lte => Ok(ComparisonComputation::LessThanOrEqual),
2218            TokenKind::Is => {
2219                // Check for "is not"
2220                if self.at(&TokenKind::Not)? {
2221                    self.next()?; // consume 'not'
2222                    Ok(ComparisonComputation::IsNot)
2223                } else {
2224                    Ok(ComparisonComputation::Is)
2225                }
2226            }
2227            _ => Err(self.error_at_token(
2228                &tok,
2229                format!("Expected a comparison operator, found {}", tok.kind),
2230            )),
2231        }
2232    }
2233
2234    fn parse_not_in_calendar_suffix(
2235        &mut self,
2236        repository: Expression,
2237        start_span: Span,
2238    ) -> Result<Expression, Error> {
2239        self.expect(&TokenKind::Not)?;
2240        self.expect(&TokenKind::In)?;
2241        self.expect_calendar_period_marker()?;
2242        let unit = self.parse_calendar_unit()?;
2243        let end = self.peek()?.span.clone();
2244        let span = self.span_covering(&start_span, &end);
2245        self.new_expression(
2246            ExpressionKind::DateCalendar(DateCalendarKind::NotIn, unit, Arc::new(repository)),
2247            self.make_source(span),
2248        )
2249    }
2250
2251    fn parse_in_suffix(
2252        &mut self,
2253        repository: Expression,
2254        start_span: Span,
2255    ) -> Result<Expression, Error> {
2256        self.expect(&TokenKind::In)?;
2257
2258        let peeked = self.peek()?;
2259
2260        // "in past calendar <unit>" or "in future calendar <unit>"
2261        if peeked.kind == TokenKind::Past || peeked.kind == TokenKind::Future {
2262            let direction = self.next()?;
2263            let rel_kind = if direction.kind == TokenKind::Past {
2264                DateRelativeKind::InPast
2265            } else {
2266                DateRelativeKind::InFuture
2267            };
2268
2269            // Check for "calendar" keyword
2270            if self.at_calendar_period_marker()? {
2271                self.next_calendar_period_marker()?;
2272                let cal_kind = if direction.kind == TokenKind::Past {
2273                    DateCalendarKind::Past
2274                } else {
2275                    DateCalendarKind::Future
2276                };
2277                let unit = self.parse_calendar_unit()?;
2278                let end = self.peek()?.span.clone();
2279                let span = self.span_covering(&start_span, &end);
2280                return self.new_expression(
2281                    ExpressionKind::DateCalendar(cal_kind, unit, Arc::new(repository)),
2282                    self.make_source(span),
2283                );
2284            }
2285
2286            if self.at(&TokenKind::And)?
2287                || self.at(&TokenKind::Unless)?
2288                || self.at(&TokenKind::Then)?
2289                || self.at(&TokenKind::RParen)?
2290                || self.at(&TokenKind::Eof)?
2291                || is_comparison_operator(&self.peek()?.kind)
2292            {
2293                let end = self.peek()?.span.clone();
2294                let span = self.span_covering(&start_span, &end);
2295                return self.new_expression(
2296                    ExpressionKind::DateRelative(rel_kind, Arc::new(repository)),
2297                    self.make_source(span),
2298                );
2299            }
2300
2301            let offset = self.parse_repository_expression()?;
2302            let offset_end_span = offset
2303                .source_location
2304                .as_ref()
2305                .map(|s| s.span.clone())
2306                .unwrap_or_else(|| start_span.clone());
2307            let range = self.new_expression(
2308                ExpressionKind::PastFutureRange(rel_kind, Arc::new(offset)),
2309                self.make_source(self.span_covering(&direction.span, &offset_end_span)),
2310            )?;
2311            let span = self.span_covering(&start_span, &offset_end_span);
2312            return self.new_expression(
2313                ExpressionKind::RangeContainment(Arc::new(repository), Arc::new(range)),
2314                self.make_source(span),
2315            );
2316        }
2317
2318        // "in calendar <unit>"
2319        if token_is_calendar_period_marker(peeked) {
2320            self.next_calendar_period_marker()?;
2321            let unit = self.parse_calendar_unit()?;
2322            let end = self.peek()?.span.clone();
2323            let span = self.span_covering(&start_span, &end);
2324            return self.new_expression(
2325                ExpressionKind::DateCalendar(DateCalendarKind::Current, unit, Arc::new(repository)),
2326                self.make_source(span),
2327            );
2328        }
2329
2330        let range = self.parse_range_expression()?;
2331        let end_span = range
2332            .source_location
2333            .as_ref()
2334            .map(|s| s.span.clone())
2335            .unwrap_or_else(|| start_span.clone());
2336        let span = self.span_covering(&start_span, &end_span);
2337        self.new_expression(
2338            ExpressionKind::RangeContainment(Arc::new(repository), Arc::new(range)),
2339            self.make_source(span),
2340        )
2341    }
2342
2343    fn parse_as_chain(
2344        &mut self,
2345        mut expr: Expression,
2346        start_span: Span,
2347    ) -> Result<Expression, Error> {
2348        while self.at(&TokenKind::As)? {
2349            self.expect(&TokenKind::As)?;
2350            let target_tok = self.next()?;
2351            let target = if matches!(target_tok.kind, TokenKind::Permille) {
2352                ConversionTarget::Unit {
2353                    unit_name: "permille".to_string(),
2354                }
2355            } else if let Some(primitive) = token_kind_to_primitive(&target_tok.kind) {
2356                ConversionTarget::Type(primitive)
2357            } else if can_be_label(&target_tok.kind) {
2358                let mut unit_path = target_tok.text.clone();
2359                let mut end_span = target_tok.span.clone();
2360                while self.at(&TokenKind::Dot)? {
2361                    self.next()?;
2362                    let seg = self.next()?;
2363                    if !can_be_label(&seg.kind) {
2364                        return Err(self.error_at_token(
2365                            &seg,
2366                            format!("Expected a unit path segment after '.', found {}", seg.kind),
2367                        ));
2368                    }
2369                    unit_path.push('.');
2370                    unit_path.push_str(&seg.text);
2371                    end_span = seg.span.clone();
2372                }
2373                let target = ConversionTarget::Unit {
2374                    unit_name: unit_path,
2375                };
2376                expr = self.new_expression(
2377                    ExpressionKind::UnitConversion(Arc::new(expr), target),
2378                    self.make_source(self.span_covering(&start_span, &end_span)),
2379                )?;
2380                continue;
2381            } else {
2382                return Err(self.error_at_token(
2383                    &target_tok,
2384                    format!(
2385                        "Expected a type keyword or unit name after 'as', found {}",
2386                        target_tok.kind
2387                    ),
2388                ));
2389            };
2390            expr = self.new_expression(
2391                ExpressionKind::UnitConversion(Arc::new(expr), target),
2392                self.make_source(self.span_covering(&start_span, &target_tok.span)),
2393            )?;
2394        }
2395        Ok(expr)
2396    }
2397
2398    fn is_plain_number_literal(expr: &Expression) -> bool {
2399        matches!(expr.kind, ExpressionKind::Literal(Value::Number(_)))
2400    }
2401
2402    fn is_unit_conversion(expr: &Expression) -> bool {
2403        matches!(expr.kind, ExpressionKind::UnitConversion(..))
2404    }
2405
2406    /// True when the next token can follow a completed suffix expression (no further operands).
2407    ///
2408    /// Must include every token that can start the next spec-body item, a new `spec`/`repo`,
2409    /// or end the file. See `parse_unit_conversion_before_expression_boundaries` in `parsing/mod.rs`.
2410    fn at_expression_suffix_end(&mut self) -> Result<bool, Error> {
2411        Ok(self.at(&TokenKind::And)?
2412            || self.at(&TokenKind::Unless)?
2413            || self.at(&TokenKind::Then)?
2414            || self.at(&TokenKind::RParen)?
2415            || self.at(&TokenKind::Eof)?
2416            || self.at(&TokenKind::Spec)?
2417            || self.at(&TokenKind::Repo)?
2418            || self.at(&TokenKind::Uses)?
2419            || is_spec_body_keyword(&self.peek()?.kind))
2420    }
2421
2422    fn parse_calendar_unit(&mut self) -> Result<CalendarPeriodUnit, Error> {
2423        let tok = self.next()?;
2424        if let Some(unit) = CalendarPeriodUnit::from_keyword(&tok.text) {
2425            return Ok(unit);
2426        }
2427        Err(self.error_at_token(
2428            &tok,
2429            format!("Expected 'year', 'month', or 'week', found '{}'", tok.text),
2430        ))
2431    }
2432
2433    // ========================================================================
2434    // Arithmetic expressions (precedence climbing)
2435    // ========================================================================
2436
2437    fn parse_range_expression(&mut self) -> Result<Expression, Error> {
2438        self.parse_repository_expression()
2439    }
2440
2441    /// Atom or range-typed value: `...` binds before `^`, `*`, `/`, `%` on the same operand.
2442    /// Both endpoints use [`Self::parse_range_ellipsis_bound`] (`+`/`-` only) so
2443    /// `now - 7 day...now` and `start...start + length` are valid, and
2444    /// `rate * period_start...period_end` keeps `*` outside the range.
2445    fn parse_range_operand(&mut self) -> Result<Expression, Error> {
2446        let start_span = self.peek()?.span.clone();
2447        let checkpoint = self.checkpoint();
2448        let left = self.parse_range_ellipsis_bound()?;
2449        if !self.at(&TokenKind::Ellipsis)? {
2450            self.restore(checkpoint);
2451            return self.parse_factor();
2452        }
2453
2454        self.next()?;
2455        let right = self.parse_range_ellipsis_bound()?;
2456        let end_span = right
2457            .source_location
2458            .as_ref()
2459            .map(|s| s.span.clone())
2460            .unwrap_or_else(|| start_span.clone());
2461        let span = self.span_covering(&start_span, &end_span);
2462        self.new_expression(
2463            ExpressionKind::RangeLiteral(Arc::new(left), Arc::new(right)),
2464            self.make_source(span),
2465        )
2466    }
2467
2468    /// One side of `...`: `+`/`-` between powers only (no `*`/`/`/`%` — those bind outside the range).
2469    fn parse_range_ellipsis_bound(&mut self) -> Result<Expression, Error> {
2470        let start_span = self.peek()?.span.clone();
2471        let mut left = self.parse_power_for_range_bound()?;
2472
2473        while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
2474            let op_tok = self.next()?;
2475            let operation = match op_tok.kind {
2476                TokenKind::Plus => ArithmeticComputation::Add,
2477                TokenKind::Minus => ArithmeticComputation::Subtract,
2478                _ => unreachable!("BUG: only + and - should reach here"),
2479            };
2480
2481            let right = self.parse_power_for_range_bound()?;
2482            let end_span = right
2483                .source_location
2484                .as_ref()
2485                .map(|s| s.span.clone())
2486                .unwrap_or_else(|| start_span.clone());
2487            let span = self.span_covering(&start_span, &end_span);
2488
2489            left = self.new_expression(
2490                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2491                self.make_source(span),
2492            )?;
2493        }
2494
2495        Ok(left)
2496    }
2497
2498    fn parse_power_for_range_bound(&mut self) -> Result<Expression, Error> {
2499        let start_span = self.peek()?.span.clone();
2500        let left = self.parse_factor()?;
2501
2502        if self.at(&TokenKind::Caret)? {
2503            self.next()?;
2504            self.check_depth()?;
2505            let right = self.parse_power_for_range_bound()?;
2506            self.depth_tracker.pop_depth();
2507            let end_span = right
2508                .source_location
2509                .as_ref()
2510                .map(|s| s.span.clone())
2511                .unwrap_or_else(|| start_span.clone());
2512            let span = self.span_covering(&start_span, &end_span);
2513
2514            return self.new_expression(
2515                ExpressionKind::Arithmetic(
2516                    Arc::new(left),
2517                    ArithmeticComputation::Power,
2518                    Arc::new(right),
2519                ),
2520                self.make_source(span),
2521            );
2522        }
2523
2524        Ok(left)
2525    }
2526
2527    fn parse_repository_expression(&mut self) -> Result<Expression, Error> {
2528        let start_span = self.peek()?.span.clone();
2529        let mut left = self.parse_term()?;
2530
2531        while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
2532            // Check if this minus is really a binary operator or could be part of something else
2533            // In "X not in calendar year", we don't want to consume "not" as an operator
2534            let op_tok = self.next()?;
2535            let operation = match op_tok.kind {
2536                TokenKind::Plus => ArithmeticComputation::Add,
2537                TokenKind::Minus => ArithmeticComputation::Subtract,
2538                _ => unreachable!("BUG: only + and - should reach here"),
2539            };
2540
2541            let right = self.parse_term()?;
2542            if Self::is_plain_number_literal(&left) && Self::is_unit_conversion(&right) {
2543                let source = right
2544                    .source_location
2545                    .clone()
2546                    .unwrap_or_else(|| self.make_source(start_span.clone()));
2547                return Err(Error::parsing(
2548                    "Cannot add a plain number to a converted value; convert each operand before \
2549                     '+' (e.g. '5 as usd + c as usd')",
2550                    source,
2551                    None::<String>,
2552                ));
2553            }
2554
2555            let end_span = right
2556                .source_location
2557                .as_ref()
2558                .map(|s| s.span.clone())
2559                .unwrap_or_else(|| start_span.clone());
2560            let span = self.span_covering(&start_span, &end_span);
2561
2562            left = self.new_expression(
2563                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2564                self.make_source(span),
2565            )?;
2566        }
2567
2568        Ok(left)
2569    }
2570
2571    fn parse_term(&mut self) -> Result<Expression, Error> {
2572        self.parse_term_with_as(true)
2573    }
2574
2575    fn parse_term_with_as(&mut self, allow_as: bool) -> Result<Expression, Error> {
2576        let start_span = self.peek()?.span.clone();
2577        let mut left = self.parse_power()?;
2578        if allow_as {
2579            left = self.parse_as_chain(left, start_span.clone())?;
2580        }
2581
2582        while self.at_any(&[TokenKind::Star, TokenKind::Slash, TokenKind::Percent])? {
2583            // Be careful: % could be a percent literal suffix (e.g. 50%)
2584            // But here in term context, it's modulo since we already parsed the number
2585            let op_tok = self.next()?;
2586            let operation = match op_tok.kind {
2587                TokenKind::Star => ArithmeticComputation::Multiply,
2588                TokenKind::Slash => ArithmeticComputation::Divide,
2589                TokenKind::Percent => ArithmeticComputation::Modulo,
2590                _ => unreachable!("BUG: only *, /, % should reach here"),
2591            };
2592
2593            let right_start_span = self.peek()?.span.clone();
2594            let mut right = self.parse_power()?;
2595            if allow_as {
2596                right = self.parse_as_chain(right, right_start_span)?;
2597            }
2598            let end_span = right
2599                .source_location
2600                .as_ref()
2601                .map(|s| s.span.clone())
2602                .unwrap_or_else(|| start_span.clone());
2603            let span = self.span_covering(&start_span, &end_span);
2604
2605            left = self.new_expression(
2606                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2607                self.make_source(span),
2608            )?;
2609        }
2610
2611        Ok(left)
2612    }
2613
2614    fn parse_power(&mut self) -> Result<Expression, Error> {
2615        let start_span = self.peek()?.span.clone();
2616        let left = self.parse_range_operand()?;
2617
2618        if self.at(&TokenKind::Caret)? {
2619            self.next()?;
2620            self.check_depth()?;
2621            let right = self.parse_power()?;
2622            self.depth_tracker.pop_depth();
2623            let end_span = right
2624                .source_location
2625                .as_ref()
2626                .map(|s| s.span.clone())
2627                .unwrap_or_else(|| start_span.clone());
2628            let span = self.span_covering(&start_span, &end_span);
2629
2630            return self.new_expression(
2631                ExpressionKind::Arithmetic(
2632                    Arc::new(left),
2633                    ArithmeticComputation::Power,
2634                    Arc::new(right),
2635                ),
2636                self.make_source(span),
2637            );
2638        }
2639
2640        Ok(left)
2641    }
2642
2643    fn parse_factor(&mut self) -> Result<Expression, Error> {
2644        let peeked = self.peek()?;
2645        let start_span = peeked.span.clone();
2646
2647        if peeked.kind == TokenKind::Minus {
2648            self.next()?;
2649            let operand = self.parse_primary_or_math()?;
2650            let end_span = operand
2651                .source_location
2652                .as_ref()
2653                .map(|s| s.span.clone())
2654                .unwrap_or_else(|| start_span.clone());
2655            let span = self.span_covering(&start_span, &end_span);
2656
2657            if let ExpressionKind::Literal(value) = &operand.kind {
2658                if let Ok(negated) = try_negate_numeric_literal(value.clone()) {
2659                    return self
2660                        .new_expression(ExpressionKind::Literal(negated), self.make_source(span));
2661                }
2662            }
2663
2664            let zero = self.new_expression(
2665                ExpressionKind::Literal(Value::Number(Decimal::ZERO)),
2666                self.make_source(start_span),
2667            )?;
2668            return self.new_expression(
2669                ExpressionKind::Arithmetic(
2670                    Arc::new(zero),
2671                    ArithmeticComputation::Subtract,
2672                    Arc::new(operand),
2673                ),
2674                self.make_source(span),
2675            );
2676        }
2677
2678        if peeked.kind == TokenKind::Plus {
2679            self.next()?;
2680            return self.parse_primary_or_math();
2681        }
2682
2683        self.parse_primary_or_math()
2684    }
2685
2686    fn parse_primary_or_math(&mut self) -> Result<Expression, Error> {
2687        let peeked = self.peek()?;
2688
2689        // Math functions
2690        if is_math_function(&peeked.kind) {
2691            return self.parse_math_function();
2692        }
2693
2694        self.parse_primary()
2695    }
2696
2697    fn parse_math_function(&mut self) -> Result<Expression, Error> {
2698        let func_tok = self.next()?;
2699        let start_span = func_tok.span.clone();
2700
2701        let operator = match func_tok.kind {
2702            TokenKind::Sqrt => MathematicalComputation::Sqrt,
2703            TokenKind::Sin => MathematicalComputation::Sin,
2704            TokenKind::Cos => MathematicalComputation::Cos,
2705            TokenKind::Tan => MathematicalComputation::Tan,
2706            TokenKind::Asin => MathematicalComputation::Asin,
2707            TokenKind::Acos => MathematicalComputation::Acos,
2708            TokenKind::Atan => MathematicalComputation::Atan,
2709            TokenKind::Log => MathematicalComputation::Log,
2710            TokenKind::Exp => MathematicalComputation::Exp,
2711            TokenKind::Abs => MathematicalComputation::Abs,
2712            TokenKind::Floor => MathematicalComputation::Floor,
2713            TokenKind::Ceil => MathematicalComputation::Ceil,
2714            TokenKind::Round => MathematicalComputation::Round,
2715            _ => unreachable!("BUG: only math functions should reach here"),
2716        };
2717
2718        self.check_depth()?;
2719        let operand = self.parse_repository_expression()?;
2720        self.depth_tracker.pop_depth();
2721
2722        let end_span = operand
2723            .source_location
2724            .as_ref()
2725            .map(|s| s.span.clone())
2726            .unwrap_or_else(|| start_span.clone());
2727        let span = self.span_covering(&start_span, &end_span);
2728
2729        self.new_expression(
2730            ExpressionKind::MathematicalComputation(operator, Arc::new(operand)),
2731            self.make_source(span),
2732        )
2733    }
2734
2735    fn parse_primary(&mut self) -> Result<Expression, Error> {
2736        let peeked = self.peek()?;
2737        let start_span = peeked.span.clone();
2738
2739        match &peeked.kind {
2740            // Parenthesized expression
2741            TokenKind::LParen => {
2742                self.next()?; // consume (
2743                let inner = self.parse_expression()?;
2744                self.expect(&TokenKind::RParen)?;
2745                Ok(inner)
2746            }
2747
2748            // Now keyword
2749            TokenKind::Now => {
2750                let tok = self.next()?;
2751                self.new_expression(ExpressionKind::Now, self.make_source(tok.span))
2752            }
2753
2754            TokenKind::Past | TokenKind::Future => {
2755                let tok = self.next()?;
2756                let kind = if tok.kind == TokenKind::Past {
2757                    DateRelativeKind::InPast
2758                } else {
2759                    DateRelativeKind::InFuture
2760                };
2761                let offset = self.parse_repository_expression()?;
2762                let span = self.span_covering(
2763                    &start_span,
2764                    &offset
2765                        .source_location
2766                        .as_ref()
2767                        .map(|s| s.span.clone())
2768                        .unwrap_or(start_span.clone()),
2769                );
2770                self.new_expression(
2771                    ExpressionKind::PastFutureRange(kind, Arc::new(offset)),
2772                    self.make_source(span),
2773                )
2774            }
2775
2776            // String literal
2777            TokenKind::StringLit => {
2778                let tok = self.next()?;
2779                self.new_expression(
2780                    ExpressionKind::Literal(Value::Text(tok.text)),
2781                    self.make_source(tok.span),
2782                )
2783            }
2784
2785            // Boolean literals
2786            k if is_boolean_keyword(k) => {
2787                let tok = self.next()?;
2788                self.new_expression(
2789                    ExpressionKind::Literal(Value::Boolean(token_kind_to_boolean_value(&tok.kind))),
2790                    self.make_source(tok.span),
2791                )
2792            }
2793
2794            // Number literal (could be: plain number, date, time, duration, percent, unit)
2795            TokenKind::NumberLit => self.parse_number_expression(),
2796
2797            // Reference (identifier, type keyword)
2798            k if can_be_label(k) => {
2799                let reference = self.parse_expression_reference()?;
2800                let span = self.span_covering(&start_span, &self.last_span);
2801                self.new_expression(ExpressionKind::Reference(reference), self.make_source(span))
2802            }
2803
2804            _ => {
2805                let tok = self.next()?;
2806                Err(self.error_at_token(
2807                    &tok,
2808                    format!("Expected an expression, found '{}'", tok.text),
2809                ))
2810            }
2811        }
2812    }
2813
2814    fn parse_number_expression(&mut self) -> Result<Expression, Error> {
2815        let num_tok = self.next()?;
2816        let num_text = num_tok.text.clone();
2817        let start_span = num_tok.span.clone();
2818
2819        // Check if this is a date literal (YYYY-MM-DD)
2820        if num_text.len() == 4
2821            && num_text.chars().all(|c| c.is_ascii_digit())
2822            && self.at(&TokenKind::Minus)?
2823        {
2824            // Peek further: if next-next is a number, this is likely a date
2825            // We need to be careful: "2024 - 5" is arithmetic, "2024-01-15" is a date
2826            // Date format requires: YYYY-MM-DD where MM and DD are 2 digits
2827            // This is ambiguous at the token level. Let's check if the pattern matches.
2828            // Since dates use -NN- pattern and arithmetic uses - N pattern (with spaces),
2829            // we can use the span positions to disambiguate.
2830            let minus_span = self.peek()?.span.clone();
2831            // If minus is immediately adjacent to the number (no space), it's a date
2832            if minus_span.start == start_span.end {
2833                let value = self.parse_date_literal(num_text, start_span.clone())?;
2834                return self
2835                    .new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
2836            }
2837        }
2838
2839        // Check for time literal (HH:MM:SS)
2840        if num_text.len() == 2
2841            && num_text.chars().all(|c| c.is_ascii_digit())
2842            && self.at(&TokenKind::Colon)?
2843        {
2844            let colon_span = self.peek()?.span.clone();
2845            if colon_span.start == start_span.end {
2846                let value = self.try_parse_time_literal(num_text, start_span.clone())?;
2847                return self
2848                    .new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
2849            }
2850        }
2851
2852        // Check for %% (permille)
2853        if self.at(&TokenKind::PercentPercent)? {
2854            let pp_tok = self.next()?;
2855            if let Ok(next_peek) = self.peek() {
2856                if next_peek.kind == TokenKind::NumberLit {
2857                    return Err(self.error_at_token(
2858                        &pp_tok,
2859                        "Permille literal cannot be followed by a digit",
2860                    ));
2861                }
2862            }
2863            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2864            return self.new_expression(
2865                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
2866                self.make_source(start_span),
2867            );
2868        }
2869
2870        // Check for % (percent)
2871        if self.at(&TokenKind::Percent)? {
2872            let pct_span = self.peek()?.span.clone();
2873            // Only consume % if it's directly adjacent (no space) for the shorthand syntax
2874            // Or if it's "50 %" (space separated is also valid per the grammar)
2875            let pct_tok = self.next()?;
2876            if let Ok(next_peek) = self.peek() {
2877                if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
2878                    return Err(self.error_at_token(
2879                        &pct_tok,
2880                        "Percent literal cannot be followed by a digit",
2881                    ));
2882                }
2883            }
2884            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2885            return self.new_expression(
2886                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "percent".to_string())),
2887                self.make_source(self.span_covering(&start_span, &pct_span)),
2888            );
2889        }
2890
2891        // Check for "permille" keyword
2892        if self.at(&TokenKind::Permille)? {
2893            self.next()?;
2894            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2895            return self.new_expression(
2896                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
2897                self.make_source(start_span),
2898            );
2899        }
2900
2901        if can_be_label(&self.peek()?.kind) {
2902            let (unit_path, end_span) = self.parse_unit_path()?;
2903            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2904            return self.new_expression(
2905                ExpressionKind::Literal(Value::NumberWithUnit(decimal, unit_path)),
2906                self.make_source(self.span_covering(&start_span, &end_span)),
2907            );
2908        }
2909
2910        // Plain number
2911        let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2912        self.new_expression(
2913            ExpressionKind::Literal(Value::Number(decimal)),
2914            self.make_source(start_span),
2915        )
2916    }
2917
2918    fn parse_expression_reference(&mut self) -> Result<Reference, Error> {
2919        let mut segments = Vec::new();
2920
2921        let first = self.next()?;
2922        segments.push(first.text.clone());
2923
2924        while self.at(&TokenKind::Dot)? {
2925            self.next()?; // consume .
2926            let seg = self.next()?;
2927            if !can_be_label(&seg.kind) {
2928                return Err(self.error_at_token(
2929                    &seg,
2930                    format!("Expected an identifier after '.', found {}", seg.kind),
2931                ));
2932            }
2933            segments.push(seg.text.clone());
2934        }
2935
2936        Ok(Reference::from_path(segments))
2937    }
2938}
2939
2940// ============================================================================
2941// Helper functions
2942// ============================================================================
2943
2944fn parse_decimal_string(text: &str, span: &Span, parser: &Parser) -> Result<Decimal, Error> {
2945    text.parse::<crate::literals::NumberLiteral>()
2946        .map(|parsed| parsed.0)
2947        .map_err(|message| {
2948            Error::parsing(message, parser.make_source(span.clone()), None::<String>)
2949        })
2950}
2951
2952/// Negate a numeric literal value. Returns `Err(value)` when the value is not a number.
2953fn try_negate_numeric_literal(value: Value) -> Result<Value, Value> {
2954    match value {
2955        Value::Number(d) => Ok(Value::Number(-d)),
2956        Value::NumberWithUnit(d, unit) => Ok(Value::NumberWithUnit(-d, unit)),
2957        other => Err(other),
2958    }
2959}
2960
2961fn is_comparison_operator(kind: &TokenKind) -> bool {
2962    matches!(
2963        kind,
2964        TokenKind::Gt | TokenKind::Lt | TokenKind::Gte | TokenKind::Lte | TokenKind::Is
2965    )
2966}