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