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