Skip to main content

harn_parser/parser/
expressions.rs

1use crate::ast::*;
2use harn_lexer::{Span, TokenKind};
3
4use super::error::ParserError;
5use super::state::Parser;
6
7impl Parser {
8    /// Parse a single expression (for string interpolation).
9    ///
10    /// An interpolation hole (`${ ... }`) must contain exactly one expression.
11    /// After parsing it, require that the token stream is exhausted so leftover
12    /// tokens are reported as a parse error instead of being silently dropped —
13    /// otherwise `${a b}` would render just `a` and `${1e20}` just `1` (`e20`
14    /// is a separate identifier, since scientific notation is not a float
15    /// literal), masking the typo.
16    pub fn parse_single_expression(&mut self) -> Result<SNode, ParserError> {
17        self.check_token_nesting_limit()?;
18        self.skip_newlines();
19        let expr = self.parse_expression()?;
20        self.skip_newlines();
21        if !self.is_at_end() {
22            return Err(self.error("end of interpolated expression"));
23        }
24        Ok(expr)
25    }
26
27    pub(super) fn parse_nested_expression(
28        &mut self,
29        context: &'static str,
30    ) -> Result<SNode, ParserError> {
31        self.with_nesting(context, |parser| parser.parse_expression())
32    }
33
34    pub(super) fn parse_expression(&mut self) -> Result<SNode, ParserError> {
35        self.skip_newlines();
36        self.parse_pipe()
37    }
38
39    pub(super) fn parse_pipe(&mut self) -> Result<SNode, ParserError> {
40        let mut left = self.parse_range()?;
41        while self.check_skip_newlines(&TokenKind::Pipe) {
42            let start = left.span;
43            self.advance();
44            self.skip_newlines();
45            let right = self.parse_range()?;
46            left = spanned(
47                Node::BinaryOp {
48                    op: "|>".into(),
49                    left: Box::new(left),
50                    right: Box::new(right),
51                },
52                Span::merge(start, self.prev_span()),
53            );
54        }
55        Ok(left)
56    }
57
58    pub(super) fn parse_range(&mut self) -> Result<SNode, ParserError> {
59        let left = self.parse_ternary()?;
60        if self.check(&TokenKind::To) {
61            let start = left.span;
62            self.advance();
63            let right = self.parse_ternary()?;
64            let inclusive = if self.check(&TokenKind::Exclusive) {
65                self.advance();
66                false
67            } else {
68                true
69            };
70            return Ok(spanned(
71                Node::RangeExpr {
72                    start: Box::new(left),
73                    end: Box::new(right),
74                    inclusive,
75                },
76                Span::merge(start, self.prev_span()),
77            ));
78        }
79        Ok(left)
80    }
81
82    pub(super) fn parse_ternary(&mut self) -> Result<SNode, ParserError> {
83        let condition = self.parse_logical_or()?;
84        // `?` may appear on the next line as a wrap-to-new-line continuation.
85        // Postfix `?` (try) is already consumed by `parse_postfix`, so by the
86        // time we reach here a `?` (possibly across a newline) is unambiguously
87        // a ternary operator.
88        if !self.check_skip_newlines(&TokenKind::Question) {
89            return Ok(condition);
90        }
91        let start = condition.span;
92        self.advance(); // skip ?
93        self.skip_newlines();
94        let true_val = self.with_nesting("ternary expression", |parser| parser.parse_ternary())?;
95        // `consume` already skips leading newlines for `:`.
96        self.consume(&TokenKind::Colon, ":")?;
97        self.skip_newlines();
98        let false_val = self.with_nesting("ternary expression", |parser| parser.parse_ternary())?;
99        Ok(spanned(
100            Node::Ternary {
101                condition: Box::new(condition),
102                true_expr: Box::new(true_val),
103                false_expr: Box::new(false_val),
104            },
105            Span::merge(start, self.prev_span()),
106        ))
107    }
108
109    // `??` binds tighter than arithmetic/comparison but looser than `* / % **`,
110    // so `xs?.count ?? 0 > 0` parses as `(xs?.count ?? 0) > 0`.
111    pub(super) fn parse_nil_coalescing(&mut self) -> Result<SNode, ParserError> {
112        let mut left = self.parse_multiplicative()?;
113        while self.check_skip_newlines(&TokenKind::NilCoal) {
114            let start = left.span;
115            self.advance();
116            self.skip_newlines();
117            let right = self.parse_multiplicative()?;
118            left = spanned(
119                Node::BinaryOp {
120                    op: "??".into(),
121                    left: Box::new(left),
122                    right: Box::new(right),
123                },
124                Span::merge(start, self.prev_span()),
125            );
126        }
127        Ok(left)
128    }
129
130    pub(super) fn parse_logical_or(&mut self) -> Result<SNode, ParserError> {
131        let mut left = self.parse_logical_and()?;
132        while self.check_skip_newlines(&TokenKind::Or) {
133            let start = left.span;
134            self.advance();
135            self.skip_newlines();
136            let right = self.parse_logical_and()?;
137            left = spanned(
138                Node::BinaryOp {
139                    op: "||".into(),
140                    left: Box::new(left),
141                    right: Box::new(right),
142                },
143                Span::merge(start, self.prev_span()),
144            );
145        }
146        Ok(left)
147    }
148
149    pub(super) fn parse_logical_and(&mut self) -> Result<SNode, ParserError> {
150        let mut left = self.parse_equality()?;
151        while self.check_skip_newlines(&TokenKind::And) {
152            let start = left.span;
153            self.advance();
154            self.skip_newlines();
155            let right = self.parse_equality()?;
156            left = spanned(
157                Node::BinaryOp {
158                    op: "&&".into(),
159                    left: Box::new(left),
160                    right: Box::new(right),
161                },
162                Span::merge(start, self.prev_span()),
163            );
164        }
165        Ok(left)
166    }
167
168    pub(super) fn parse_equality(&mut self) -> Result<SNode, ParserError> {
169        let mut left = self.parse_comparison()?;
170        while self.check_skip_newlines(&TokenKind::Eq) || self.check_skip_newlines(&TokenKind::Neq)
171        {
172            let start = left.span;
173            let op = if self.check(&TokenKind::Eq) {
174                "=="
175            } else {
176                "!="
177            };
178            self.advance();
179            self.skip_newlines();
180            let right = self.parse_comparison()?;
181            left = spanned(
182                Node::BinaryOp {
183                    op: op.into(),
184                    left: Box::new(left),
185                    right: Box::new(right),
186                },
187                Span::merge(start, self.prev_span()),
188            );
189        }
190        Ok(left)
191    }
192
193    pub(super) fn parse_comparison(&mut self) -> Result<SNode, ParserError> {
194        let mut left = self.parse_additive()?;
195        loop {
196            if self.check_skip_newlines(&TokenKind::Lt)
197                || self.check_skip_newlines(&TokenKind::Gt)
198                || self.check_skip_newlines(&TokenKind::Lte)
199                || self.check_skip_newlines(&TokenKind::Gte)
200            {
201                let start = left.span;
202                let op = match self.current().map(|t| &t.kind) {
203                    Some(TokenKind::Lt) => "<",
204                    Some(TokenKind::Gt) => ">",
205                    Some(TokenKind::Lte) => "<=",
206                    Some(TokenKind::Gte) => ">=",
207                    _ => "<",
208                };
209                self.advance();
210                self.skip_newlines();
211                let right = self.parse_additive()?;
212                left = spanned(
213                    Node::BinaryOp {
214                        op: op.into(),
215                        left: Box::new(left),
216                        right: Box::new(right),
217                    },
218                    Span::merge(start, self.prev_span()),
219                );
220            } else if self.check(&TokenKind::In) {
221                let start = left.span;
222                self.advance();
223                self.skip_newlines();
224                let right = self.parse_additive()?;
225                left = spanned(
226                    Node::BinaryOp {
227                        op: "in".into(),
228                        left: Box::new(left),
229                        right: Box::new(right),
230                    },
231                    Span::merge(start, self.prev_span()),
232                );
233            } else if self.check_identifier("not") {
234                let saved = self.pos;
235                self.advance();
236                if self.check(&TokenKind::In) {
237                    let start = left.span;
238                    self.advance();
239                    self.skip_newlines();
240                    let right = self.parse_additive()?;
241                    left = spanned(
242                        Node::BinaryOp {
243                            op: "not_in".into(),
244                            left: Box::new(left),
245                            right: Box::new(right),
246                        },
247                        Span::merge(start, self.prev_span()),
248                    );
249                } else {
250                    self.pos = saved;
251                    break;
252                }
253            } else {
254                break;
255            }
256        }
257        Ok(left)
258    }
259
260    pub(super) fn parse_additive(&mut self) -> Result<SNode, ParserError> {
261        let mut left = self.parse_nil_coalescing()?;
262        while self.check_skip_newlines(&TokenKind::Plus) || self.check(&TokenKind::Minus) {
263            let start = left.span;
264            let op = if self.check(&TokenKind::Plus) {
265                "+"
266            } else {
267                "-"
268            };
269            self.advance();
270            self.skip_newlines();
271            let right = self.parse_nil_coalescing()?;
272            left = spanned(
273                Node::BinaryOp {
274                    op: op.into(),
275                    left: Box::new(left),
276                    right: Box::new(right),
277                },
278                Span::merge(start, self.prev_span()),
279            );
280        }
281        Ok(left)
282    }
283
284    pub(super) fn parse_multiplicative(&mut self) -> Result<SNode, ParserError> {
285        let mut left = self.parse_unary()?;
286        while self.check_skip_newlines(&TokenKind::Star)
287            || self.check_skip_newlines(&TokenKind::Slash)
288            || self.check_skip_newlines(&TokenKind::Percent)
289        {
290            let start = left.span;
291            let op = if self.check(&TokenKind::Star) {
292                "*"
293            } else if self.check(&TokenKind::Slash) {
294                "/"
295            } else {
296                "%"
297            };
298            self.advance();
299            self.skip_newlines();
300            let right = self.parse_unary()?;
301            left = spanned(
302                Node::BinaryOp {
303                    op: op.into(),
304                    left: Box::new(left),
305                    right: Box::new(right),
306                },
307                Span::merge(start, self.prev_span()),
308            );
309        }
310        Ok(left)
311    }
312
313    // `**` binds more tightly than a unary prefix on its *left* operand, so
314    // `-2 ** 2` parses as `-(2 ** 2)` (matching Python, Ruby, and ordinary math
315    // notation rather than the spreadsheet `(-2) ** 2` reading). The base is
316    // therefore a `postfix` expression, while the exponent recurses through
317    // `parse_unary` so a unary prefix on the *right* still works (`2 ** -3` is
318    // `2 ** (-3)`) and chained `**` stays right-associative.
319    pub(super) fn parse_exponent(&mut self) -> Result<SNode, ParserError> {
320        let left = self.parse_postfix()?;
321        if !self.check_skip_newlines(&TokenKind::Pow) {
322            return Ok(left);
323        }
324
325        let start = left.span;
326        self.advance();
327        self.skip_newlines();
328        let right = self.with_nesting("exponent expression", |parser| parser.parse_unary())?;
329        Ok(spanned(
330            Node::BinaryOp {
331                op: "**".into(),
332                left: Box::new(left),
333                right: Box::new(right),
334            },
335            Span::merge(start, self.prev_span()),
336        ))
337    }
338
339    pub(super) fn parse_unary(&mut self) -> Result<SNode, ParserError> {
340        if self.check(&TokenKind::Not) {
341            let start = self.current_span();
342            self.advance();
343            let operand = self.with_nesting("unary expression", |parser| parser.parse_unary())?;
344            return Ok(spanned(
345                Node::UnaryOp {
346                    op: "!".into(),
347                    operand: Box::new(operand),
348                },
349                Span::merge(start, self.prev_span()),
350            ));
351        }
352        if self.check(&TokenKind::Minus) {
353            let start = self.current_span();
354            self.advance();
355            let operand = self.with_nesting("unary expression", |parser| parser.parse_unary())?;
356            return Ok(spanned(
357                Node::UnaryOp {
358                    op: "-".into(),
359                    operand: Box::new(operand),
360                },
361                Span::merge(start, self.prev_span()),
362            ));
363        }
364        self.parse_exponent()
365    }
366
367    pub(super) fn parse_postfix(&mut self) -> Result<SNode, ParserError> {
368        let mut expr = self.parse_primary()?;
369
370        loop {
371            if self.check_skip_newlines(&TokenKind::Dot)
372                || self.check_skip_newlines(&TokenKind::QuestionDot)
373            {
374                let optional = self.check(&TokenKind::QuestionDot);
375                let start = expr.span;
376                self.advance();
377                let member = self.consume_identifier_or_keyword("member name")?;
378                if self.check(&TokenKind::LParen) {
379                    self.advance();
380                    let args = self.parse_arg_list()?;
381                    self.consume(&TokenKind::RParen, ")")?;
382                    if optional {
383                        expr = spanned(
384                            Node::OptionalMethodCall {
385                                object: Box::new(expr),
386                                method: member,
387                                args,
388                            },
389                            Span::merge(start, self.prev_span()),
390                        );
391                    } else {
392                        expr = spanned(
393                            Node::MethodCall {
394                                object: Box::new(expr),
395                                method: member,
396                                args,
397                            },
398                            Span::merge(start, self.prev_span()),
399                        );
400                    }
401                } else if optional {
402                    expr = spanned(
403                        Node::OptionalPropertyAccess {
404                            object: Box::new(expr),
405                            property: member,
406                        },
407                        Span::merge(start, self.prev_span()),
408                    );
409                } else {
410                    expr = spanned(
411                        Node::PropertyAccess {
412                            object: Box::new(expr),
413                            property: member,
414                        },
415                        Span::merge(start, self.prev_span()),
416                    );
417                }
418            } else if self.check(&TokenKind::LBracket) {
419                let start = expr.span;
420                self.advance();
421
422                // Disambiguate `[:end]` / `[start:end]` / `[start:]` slices from
423                // `[index]` subscript access.
424                if self.check(&TokenKind::Colon) {
425                    self.advance();
426                    let end_expr = if self.check(&TokenKind::RBracket) {
427                        None
428                    } else {
429                        Some(Box::new(self.parse_nested_expression("slice bound")?))
430                    };
431                    self.consume(&TokenKind::RBracket, "]")?;
432                    expr = spanned(
433                        Node::SliceAccess {
434                            object: Box::new(expr),
435                            start: None,
436                            end: end_expr,
437                        },
438                        Span::merge(start, self.prev_span()),
439                    );
440                } else {
441                    let index = self.parse_nested_expression("subscript index")?;
442                    if self.check(&TokenKind::Colon) {
443                        self.advance();
444                        let end_expr = if self.check(&TokenKind::RBracket) {
445                            None
446                        } else {
447                            Some(Box::new(self.parse_nested_expression("slice bound")?))
448                        };
449                        self.consume(&TokenKind::RBracket, "]")?;
450                        expr = spanned(
451                            Node::SliceAccess {
452                                object: Box::new(expr),
453                                start: Some(Box::new(index)),
454                                end: end_expr,
455                            },
456                            Span::merge(start, self.prev_span()),
457                        );
458                    } else {
459                        self.consume(&TokenKind::RBracket, "]")?;
460                        expr = spanned(
461                            Node::SubscriptAccess {
462                                object: Box::new(expr),
463                                index: Box::new(index),
464                            },
465                            Span::merge(start, self.prev_span()),
466                        );
467                    }
468                }
469            } else if self.check(&TokenKind::LBrace) {
470                let struct_name = match &expr.node {
471                    Node::Identifier(name) if self.is_struct_construct_lookahead(name) => {
472                        Some(name.clone())
473                    }
474                    _ => None,
475                };
476                let Some(struct_name) = struct_name else {
477                    break;
478                };
479                let start = expr.span;
480                self.advance();
481                let dict = self.parse_dict_literal(start)?;
482                let fields = match dict.node {
483                    Node::DictLiteral(fields) => fields,
484                    _ => unreachable!("dict parser must return a dict literal"),
485                };
486                expr = spanned(
487                    Node::StructConstruct {
488                        struct_name,
489                        fields,
490                    },
491                    dict.span,
492                );
493            } else if self.check(&TokenKind::Lt) && matches!(expr.node, Node::Identifier(_)) {
494                let saved_pos = self.pos;
495                let start = expr.span;
496                self.advance();
497                let parsed_type_args = self.parse_type_arg_list();
498                if let Ok(type_args) = parsed_type_args {
499                    if self.check(&TokenKind::LParen) {
500                        self.advance();
501                        let args = self.parse_arg_list()?;
502                        self.consume(&TokenKind::RParen, ")")?;
503                        if let Node::Identifier(name) = expr.node {
504                            expr = spanned(
505                                Node::FunctionCall {
506                                    name,
507                                    type_args,
508                                    args,
509                                },
510                                Span::merge(start, self.prev_span()),
511                            );
512                        }
513                    } else {
514                        self.pos = saved_pos;
515                        break;
516                    }
517                } else {
518                    self.pos = saved_pos;
519                    break;
520                }
521            } else if self.check(&TokenKind::LParen) && matches!(expr.node, Node::Identifier(_)) {
522                let start = expr.span;
523                self.advance();
524                let args = self.parse_arg_list()?;
525                self.consume(&TokenKind::RParen, ")")?;
526                if let Node::Identifier(name) = expr.node {
527                    expr = spanned(
528                        Node::FunctionCall {
529                            name,
530                            type_args: Vec::new(),
531                            args,
532                        },
533                        Span::merge(start, self.prev_span()),
534                    );
535                }
536            } else if self.check(&TokenKind::Question) {
537                // Disambiguate `?[index]` (optional subscript), `expr?`
538                // (postfix try), and `expr ? a : b` (ternary).
539                if self.question_starts_ternary_branch() {
540                    break;
541                }
542                if matches!(self.peek_kind_at(1), Some(TokenKind::LBracket)) {
543                    let start = expr.span;
544                    self.advance(); // consume ?
545                    self.advance(); // consume [
546                    let index = self.parse_nested_expression("optional subscript index")?;
547                    self.consume(&TokenKind::RBracket, "]")?;
548                    expr = spanned(
549                        Node::OptionalSubscriptAccess {
550                            object: Box::new(expr),
551                            index: Box::new(index),
552                        },
553                        Span::merge(start, self.prev_span()),
554                    );
555                    continue;
556                }
557                let start = expr.span;
558                self.advance();
559                expr = spanned(
560                    Node::TryOperator {
561                        operand: Box::new(expr),
562                    },
563                    Span::merge(start, self.prev_span()),
564                );
565            } else {
566                break;
567            }
568        }
569
570        Ok(expr)
571    }
572
573    fn question_starts_ternary_branch(&self) -> bool {
574        // Look at the first non-newline token after `?`. A ternary may wrap
575        // its true-branch onto a new line (`cond ?\n value : other`), so a
576        // newline immediately after `?` must not cause us to misclassify this
577        // as a postfix-`?`.
578        let next = self
579            .tokens
580            .iter()
581            .skip(self.pos + 1)
582            .find(|t| t.kind != TokenKind::Newline)
583            .map(|t| &t.kind);
584        next.is_some_and(Self::token_starts_ternary_branch)
585            && self.question_has_top_level_ternary_colon()
586    }
587
588    fn token_starts_ternary_branch(kind: &TokenKind) -> bool {
589        matches!(
590            kind,
591            TokenKind::Identifier(_)
592                | TokenKind::IntLiteral(_)
593                | TokenKind::FloatLiteral(_)
594                | TokenKind::StringLiteral(_)
595                | TokenKind::RawStringLiteral(_)
596                | TokenKind::InterpolatedString(_)
597                | TokenKind::True
598                | TokenKind::False
599                | TokenKind::Nil
600                | TokenKind::LParen
601                | TokenKind::LBracket
602                | TokenKind::LBrace
603                | TokenKind::Not
604                | TokenKind::Minus
605                | TokenKind::Fn
606                | TokenKind::If
607                | TokenKind::Match
608                | TokenKind::Try
609                | TokenKind::Spawn
610                | TokenKind::Parallel
611                | TokenKind::Retry
612                | TokenKind::Deadline
613                | TokenKind::RequestApproval
614                | TokenKind::DualControl
615                | TokenKind::AskUser
616                | TokenKind::EscalateTo
617                | TokenKind::DurationLiteral(_)
618        )
619    }
620
621    fn question_has_top_level_ternary_colon(&self) -> bool {
622        let mut delimiter_depth = 0usize;
623        // True when the most recent significant top-level token was `?` or
624        // `:` — i.e. we're scanning for the start of a branch and a newline
625        // here is just a wrap, not an end-of-ternary.
626        let mut at_branch_start = true;
627        for (pos, token) in self.tokens.iter().enumerate().skip(self.pos + 1) {
628            if delimiter_depth == 0 {
629                match token.kind {
630                    TokenKind::Colon => return true,
631                    TokenKind::Newline => {
632                        if at_branch_start {
633                            // `?` (or `:`) was the last significant token; this
634                            // newline simply wraps the branch onto a new line.
635                            continue;
636                        }
637                        if self.next_non_newline_continues_ternary_branch(pos + 1) {
638                            continue;
639                        }
640                        return false;
641                    }
642                    TokenKind::RParen
643                    | TokenKind::RBracket
644                    | TokenKind::RBrace
645                    | TokenKind::Eof => {
646                        return false;
647                    }
648                    _ => {
649                        at_branch_start = false;
650                    }
651                }
652            }
653
654            match token.kind {
655                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => {
656                    delimiter_depth += 1;
657                }
658                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
659                    delimiter_depth = delimiter_depth.saturating_sub(1);
660                }
661                TokenKind::Eof => return false,
662                _ => {}
663            }
664        }
665        false
666    }
667
668    fn next_non_newline_continues_ternary_branch(&self, start_pos: usize) -> bool {
669        let Some(kind) = self
670            .tokens
671            .iter()
672            .skip(start_pos)
673            .find(|token| token.kind != TokenKind::Newline)
674            .map(|token| &token.kind)
675        else {
676            return false;
677        };
678        matches!(
679            kind,
680            TokenKind::Colon
681                | TokenKind::Plus
682                | TokenKind::Star
683                | TokenKind::Slash
684                | TokenKind::Percent
685                | TokenKind::Pow
686                | TokenKind::And
687                | TokenKind::Or
688                | TokenKind::Eq
689                | TokenKind::Neq
690                | TokenKind::Lt
691                | TokenKind::Gt
692                | TokenKind::Lte
693                | TokenKind::Gte
694                | TokenKind::NilCoal
695                | TokenKind::Pipe
696                | TokenKind::Dot
697                | TokenKind::QuestionDot
698        )
699    }
700
701    pub(super) fn parse_primary(&mut self) -> Result<SNode, ParserError> {
702        let tok = self.current().ok_or_else(|| ParserError::UnexpectedEof {
703            expected: "expression".into(),
704            span: self.prev_span(),
705        })?;
706        let start = self.current_span();
707
708        match &tok.kind {
709            TokenKind::StringLiteral(s) => {
710                let s = s.clone();
711                self.advance();
712                Ok(spanned(
713                    Node::StringLiteral(s),
714                    Span::merge(start, self.prev_span()),
715                ))
716            }
717            TokenKind::RawStringLiteral(s) => {
718                let s = s.clone();
719                self.advance();
720                Ok(spanned(
721                    Node::RawStringLiteral(s),
722                    Span::merge(start, self.prev_span()),
723                ))
724            }
725            TokenKind::InterpolatedString(segments) => {
726                let segments = segments.clone();
727                self.advance();
728                Ok(spanned(
729                    Node::InterpolatedString(segments),
730                    Span::merge(start, self.prev_span()),
731                ))
732            }
733            TokenKind::IntLiteral(n) => {
734                let n = *n;
735                self.advance();
736                Ok(spanned(
737                    Node::IntLiteral(n),
738                    Span::merge(start, self.prev_span()),
739                ))
740            }
741            TokenKind::FloatLiteral(n) => {
742                let n = *n;
743                self.advance();
744                Ok(spanned(
745                    Node::FloatLiteral(n),
746                    Span::merge(start, self.prev_span()),
747                ))
748            }
749            TokenKind::True => {
750                self.advance();
751                Ok(spanned(
752                    Node::BoolLiteral(true),
753                    Span::merge(start, self.prev_span()),
754                ))
755            }
756            TokenKind::False => {
757                self.advance();
758                Ok(spanned(
759                    Node::BoolLiteral(false),
760                    Span::merge(start, self.prev_span()),
761                ))
762            }
763            TokenKind::Nil => {
764                self.advance();
765                Ok(spanned(
766                    Node::NilLiteral,
767                    Span::merge(start, self.prev_span()),
768                ))
769            }
770            TokenKind::Identifier(name)
771                if name == "cost_route" && self.peek_kind() == Some(&TokenKind::LBrace) =>
772            {
773                self.parse_cost_route()
774            }
775            TokenKind::Identifier(name) => {
776                let name = name.clone();
777                self.advance();
778                Ok(spanned(
779                    Node::Identifier(name),
780                    Span::merge(start, self.prev_span()),
781                ))
782            }
783            TokenKind::LParen => {
784                self.advance();
785                let expr = self.with_nesting("parenthesized expression", |parser| {
786                    let expr = parser.parse_expression()?;
787                    parser.consume(&TokenKind::RParen, ")")?;
788                    Ok(expr)
789                })?;
790                Ok(expr)
791            }
792            TokenKind::LBracket => self.parse_list_literal(),
793            TokenKind::LBrace => self.parse_dict_or_closure(),
794            TokenKind::Parallel => self.parse_parallel(),
795            TokenKind::Retry => self.parse_retry(),
796            TokenKind::If => self.parse_if_else(),
797            TokenKind::Spawn => self.parse_spawn_expr(),
798            TokenKind::RequestApproval => self.parse_hitl_expr(HitlKind::RequestApproval),
799            TokenKind::DualControl => self.parse_hitl_expr(HitlKind::DualControl),
800            TokenKind::AskUser => self.parse_hitl_expr(HitlKind::AskUser),
801            TokenKind::EscalateTo => self.parse_hitl_expr(HitlKind::EscalateTo),
802            TokenKind::DurationLiteral(ms) => {
803                let ms = *ms;
804                self.advance();
805                Ok(spanned(
806                    Node::DurationLiteral(ms),
807                    Span::merge(start, self.prev_span()),
808                ))
809            }
810            TokenKind::Deadline => self.parse_deadline(),
811            TokenKind::Try => self.parse_try_catch(),
812            TokenKind::Match => self.parse_match(),
813            TokenKind::Fn => self.parse_fn_expr(),
814            // Heredoc `<<TAG ... TAG` is only valid inside LLM tool-call JSON;
815            // in source-position expressions, redirect authors to triple-quoted strings.
816            TokenKind::Lt
817                if matches!(self.peek_kind(), Some(&TokenKind::Lt))
818                    && matches!(self.peek_kind_at(2), Some(TokenKind::Identifier(_))) =>
819            {
820                Err(ParserError::Unexpected {
821                    got: "`<<` heredoc-like syntax".to_string(),
822                    expected: "an expression — heredocs are only valid \
823                               inside LLM tool-call argument JSON; \
824                               for multiline strings in source code use \
825                               triple-quoted `\"\"\"...\"\"\"`"
826                        .to_string(),
827                    span: start,
828                })
829            }
830            _ => Err(self.error("expression")),
831        }
832    }
833
834    /// Anonymous function `fn(params) { body }`. Sets `fn_syntax: true` on the
835    /// Closure so the formatter can round-trip the original syntax.
836    pub(super) fn parse_fn_expr(&mut self) -> Result<SNode, ParserError> {
837        let start = self.current_span();
838        self.consume(&TokenKind::Fn, "fn")?;
839        self.consume(&TokenKind::LParen, "(")?;
840        let params = self.parse_typed_param_list()?;
841        self.consume(&TokenKind::RParen, ")")?;
842        self.consume(&TokenKind::LBrace, "{")?;
843        let body = self.parse_block()?;
844        self.consume(&TokenKind::RBrace, "}")?;
845        Ok(spanned(
846            Node::Closure {
847                params,
848                body,
849                fn_syntax: true,
850            },
851            Span::merge(start, self.prev_span()),
852        ))
853    }
854
855    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
856        let start = self.current_span();
857        self.consume(&TokenKind::Spawn, "spawn")?;
858        self.consume(&TokenKind::LBrace, "{")?;
859        let body = self.parse_block()?;
860        self.consume(&TokenKind::RBrace, "}")?;
861        Ok(spanned(
862            Node::SpawnExpr { body },
863            Span::merge(start, self.prev_span()),
864        ))
865    }
866
867    /// Parse a first-class HITL primitive: one of `request_approval`,
868    /// `dual_control`, `ask_user`, `escalate_to`. The keyword has
869    /// already been peeked at; this method consumes it plus the
870    /// parenthesized argument list.
871    ///
872    /// Each argument is either positional (`expr`) or named
873    /// (`name: expr`). The grammar accepts the existing positional
874    /// invocation form so existing scripts and conformance tests
875    /// (e.g. `request_approval("deploy", {quorum: 2, ...})`) keep
876    /// working unchanged. Argument validation (required names,
877    /// duplicates, ordering) is performed by the typechecker.
878    pub(super) fn parse_hitl_expr(&mut self, kind: HitlKind) -> Result<SNode, ParserError> {
879        let start = self.current_span();
880        let kw_token = match kind {
881            HitlKind::RequestApproval => TokenKind::RequestApproval,
882            HitlKind::DualControl => TokenKind::DualControl,
883            HitlKind::AskUser => TokenKind::AskUser,
884            HitlKind::EscalateTo => TokenKind::EscalateTo,
885        };
886        self.consume(&kw_token, kind.as_keyword())?;
887        self.consume(&TokenKind::LParen, "(")?;
888        self.skip_newlines();
889
890        let mut args: Vec<HitlArg> = Vec::new();
891        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
892            let arg_start = self.current_span();
893            // Look ahead two tokens to detect `identifier ":"`. The
894            // identifier itself is parsed as part of the expression so
895            // we keep the dispatch simple: peek for `Identifier` then
896            // a `Colon` to identify a named argument.
897            // `peek_kind_at(0)` is the current token; `peek_kind_at(1)`
898            // is one ahead. A named-arg slot starts with `ident :`.
899            let is_named = matches!(
900                (self.peek_kind_at(0), self.peek_kind_at(1)),
901                (Some(TokenKind::Identifier(_)), Some(TokenKind::Colon))
902            );
903            let (name, value) = if is_named {
904                let Some(TokenKind::Identifier(raw)) = self.peek_kind_at(0).cloned() else {
905                    unreachable!("named arg dispatch already matched Identifier token")
906                };
907                self.advance();
908                self.consume(&TokenKind::Colon, ":")?;
909                self.skip_newlines();
910                let value = self.parse_nested_expression("HITL argument")?;
911                (Some(raw), value)
912            } else {
913                (None, self.parse_nested_expression("HITL argument")?)
914            };
915            let arg_span = Span::merge(arg_start, self.prev_span());
916            args.push(HitlArg {
917                name,
918                value,
919                span: arg_span,
920            });
921            self.skip_newlines();
922            if self.check(&TokenKind::Comma) {
923                self.advance();
924                self.skip_newlines();
925            } else {
926                break;
927            }
928        }
929
930        self.skip_newlines();
931        self.consume(&TokenKind::RParen, ")")?;
932        Ok(spanned(
933            Node::HitlExpr { kind, args },
934            Span::merge(start, self.prev_span()),
935        ))
936    }
937
938    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
939        let start = self.current_span();
940        self.consume(&TokenKind::LBracket, "[")?;
941        let mut elements = Vec::new();
942        self.skip_newlines();
943
944        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
945            if self.check(&TokenKind::Dot) {
946                let saved_pos = self.pos;
947                self.advance();
948                if self.check(&TokenKind::Dot) {
949                    self.advance();
950                    self.consume(&TokenKind::Dot, ".")?;
951                    let spread_start = self.tokens[saved_pos].span;
952                    let expr = self.parse_nested_expression("list spread")?;
953                    elements.push(spanned(
954                        Node::Spread(Box::new(expr)),
955                        Span::merge(spread_start, self.prev_span()),
956                    ));
957                } else {
958                    self.pos = saved_pos;
959                    elements.push(self.parse_nested_expression("list element")?);
960                }
961            } else {
962                elements.push(self.parse_nested_expression("list element")?);
963            }
964            self.skip_newlines();
965            if self.check(&TokenKind::Comma) {
966                self.advance();
967                self.skip_newlines();
968            }
969        }
970
971        self.consume(&TokenKind::RBracket, "]")?;
972        Ok(spanned(
973            Node::ListLiteral(elements),
974            Span::merge(start, self.prev_span()),
975        ))
976    }
977
978    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
979        let start = self.current_span();
980        self.consume(&TokenKind::LBrace, "{")?;
981        self.skip_newlines();
982
983        if self.check(&TokenKind::RBrace) {
984            self.advance();
985            return Ok(spanned(
986                Node::DictLiteral(Vec::new()),
987                Span::merge(start, self.prev_span()),
988            ));
989        }
990
991        // Scan for `->` before the closing `}` to distinguish closure from dict.
992        let saved = self.pos;
993        if self.is_closure_lookahead() {
994            self.pos = saved;
995            return self.parse_closure_body(start);
996        }
997        self.pos = saved;
998        self.parse_dict_literal(start)
999    }
1000
1001    /// After seeing `Identifier {`, decide whether the brace block is a
1002    /// struct-construction field list rather than a control-flow block.
1003    /// Struct fields always start with `name:` / `"name":` or `}`.
1004    pub(super) fn is_struct_construct_lookahead(&self, struct_name: &str) -> bool {
1005        if !struct_name
1006            .chars()
1007            .next()
1008            .is_some_and(|ch| ch.is_uppercase())
1009        {
1010            return false;
1011        }
1012
1013        let mut offset = 1;
1014        while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
1015            offset += 1;
1016        }
1017
1018        match self.peek_kind_at(offset) {
1019            Some(TokenKind::RBrace) => true,
1020            Some(TokenKind::Identifier(_)) | Some(TokenKind::StringLiteral(_)) => {
1021                offset += 1;
1022                while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
1023                    offset += 1;
1024                }
1025                matches!(self.peek_kind_at(offset), Some(TokenKind::Colon))
1026            }
1027            _ => false,
1028        }
1029    }
1030
1031    /// Caller must save/restore `pos`; this advances while scanning.
1032    pub(super) fn is_closure_lookahead(&mut self) -> bool {
1033        let mut depth = 0;
1034        while !self.is_at_end() {
1035            if let Some(tok) = self.current() {
1036                match &tok.kind {
1037                    TokenKind::Arrow if depth == 0 => return true,
1038                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
1039                    TokenKind::RBrace if depth == 0 => return false,
1040                    TokenKind::RBrace => depth -= 1,
1041                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
1042                    _ => {}
1043                }
1044                self.advance();
1045            } else {
1046                return false;
1047            }
1048        }
1049        false
1050    }
1051
1052    /// Parse closure params and body (after opening { has been consumed).
1053    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
1054        let params = self.parse_typed_param_list_until_arrow()?;
1055        self.consume(&TokenKind::Arrow, "->")?;
1056        let body = self.parse_block()?;
1057        self.consume(&TokenKind::RBrace, "}")?;
1058        Ok(spanned(
1059            Node::Closure {
1060                params,
1061                body,
1062                fn_syntax: false,
1063            },
1064            Span::merge(start, self.prev_span()),
1065        ))
1066    }
1067
1068    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
1069    pub(super) fn parse_typed_param_list_until_arrow(
1070        &mut self,
1071    ) -> Result<Vec<TypedParam>, ParserError> {
1072        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
1073    }
1074
1075    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
1076        let entries = self.parse_dict_entries()?;
1077        Ok(spanned(
1078            Node::DictLiteral(entries),
1079            Span::merge(start, self.prev_span()),
1080        ))
1081    }
1082
1083    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
1084        let mut entries = Vec::new();
1085        self.skip_newlines();
1086
1087        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
1088            if self.check(&TokenKind::Dot) {
1089                let saved_pos = self.pos;
1090                self.advance();
1091                if self.check(&TokenKind::Dot) {
1092                    self.advance();
1093                    if self.check(&TokenKind::Dot) {
1094                        self.advance();
1095                        let spread_start = self.tokens[saved_pos].span;
1096                        let expr = self.parse_nested_expression("dict spread")?;
1097                        entries.push(DictEntry {
1098                            key: spanned(Node::NilLiteral, spread_start),
1099                            value: spanned(
1100                                Node::Spread(Box::new(expr)),
1101                                Span::merge(spread_start, self.prev_span()),
1102                            ),
1103                        });
1104                        self.skip_newlines();
1105                        if self.check(&TokenKind::Comma) {
1106                            self.advance();
1107                            self.skip_newlines();
1108                        }
1109                        continue;
1110                    }
1111                    self.pos = saved_pos;
1112                } else {
1113                    self.pos = saved_pos;
1114                }
1115            }
1116            let key = if self.check(&TokenKind::LBracket) {
1117                self.advance();
1118                let k = self.parse_nested_expression("computed dict key")?;
1119                self.consume(&TokenKind::RBracket, "]")?;
1120                k
1121            } else if matches!(
1122                self.current().map(|t| &t.kind),
1123                Some(TokenKind::StringLiteral(_))
1124            ) {
1125                let key_span = self.current_span();
1126                let name =
1127                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
1128                        s.clone()
1129                    } else {
1130                        unreachable!()
1131                    };
1132                self.advance();
1133                spanned(Node::StringLiteral(name), key_span)
1134            } else {
1135                let key_span = self.current_span();
1136                let name = self.consume_identifier_or_keyword("dict key")?;
1137                spanned(Node::StringLiteral(name), key_span)
1138            };
1139            self.consume(&TokenKind::Colon, ":")?;
1140            let value = self.parse_nested_expression("dict value")?;
1141            entries.push(DictEntry { key, value });
1142            self.skip_newlines();
1143            if self.check(&TokenKind::Comma) {
1144                self.advance();
1145                self.skip_newlines();
1146            }
1147        }
1148
1149        self.consume(&TokenKind::RBrace, "}")?;
1150        Ok(entries)
1151    }
1152
1153    /// Parse untyped parameter list (for pipelines, overrides).
1154    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
1155        let mut params = Vec::new();
1156        self.skip_newlines();
1157
1158        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1159            params.push(self.consume_identifier("parameter name")?);
1160            if self.check(&TokenKind::Comma) {
1161                self.advance();
1162                self.skip_newlines();
1163            }
1164        }
1165        Ok(params)
1166    }
1167
1168    /// Parse typed parameter list (for fn declarations).
1169    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
1170        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
1171    }
1172
1173    /// Shared implementation: parse typed params with optional defaults until
1174    /// a terminator token is reached.
1175    pub(super) fn parse_typed_params_until(
1176        &mut self,
1177        is_terminator: impl Fn(&TokenKind) -> bool,
1178    ) -> Result<Vec<TypedParam>, ParserError> {
1179        let mut params = Vec::new();
1180        let mut seen_default = false;
1181        self.skip_newlines();
1182
1183        while !self.is_at_end() {
1184            if let Some(tok) = self.current() {
1185                if is_terminator(&tok.kind) {
1186                    break;
1187                }
1188            } else {
1189                break;
1190            }
1191            let is_rest = if self.check(&TokenKind::Dot) {
1192                let p1 = self.pos + 1;
1193                let p2 = self.pos + 2;
1194                let is_ellipsis = p1 < self.tokens.len()
1195                    && p2 < self.tokens.len()
1196                    && self.tokens[p1].kind == TokenKind::Dot
1197                    && self.tokens[p2].kind == TokenKind::Dot;
1198                if is_ellipsis {
1199                    self.advance();
1200                    self.advance();
1201                    self.advance();
1202                    true
1203                } else {
1204                    false
1205                }
1206            } else {
1207                false
1208            };
1209            let name = self.consume_identifier("parameter name")?;
1210            let type_expr = self.try_parse_type_annotation()?;
1211            let default_value = if self.check(&TokenKind::Assign) {
1212                self.advance();
1213                seen_default = true;
1214                Some(Box::new(self.parse_nested_expression("parameter default")?))
1215            } else {
1216                if seen_default && !is_rest {
1217                    return Err(self.error(
1218                        "Required parameter cannot follow a parameter with a default value",
1219                    ));
1220                }
1221                None
1222            };
1223            if is_rest
1224                && !is_terminator(
1225                    &self
1226                        .current()
1227                        .map(|t| t.kind.clone())
1228                        .unwrap_or(TokenKind::Eof),
1229                )
1230            {
1231                return Err(self.error("Rest parameter must be the last parameter"));
1232            }
1233            params.push(TypedParam {
1234                name,
1235                type_expr,
1236                default_value,
1237                rest: is_rest,
1238            });
1239            if self.check(&TokenKind::Comma) {
1240                self.advance();
1241                self.skip_newlines();
1242            }
1243        }
1244        Ok(params)
1245    }
1246
1247    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
1248        let mut args = Vec::new();
1249        self.skip_newlines();
1250
1251        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1252            if self.check(&TokenKind::Dot) {
1253                let saved_pos = self.pos;
1254                self.advance();
1255                if self.check(&TokenKind::Dot) {
1256                    self.advance();
1257                    self.consume(&TokenKind::Dot, ".")?;
1258                    let spread_start = self.tokens[saved_pos].span;
1259                    let expr = self.parse_nested_expression("spread argument")?;
1260                    args.push(spanned(
1261                        Node::Spread(Box::new(expr)),
1262                        Span::merge(spread_start, self.prev_span()),
1263                    ));
1264                } else {
1265                    self.pos = saved_pos;
1266                    args.push(self.parse_nested_expression("argument")?);
1267                }
1268            } else {
1269                args.push(self.parse_nested_expression("argument")?);
1270            }
1271            self.skip_newlines();
1272            if self.check(&TokenKind::Comma) {
1273                self.advance();
1274                self.skip_newlines();
1275            }
1276        }
1277        Ok(args)
1278    }
1279}