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        let return_type = if self.check(&TokenKind::Arrow) {
843            self.advance();
844            Some(self.parse_nested_type_expr("closure return type")?)
845        } else {
846            None
847        };
848        self.consume(&TokenKind::LBrace, "{")?;
849        let body = self.parse_block()?;
850        self.consume(&TokenKind::RBrace, "}")?;
851        Ok(spanned(
852            Node::Closure {
853                params,
854                return_type,
855                body,
856                fn_syntax: true,
857            },
858            Span::merge(start, self.prev_span()),
859        ))
860    }
861
862    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
863        let start = self.current_span();
864        self.consume(&TokenKind::Spawn, "spawn")?;
865        self.consume(&TokenKind::LBrace, "{")?;
866        let body = self.parse_block()?;
867        self.consume(&TokenKind::RBrace, "}")?;
868        Ok(spanned(
869            Node::SpawnExpr { body },
870            Span::merge(start, self.prev_span()),
871        ))
872    }
873
874    /// Parse a first-class HITL primitive: one of `request_approval`,
875    /// `dual_control`, `ask_user`, `escalate_to`. The keyword has
876    /// already been peeked at; this method consumes it plus the
877    /// parenthesized argument list.
878    ///
879    /// Each argument is either positional (`expr`) or named
880    /// (`name: expr`). The grammar accepts the existing positional
881    /// invocation form so existing scripts and conformance tests
882    /// (e.g. `request_approval("deploy", {quorum: 2, ...})`) keep
883    /// working unchanged. Argument validation (required names,
884    /// duplicates, ordering) is performed by the typechecker.
885    pub(super) fn parse_hitl_expr(&mut self, kind: HitlKind) -> Result<SNode, ParserError> {
886        let start = self.current_span();
887        let kw_token = match kind {
888            HitlKind::RequestApproval => TokenKind::RequestApproval,
889            HitlKind::DualControl => TokenKind::DualControl,
890            HitlKind::AskUser => TokenKind::AskUser,
891            HitlKind::EscalateTo => TokenKind::EscalateTo,
892        };
893        self.consume(&kw_token, kind.as_keyword())?;
894        self.consume(&TokenKind::LParen, "(")?;
895        self.skip_newlines();
896
897        let mut args: Vec<HitlArg> = Vec::new();
898        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
899            let arg_start = self.current_span();
900            // Look ahead two tokens to detect `identifier ":"`. The
901            // identifier itself is parsed as part of the expression so
902            // we keep the dispatch simple: peek for `Identifier` then
903            // a `Colon` to identify a named argument.
904            // `peek_kind_at(0)` is the current token; `peek_kind_at(1)`
905            // is one ahead. A named-arg slot starts with `ident :`.
906            let is_named = matches!(
907                (self.peek_kind_at(0), self.peek_kind_at(1)),
908                (Some(TokenKind::Identifier(_)), Some(TokenKind::Colon))
909            );
910            let (name, value) = if is_named {
911                let Some(TokenKind::Identifier(raw)) = self.peek_kind_at(0).cloned() else {
912                    unreachable!("named arg dispatch already matched Identifier token")
913                };
914                self.advance();
915                self.consume(&TokenKind::Colon, ":")?;
916                self.skip_newlines();
917                let value = self.parse_nested_expression("HITL argument")?;
918                (Some(raw), value)
919            } else {
920                (None, self.parse_nested_expression("HITL argument")?)
921            };
922            let arg_span = Span::merge(arg_start, self.prev_span());
923            args.push(HitlArg {
924                name,
925                value,
926                span: arg_span,
927            });
928            self.skip_newlines();
929            if self.check(&TokenKind::Comma) {
930                self.advance();
931                self.skip_newlines();
932            } else {
933                break;
934            }
935        }
936
937        self.skip_newlines();
938        self.consume(&TokenKind::RParen, ")")?;
939        Ok(spanned(
940            Node::HitlExpr { kind, args },
941            Span::merge(start, self.prev_span()),
942        ))
943    }
944
945    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
946        let start = self.current_span();
947        self.consume(&TokenKind::LBracket, "[")?;
948        let mut elements = Vec::new();
949        self.skip_newlines();
950
951        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
952            if self.check(&TokenKind::Dot) {
953                let saved_pos = self.pos;
954                self.advance();
955                if self.check(&TokenKind::Dot) {
956                    self.advance();
957                    self.consume(&TokenKind::Dot, ".")?;
958                    let spread_start = self.tokens[saved_pos].span;
959                    let expr = self.parse_nested_expression("list spread")?;
960                    elements.push(spanned(
961                        Node::Spread(Box::new(expr)),
962                        Span::merge(spread_start, self.prev_span()),
963                    ));
964                } else {
965                    self.pos = saved_pos;
966                    elements.push(self.parse_nested_expression("list element")?);
967                }
968            } else {
969                elements.push(self.parse_nested_expression("list element")?);
970            }
971            self.skip_newlines();
972            if self.check(&TokenKind::Comma) {
973                self.advance();
974                self.skip_newlines();
975            }
976        }
977
978        self.consume(&TokenKind::RBracket, "]")?;
979        Ok(spanned(
980            Node::ListLiteral(elements),
981            Span::merge(start, self.prev_span()),
982        ))
983    }
984
985    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
986        let start = self.current_span();
987        self.consume(&TokenKind::LBrace, "{")?;
988        self.skip_newlines();
989
990        if self.check(&TokenKind::RBrace) {
991            self.advance();
992            return Ok(spanned(
993                Node::DictLiteral(Vec::new()),
994                Span::merge(start, self.prev_span()),
995            ));
996        }
997
998        // Scan for `->` before the closing `}` to distinguish closure from dict.
999        let saved = self.pos;
1000        if self.is_closure_lookahead() {
1001            self.pos = saved;
1002            return self.parse_closure_body(start);
1003        }
1004        self.pos = saved;
1005        self.parse_dict_literal(start)
1006    }
1007
1008    /// After seeing `Identifier {`, decide whether the brace block is a
1009    /// struct-construction field list rather than a control-flow block.
1010    /// Struct fields always start with `name:` / `"name":` or `}`.
1011    pub(super) fn is_struct_construct_lookahead(&self, struct_name: &str) -> bool {
1012        if !struct_name
1013            .chars()
1014            .next()
1015            .is_some_and(|ch| ch.is_uppercase())
1016        {
1017            return false;
1018        }
1019
1020        let mut offset = 1;
1021        while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
1022            offset += 1;
1023        }
1024
1025        match self.peek_kind_at(offset) {
1026            Some(TokenKind::RBrace) => true,
1027            Some(TokenKind::Identifier(_)) | Some(TokenKind::StringLiteral(_)) => {
1028                offset += 1;
1029                while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
1030                    offset += 1;
1031                }
1032                matches!(self.peek_kind_at(offset), Some(TokenKind::Colon))
1033            }
1034            _ => false,
1035        }
1036    }
1037
1038    /// Caller must save/restore `pos`; this advances while scanning.
1039    pub(super) fn is_closure_lookahead(&mut self) -> bool {
1040        let mut depth = 0;
1041        while !self.is_at_end() {
1042            if let Some(tok) = self.current() {
1043                match &tok.kind {
1044                    TokenKind::Arrow if depth == 0 => return true,
1045                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
1046                    TokenKind::RBrace if depth == 0 => return false,
1047                    TokenKind::RBrace => depth -= 1,
1048                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
1049                    _ => {}
1050                }
1051                self.advance();
1052            } else {
1053                return false;
1054            }
1055        }
1056        false
1057    }
1058
1059    /// Parse closure params and body (after opening { has been consumed).
1060    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
1061        let params = self.parse_typed_param_list_until_arrow()?;
1062        self.consume(&TokenKind::Arrow, "->")?;
1063        let body = self.parse_block()?;
1064        self.consume(&TokenKind::RBrace, "}")?;
1065        Ok(spanned(
1066            Node::Closure {
1067                params,
1068                return_type: None,
1069                body,
1070                fn_syntax: false,
1071            },
1072            Span::merge(start, self.prev_span()),
1073        ))
1074    }
1075
1076    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
1077    pub(super) fn parse_typed_param_list_until_arrow(
1078        &mut self,
1079    ) -> Result<Vec<TypedParam>, ParserError> {
1080        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
1081    }
1082
1083    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
1084        let entries = self.parse_dict_entries()?;
1085        Ok(spanned(
1086            Node::DictLiteral(entries),
1087            Span::merge(start, self.prev_span()),
1088        ))
1089    }
1090
1091    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
1092        let mut entries = Vec::new();
1093        self.skip_newlines();
1094
1095        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
1096            if self.check(&TokenKind::Dot) {
1097                let saved_pos = self.pos;
1098                self.advance();
1099                if self.check(&TokenKind::Dot) {
1100                    self.advance();
1101                    if self.check(&TokenKind::Dot) {
1102                        self.advance();
1103                        let spread_start = self.tokens[saved_pos].span;
1104                        let expr = self.parse_nested_expression("dict spread")?;
1105                        entries.push(DictEntry {
1106                            key: spanned(Node::NilLiteral, spread_start),
1107                            value: spanned(
1108                                Node::Spread(Box::new(expr)),
1109                                Span::merge(spread_start, self.prev_span()),
1110                            ),
1111                        });
1112                        self.skip_newlines();
1113                        if self.check(&TokenKind::Comma) {
1114                            self.advance();
1115                            self.skip_newlines();
1116                        }
1117                        continue;
1118                    }
1119                    self.pos = saved_pos;
1120                } else {
1121                    self.pos = saved_pos;
1122                }
1123            }
1124            let key = if self.check(&TokenKind::LBracket) {
1125                self.advance();
1126                let k = self.parse_nested_expression("computed dict key")?;
1127                self.consume(&TokenKind::RBracket, "]")?;
1128                k
1129            } else if matches!(
1130                self.current().map(|t| &t.kind),
1131                Some(TokenKind::StringLiteral(_))
1132            ) {
1133                let key_span = self.current_span();
1134                let name =
1135                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
1136                        s.clone()
1137                    } else {
1138                        unreachable!()
1139                    };
1140                self.advance();
1141                spanned(Node::StringLiteral(name), key_span)
1142            } else {
1143                let key_span = self.current_span();
1144                let name = self.consume_identifier_or_keyword("dict key")?;
1145                spanned(Node::StringLiteral(name), key_span)
1146            };
1147            self.consume(&TokenKind::Colon, ":")?;
1148            let value = self.parse_nested_expression("dict value")?;
1149            entries.push(DictEntry { key, value });
1150            self.skip_newlines();
1151            if self.check(&TokenKind::Comma) {
1152                self.advance();
1153                self.skip_newlines();
1154            }
1155        }
1156
1157        self.consume(&TokenKind::RBrace, "}")?;
1158        Ok(entries)
1159    }
1160
1161    /// Parse untyped parameter list (for pipelines, overrides).
1162    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
1163        let mut params = Vec::new();
1164        self.skip_newlines();
1165
1166        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1167            params.push(self.consume_identifier("parameter name")?);
1168            if self.check(&TokenKind::Comma) {
1169                self.advance();
1170                self.skip_newlines();
1171            }
1172        }
1173        Ok(params)
1174    }
1175
1176    /// Parse typed parameter list (for fn declarations).
1177    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
1178        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
1179    }
1180
1181    /// Shared implementation: parse typed params with optional defaults until
1182    /// a terminator token is reached.
1183    pub(super) fn parse_typed_params_until(
1184        &mut self,
1185        is_terminator: impl Fn(&TokenKind) -> bool,
1186    ) -> Result<Vec<TypedParam>, ParserError> {
1187        let mut params = Vec::new();
1188        let mut seen_default = false;
1189        self.skip_newlines();
1190
1191        while !self.is_at_end() {
1192            if let Some(tok) = self.current() {
1193                if is_terminator(&tok.kind) {
1194                    break;
1195                }
1196            } else {
1197                break;
1198            }
1199            let is_rest = if self.check(&TokenKind::Dot) {
1200                let p1 = self.pos + 1;
1201                let p2 = self.pos + 2;
1202                let is_ellipsis = p1 < self.tokens.len()
1203                    && p2 < self.tokens.len()
1204                    && self.tokens[p1].kind == TokenKind::Dot
1205                    && self.tokens[p2].kind == TokenKind::Dot;
1206                if is_ellipsis {
1207                    self.advance();
1208                    self.advance();
1209                    self.advance();
1210                    true
1211                } else {
1212                    false
1213                }
1214            } else {
1215                false
1216            };
1217            let name = self.consume_identifier("parameter name")?;
1218            let type_expr = self.try_parse_type_annotation()?;
1219            let default_value = if self.check(&TokenKind::Assign) {
1220                self.advance();
1221                seen_default = true;
1222                Some(Box::new(self.parse_nested_expression("parameter default")?))
1223            } else {
1224                if seen_default && !is_rest {
1225                    return Err(self.error(
1226                        "Required parameter cannot follow a parameter with a default value",
1227                    ));
1228                }
1229                None
1230            };
1231            if is_rest
1232                && !is_terminator(
1233                    &self
1234                        .current()
1235                        .map(|t| t.kind.clone())
1236                        .unwrap_or(TokenKind::Eof),
1237                )
1238            {
1239                return Err(self.error("Rest parameter must be the last parameter"));
1240            }
1241            params.push(TypedParam {
1242                name,
1243                type_expr,
1244                default_value,
1245                rest: is_rest,
1246            });
1247            if self.check(&TokenKind::Comma) {
1248                self.advance();
1249                self.skip_newlines();
1250            }
1251        }
1252        Ok(params)
1253    }
1254
1255    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
1256        let mut args = Vec::new();
1257        self.skip_newlines();
1258
1259        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1260            if self.check(&TokenKind::Dot) {
1261                let saved_pos = self.pos;
1262                self.advance();
1263                if self.check(&TokenKind::Dot) {
1264                    self.advance();
1265                    self.consume(&TokenKind::Dot, ".")?;
1266                    let spread_start = self.tokens[saved_pos].span;
1267                    let expr = self.parse_nested_expression("spread argument")?;
1268                    args.push(spanned(
1269                        Node::Spread(Box::new(expr)),
1270                        Span::merge(spread_start, self.prev_span()),
1271                    ));
1272                } else {
1273                    self.pos = saved_pos;
1274                    args.push(self.parse_nested_expression("argument")?);
1275                }
1276            } else {
1277                args.push(self.parse_nested_expression("argument")?);
1278            }
1279            self.skip_newlines();
1280            if self.check(&TokenKind::Comma) {
1281                self.advance();
1282                self.skip_newlines();
1283            }
1284        }
1285        Ok(args)
1286    }
1287}