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    /// Endpoints use [`Self::parse_range_ellipsis_bound`] (`+`/`-` only) so `now - 7 days...now` is valid
2310    /// and `rate * period_start...period_end` keeps `*` outside the range.
2311    fn parse_range_operand(&mut self) -> Result<Expression, Error> {
2312        let start_span = self.peek()?.span.clone();
2313        let checkpoint = self.checkpoint();
2314        let left = self.parse_range_ellipsis_bound()?;
2315        if !self.at(&TokenKind::Ellipsis)? {
2316            self.restore(checkpoint);
2317            return self.parse_factor();
2318        }
2319
2320        self.next()?;
2321        let right = self.parse_power_for_range_bound()?;
2322        let end_span = right
2323            .source_location
2324            .as_ref()
2325            .map(|s| s.span.clone())
2326            .unwrap_or_else(|| start_span.clone());
2327        let span = self.span_covering(&start_span, &end_span);
2328        self.new_expression(
2329            ExpressionKind::RangeLiteral(Arc::new(left), Arc::new(right)),
2330            self.make_source(span),
2331        )
2332    }
2333
2334    /// One side of `...`: `+`/`-` between powers only (no `*`/`/`/`%` — those bind outside the range).
2335    fn parse_range_ellipsis_bound(&mut self) -> Result<Expression, Error> {
2336        let start_span = self.peek()?.span.clone();
2337        let mut left = self.parse_power_for_range_bound()?;
2338
2339        while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
2340            let op_tok = self.next()?;
2341            let operation = match op_tok.kind {
2342                TokenKind::Plus => ArithmeticComputation::Add,
2343                TokenKind::Minus => ArithmeticComputation::Subtract,
2344                _ => unreachable!("BUG: only + and - should reach here"),
2345            };
2346
2347            let right = self.parse_power_for_range_bound()?;
2348            let end_span = right
2349                .source_location
2350                .as_ref()
2351                .map(|s| s.span.clone())
2352                .unwrap_or_else(|| start_span.clone());
2353            let span = self.span_covering(&start_span, &end_span);
2354
2355            left = self.new_expression(
2356                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2357                self.make_source(span),
2358            )?;
2359        }
2360
2361        Ok(left)
2362    }
2363
2364    fn parse_power_for_range_bound(&mut self) -> Result<Expression, Error> {
2365        let start_span = self.peek()?.span.clone();
2366        let left = self.parse_factor()?;
2367
2368        if self.at(&TokenKind::Caret)? {
2369            self.next()?;
2370            self.check_depth()?;
2371            let right = self.parse_power_for_range_bound()?;
2372            self.depth_tracker.pop_depth();
2373            let end_span = right
2374                .source_location
2375                .as_ref()
2376                .map(|s| s.span.clone())
2377                .unwrap_or_else(|| start_span.clone());
2378            let span = self.span_covering(&start_span, &end_span);
2379
2380            return self.new_expression(
2381                ExpressionKind::Arithmetic(
2382                    Arc::new(left),
2383                    ArithmeticComputation::Power,
2384                    Arc::new(right),
2385                ),
2386                self.make_source(span),
2387            );
2388        }
2389
2390        Ok(left)
2391    }
2392
2393    fn parse_repository_expression(&mut self) -> Result<Expression, Error> {
2394        let start_span = self.peek()?.span.clone();
2395        let mut left = self.parse_term()?;
2396
2397        while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
2398            // Check if this minus is really a binary operator or could be part of something else
2399            // In "X not in calendar year", we don't want to consume "not" as an operator
2400            let op_tok = self.next()?;
2401            let operation = match op_tok.kind {
2402                TokenKind::Plus => ArithmeticComputation::Add,
2403                TokenKind::Minus => ArithmeticComputation::Subtract,
2404                _ => unreachable!("BUG: only + and - should reach here"),
2405            };
2406
2407            let right = self.parse_term()?;
2408            if Self::is_plain_number_literal(&left) && Self::is_unit_conversion(&right) {
2409                let source = right
2410                    .source_location
2411                    .clone()
2412                    .unwrap_or_else(|| self.make_source(start_span.clone()));
2413                return Err(Error::parsing(
2414                    "Cannot add a plain number to a converted value; convert each operand before \
2415                     '+' (e.g. '5 as usd + c as usd')",
2416                    source,
2417                    None::<String>,
2418                ));
2419            }
2420
2421            let end_span = right
2422                .source_location
2423                .as_ref()
2424                .map(|s| s.span.clone())
2425                .unwrap_or_else(|| start_span.clone());
2426            let span = self.span_covering(&start_span, &end_span);
2427
2428            left = self.new_expression(
2429                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2430                self.make_source(span),
2431            )?;
2432        }
2433
2434        Ok(left)
2435    }
2436
2437    fn parse_term(&mut self) -> Result<Expression, Error> {
2438        self.parse_term_with_as(true)
2439    }
2440
2441    fn parse_term_with_as(&mut self, allow_as: bool) -> Result<Expression, Error> {
2442        let start_span = self.peek()?.span.clone();
2443        let mut left = self.parse_power()?;
2444        if allow_as {
2445            left = self.parse_as_chain(left, start_span.clone())?;
2446        }
2447
2448        while self.at_any(&[TokenKind::Star, TokenKind::Slash, TokenKind::Percent])? {
2449            // Be careful: % could be a percent literal suffix (e.g. 50%)
2450            // But here in term context, it's modulo since we already parsed the number
2451            let op_tok = self.next()?;
2452            let operation = match op_tok.kind {
2453                TokenKind::Star => ArithmeticComputation::Multiply,
2454                TokenKind::Slash => ArithmeticComputation::Divide,
2455                TokenKind::Percent => ArithmeticComputation::Modulo,
2456                _ => unreachable!("BUG: only *, /, % should reach here"),
2457            };
2458
2459            let right_start_span = self.peek()?.span.clone();
2460            let mut right = self.parse_power()?;
2461            if allow_as {
2462                right = self.parse_as_chain(right, right_start_span)?;
2463            }
2464            let end_span = right
2465                .source_location
2466                .as_ref()
2467                .map(|s| s.span.clone())
2468                .unwrap_or_else(|| start_span.clone());
2469            let span = self.span_covering(&start_span, &end_span);
2470
2471            left = self.new_expression(
2472                ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
2473                self.make_source(span),
2474            )?;
2475        }
2476
2477        Ok(left)
2478    }
2479
2480    fn parse_power(&mut self) -> Result<Expression, Error> {
2481        let start_span = self.peek()?.span.clone();
2482        let left = self.parse_range_operand()?;
2483
2484        if self.at(&TokenKind::Caret)? {
2485            self.next()?;
2486            self.check_depth()?;
2487            let right = self.parse_power()?;
2488            self.depth_tracker.pop_depth();
2489            let end_span = right
2490                .source_location
2491                .as_ref()
2492                .map(|s| s.span.clone())
2493                .unwrap_or_else(|| start_span.clone());
2494            let span = self.span_covering(&start_span, &end_span);
2495
2496            return self.new_expression(
2497                ExpressionKind::Arithmetic(
2498                    Arc::new(left),
2499                    ArithmeticComputation::Power,
2500                    Arc::new(right),
2501                ),
2502                self.make_source(span),
2503            );
2504        }
2505
2506        Ok(left)
2507    }
2508
2509    fn parse_factor(&mut self) -> Result<Expression, Error> {
2510        let peeked = self.peek()?;
2511        let start_span = peeked.span.clone();
2512
2513        if peeked.kind == TokenKind::Minus {
2514            self.next()?;
2515            let operand = self.parse_primary_or_math()?;
2516            let end_span = operand
2517                .source_location
2518                .as_ref()
2519                .map(|s| s.span.clone())
2520                .unwrap_or_else(|| start_span.clone());
2521            let span = self.span_covering(&start_span, &end_span);
2522
2523            let zero = self.new_expression(
2524                ExpressionKind::Literal(Value::Number(Decimal::ZERO)),
2525                self.make_source(start_span),
2526            )?;
2527            return self.new_expression(
2528                ExpressionKind::Arithmetic(
2529                    Arc::new(zero),
2530                    ArithmeticComputation::Subtract,
2531                    Arc::new(operand),
2532                ),
2533                self.make_source(span),
2534            );
2535        }
2536
2537        if peeked.kind == TokenKind::Plus {
2538            self.next()?;
2539            return self.parse_primary_or_math();
2540        }
2541
2542        self.parse_primary_or_math()
2543    }
2544
2545    fn parse_primary_or_math(&mut self) -> Result<Expression, Error> {
2546        let peeked = self.peek()?;
2547
2548        // Math functions
2549        if is_math_function(&peeked.kind) {
2550            return self.parse_math_function();
2551        }
2552
2553        self.parse_primary()
2554    }
2555
2556    fn parse_math_function(&mut self) -> Result<Expression, Error> {
2557        let func_tok = self.next()?;
2558        let start_span = func_tok.span.clone();
2559
2560        let operator = match func_tok.kind {
2561            TokenKind::Sqrt => MathematicalComputation::Sqrt,
2562            TokenKind::Sin => MathematicalComputation::Sin,
2563            TokenKind::Cos => MathematicalComputation::Cos,
2564            TokenKind::Tan => MathematicalComputation::Tan,
2565            TokenKind::Asin => MathematicalComputation::Asin,
2566            TokenKind::Acos => MathematicalComputation::Acos,
2567            TokenKind::Atan => MathematicalComputation::Atan,
2568            TokenKind::Log => MathematicalComputation::Log,
2569            TokenKind::Exp => MathematicalComputation::Exp,
2570            TokenKind::Abs => MathematicalComputation::Abs,
2571            TokenKind::Floor => MathematicalComputation::Floor,
2572            TokenKind::Ceil => MathematicalComputation::Ceil,
2573            TokenKind::Round => MathematicalComputation::Round,
2574            _ => unreachable!("BUG: only math functions should reach here"),
2575        };
2576
2577        self.check_depth()?;
2578        let operand = self.parse_repository_expression()?;
2579        self.depth_tracker.pop_depth();
2580
2581        let end_span = operand
2582            .source_location
2583            .as_ref()
2584            .map(|s| s.span.clone())
2585            .unwrap_or_else(|| start_span.clone());
2586        let span = self.span_covering(&start_span, &end_span);
2587
2588        self.new_expression(
2589            ExpressionKind::MathematicalComputation(operator, Arc::new(operand)),
2590            self.make_source(span),
2591        )
2592    }
2593
2594    fn parse_primary(&mut self) -> Result<Expression, Error> {
2595        let peeked = self.peek()?;
2596        let start_span = peeked.span.clone();
2597
2598        match &peeked.kind {
2599            // Parenthesized expression
2600            TokenKind::LParen => {
2601                self.next()?; // consume (
2602                let inner = self.parse_expression()?;
2603                self.expect(&TokenKind::RParen)?;
2604                Ok(inner)
2605            }
2606
2607            // Now keyword
2608            TokenKind::Now => {
2609                let tok = self.next()?;
2610                self.new_expression(ExpressionKind::Now, self.make_source(tok.span))
2611            }
2612
2613            TokenKind::Past | TokenKind::Future => {
2614                let tok = self.next()?;
2615                let kind = if tok.kind == TokenKind::Past {
2616                    DateRelativeKind::InPast
2617                } else {
2618                    DateRelativeKind::InFuture
2619                };
2620                let offset = self.parse_repository_expression()?;
2621                let span = self.span_covering(
2622                    &start_span,
2623                    &offset
2624                        .source_location
2625                        .as_ref()
2626                        .map(|s| s.span.clone())
2627                        .unwrap_or(start_span.clone()),
2628                );
2629                self.new_expression(
2630                    ExpressionKind::PastFutureRange(kind, Arc::new(offset)),
2631                    self.make_source(span),
2632                )
2633            }
2634
2635            // String literal
2636            TokenKind::StringLit => {
2637                let tok = self.next()?;
2638                let content = unquote_string(&tok.text);
2639                self.new_expression(
2640                    ExpressionKind::Literal(Value::Text(content)),
2641                    self.make_source(tok.span),
2642                )
2643            }
2644
2645            // Boolean literals
2646            k if is_boolean_keyword(k) => {
2647                let tok = self.next()?;
2648                self.new_expression(
2649                    ExpressionKind::Literal(Value::Boolean(token_kind_to_boolean_value(&tok.kind))),
2650                    self.make_source(tok.span),
2651                )
2652            }
2653
2654            // Number literal (could be: plain number, date, time, duration, percent, unit)
2655            TokenKind::NumberLit => self.parse_number_expression(),
2656
2657            // Reference (identifier, type keyword)
2658            k if can_be_reference_segment(k) => {
2659                let reference = self.parse_expression_reference()?;
2660                let span = self.span_covering(&start_span, &self.last_span);
2661                self.new_expression(ExpressionKind::Reference(reference), self.make_source(span))
2662            }
2663
2664            _ => {
2665                let tok = self.next()?;
2666                Err(self.error_at_token(
2667                    &tok,
2668                    format!("Expected an expression, found '{}'", tok.text),
2669                ))
2670            }
2671        }
2672    }
2673
2674    fn parse_number_expression(&mut self) -> Result<Expression, Error> {
2675        let num_tok = self.next()?;
2676        let num_text = num_tok.text.clone();
2677        let start_span = num_tok.span.clone();
2678
2679        // Check if this is a date literal (YYYY-MM-DD)
2680        if num_text.len() == 4
2681            && num_text.chars().all(|c| c.is_ascii_digit())
2682            && self.at(&TokenKind::Minus)?
2683        {
2684            // Peek further: if next-next is a number, this is likely a date
2685            // We need to be careful: "2024 - 5" is arithmetic, "2024-01-15" is a date
2686            // Date format requires: YYYY-MM-DD where MM and DD are 2 digits
2687            // This is ambiguous at the token level. Let's check if the pattern matches.
2688            // Since dates use -NN- pattern and arithmetic uses - N pattern (with spaces),
2689            // we can use the span positions to disambiguate.
2690            let minus_span = self.peek()?.span.clone();
2691            // If minus is immediately adjacent to the number (no space), it's a date
2692            if minus_span.start == start_span.end {
2693                let value = self.parse_date_literal(num_text, start_span.clone())?;
2694                return self
2695                    .new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
2696            }
2697        }
2698
2699        // Check for time literal (HH:MM:SS)
2700        if num_text.len() == 2
2701            && num_text.chars().all(|c| c.is_ascii_digit())
2702            && self.at(&TokenKind::Colon)?
2703        {
2704            let colon_span = self.peek()?.span.clone();
2705            if colon_span.start == start_span.end {
2706                let value = self.try_parse_time_literal(num_text, start_span.clone())?;
2707                return self
2708                    .new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
2709            }
2710        }
2711
2712        // Check for %% (permille)
2713        if self.at(&TokenKind::PercentPercent)? {
2714            let pp_tok = self.next()?;
2715            if let Ok(next_peek) = self.peek() {
2716                if next_peek.kind == TokenKind::NumberLit {
2717                    return Err(self.error_at_token(
2718                        &pp_tok,
2719                        "Permille literal cannot be followed by a digit",
2720                    ));
2721                }
2722            }
2723            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2724            return self.new_expression(
2725                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
2726                self.make_source(start_span),
2727            );
2728        }
2729
2730        // Check for % (percent)
2731        if self.at(&TokenKind::Percent)? {
2732            let pct_span = self.peek()?.span.clone();
2733            // Only consume % if it's directly adjacent (no space) for the shorthand syntax
2734            // Or if it's "50 %" (space separated is also valid per the grammar)
2735            let pct_tok = self.next()?;
2736            if let Ok(next_peek) = self.peek() {
2737                if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
2738                    return Err(self.error_at_token(
2739                        &pct_tok,
2740                        "Percent literal cannot be followed by a digit",
2741                    ));
2742                }
2743            }
2744            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2745            return self.new_expression(
2746                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "percent".to_string())),
2747                self.make_source(self.span_covering(&start_span, &pct_span)),
2748            );
2749        }
2750
2751        // Check for "permille" keyword
2752        if self.at(&TokenKind::Permille)? {
2753            self.next()?;
2754            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2755            return self.new_expression(
2756                ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
2757                self.make_source(start_span),
2758            );
2759        }
2760
2761        if can_be_label(&self.peek()?.kind) {
2762            let unit_tok = self.next()?;
2763            let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2764            return self.new_expression(
2765                ExpressionKind::Literal(Value::NumberWithUnit(decimal, unit_tok.text.clone())),
2766                self.make_source(self.span_covering(&start_span, &unit_tok.span)),
2767            );
2768        }
2769
2770        // Plain number
2771        let decimal = parse_decimal_string(&num_text, &start_span, self)?;
2772        self.new_expression(
2773            ExpressionKind::Literal(Value::Number(decimal)),
2774            self.make_source(start_span),
2775        )
2776    }
2777
2778    fn parse_expression_reference(&mut self) -> Result<Reference, Error> {
2779        let mut segments = Vec::new();
2780
2781        let first = self.next()?;
2782        segments.push(first.text.clone());
2783
2784        while self.at(&TokenKind::Dot)? {
2785            self.next()?; // consume .
2786            let seg = self.next()?;
2787            if !can_be_reference_segment(&seg.kind) {
2788                return Err(self.error_at_token(
2789                    &seg,
2790                    format!("Expected an identifier after '.', found {}", seg.kind),
2791                ));
2792            }
2793            segments.push(seg.text.clone());
2794        }
2795
2796        Ok(Reference::from_path(segments))
2797    }
2798}
2799
2800// ============================================================================
2801// Helper functions
2802// ============================================================================
2803
2804fn unquote_string(s: &str) -> String {
2805    if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
2806        s[1..s.len() - 1].to_string()
2807    } else {
2808        s.to_string()
2809    }
2810}
2811
2812fn parse_decimal_string(text: &str, span: &Span, parser: &Parser) -> Result<Decimal, Error> {
2813    let clean = text.replace(['_', ','], "");
2814    Decimal::from_str(&clean).map_err(|_| {
2815        Error::parsing(
2816            format!(
2817                "Invalid number: '{}'. Expected a valid decimal number (e.g., 42, 3.14, 1_000_000)",
2818                text
2819            ),
2820            parser.make_source(span.clone()),
2821            None::<String>,
2822        )
2823    })
2824}
2825
2826fn is_comparison_operator(kind: &TokenKind) -> bool {
2827    matches!(
2828        kind,
2829        TokenKind::Gt | TokenKind::Lt | TokenKind::Gte | TokenKind::Lte | TokenKind::Is
2830    )
2831}
2832
2833// Helper trait for TokenKind
2834impl TokenKind {
2835    fn is_identifier_like(&self) -> bool {
2836        matches!(self, TokenKind::Identifier)
2837            || can_be_label(self)
2838            || is_boolean_keyword(self)
2839            || is_math_function(self)
2840    }
2841}