Skip to main content

lemma/parsing/
parser.rs

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