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