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                if optional && self.check(&TokenKind::LBracket) {
378                    self.advance();
379                    let index = self.parse_nested_expression("optional subscript index")?;
380                    self.consume(&TokenKind::RBracket, "]")?;
381                    expr = spanned(
382                        Node::OptionalSubscriptAccess {
383                            object: Box::new(expr),
384                            index: Box::new(index),
385                        },
386                        Span::merge(start, self.prev_span()),
387                    );
388                    continue;
389                }
390                let member = self.consume_identifier_or_keyword("member name")?;
391                if self.check(&TokenKind::LParen) {
392                    self.advance();
393                    let args = self.parse_arg_list()?;
394                    self.consume(&TokenKind::RParen, ")")?;
395                    if optional {
396                        expr = spanned(
397                            Node::OptionalMethodCall {
398                                object: Box::new(expr),
399                                method: member,
400                                args,
401                            },
402                            Span::merge(start, self.prev_span()),
403                        );
404                    } else {
405                        expr = spanned(
406                            Node::MethodCall {
407                                object: Box::new(expr),
408                                method: member,
409                                args,
410                            },
411                            Span::merge(start, self.prev_span()),
412                        );
413                    }
414                } else if optional {
415                    expr = spanned(
416                        Node::OptionalPropertyAccess {
417                            object: Box::new(expr),
418                            property: member,
419                        },
420                        Span::merge(start, self.prev_span()),
421                    );
422                } else {
423                    expr = spanned(
424                        Node::PropertyAccess {
425                            object: Box::new(expr),
426                            property: member,
427                        },
428                        Span::merge(start, self.prev_span()),
429                    );
430                }
431            } else if self.check(&TokenKind::LBracket) {
432                let start = expr.span;
433                self.advance();
434
435                // Disambiguate `[:end]` / `[start:end]` / `[start:]` slices from
436                // `[index]` subscript access.
437                if self.check(&TokenKind::Colon) {
438                    self.advance();
439                    let end_expr = if self.check(&TokenKind::RBracket) {
440                        None
441                    } else {
442                        Some(Box::new(self.parse_nested_expression("slice bound")?))
443                    };
444                    self.consume(&TokenKind::RBracket, "]")?;
445                    expr = spanned(
446                        Node::SliceAccess {
447                            object: Box::new(expr),
448                            start: None,
449                            end: end_expr,
450                        },
451                        Span::merge(start, self.prev_span()),
452                    );
453                } else {
454                    let index = self.parse_nested_expression("subscript index")?;
455                    if self.check(&TokenKind::Colon) {
456                        self.advance();
457                        let end_expr = if self.check(&TokenKind::RBracket) {
458                            None
459                        } else {
460                            Some(Box::new(self.parse_nested_expression("slice bound")?))
461                        };
462                        self.consume(&TokenKind::RBracket, "]")?;
463                        expr = spanned(
464                            Node::SliceAccess {
465                                object: Box::new(expr),
466                                start: Some(Box::new(index)),
467                                end: end_expr,
468                            },
469                            Span::merge(start, self.prev_span()),
470                        );
471                    } else {
472                        self.consume(&TokenKind::RBracket, "]")?;
473                        expr = spanned(
474                            Node::SubscriptAccess {
475                                object: Box::new(expr),
476                                index: Box::new(index),
477                            },
478                            Span::merge(start, self.prev_span()),
479                        );
480                    }
481                }
482            } else if self.check(&TokenKind::LBrace) {
483                let struct_name = match &expr.node {
484                    Node::Identifier(name) if self.is_struct_construct_lookahead(name) => {
485                        Some(name.clone())
486                    }
487                    _ => None,
488                };
489                let Some(struct_name) = struct_name else {
490                    break;
491                };
492                let start = expr.span;
493                self.advance();
494                let dict = self.parse_dict_literal(start)?;
495                let fields = match dict.node {
496                    Node::DictLiteral(fields) => fields,
497                    _ => unreachable!("dict parser must return a dict literal"),
498                };
499                expr = spanned(
500                    Node::StructConstruct {
501                        struct_name,
502                        fields,
503                    },
504                    dict.span,
505                );
506            } else if self.check(&TokenKind::Lt) && matches!(expr.node, Node::Identifier(_)) {
507                let saved_pos = self.pos;
508                let start = expr.span;
509                self.advance();
510                let parsed_type_args = self.parse_type_arg_list();
511                if let Ok(type_args) = parsed_type_args {
512                    if self.check(&TokenKind::LParen) {
513                        self.advance();
514                        let args = self.parse_arg_list()?;
515                        self.consume(&TokenKind::RParen, ")")?;
516                        if let Node::Identifier(name) = expr.node {
517                            expr = spanned(
518                                Node::FunctionCall {
519                                    name,
520                                    type_args,
521                                    args,
522                                },
523                                Span::merge(start, self.prev_span()),
524                            );
525                        }
526                    } else {
527                        self.pos = saved_pos;
528                        break;
529                    }
530                } else {
531                    self.pos = saved_pos;
532                    break;
533                }
534            } else if self.check(&TokenKind::LParen) && matches!(expr.node, Node::Identifier(_)) {
535                let start = expr.span;
536                self.advance();
537                let args = self.parse_arg_list()?;
538                self.consume(&TokenKind::RParen, ")")?;
539                if let Node::Identifier(name) = expr.node {
540                    expr = spanned(
541                        Node::FunctionCall {
542                            name,
543                            type_args: Vec::new(),
544                            args,
545                        },
546                        Span::merge(start, self.prev_span()),
547                    );
548                }
549            } else if self.check(&TokenKind::Question) {
550                // Disambiguate `?[index]` (legacy optional subscript), `expr?`
551                // (postfix try), and `expr ? a : b` (ternary).
552                if self.question_starts_ternary_branch() {
553                    break;
554                }
555                if matches!(self.peek_kind_at(1), Some(TokenKind::LBracket)) {
556                    let start = expr.span;
557                    self.advance(); // consume ?
558                    self.advance(); // consume [
559                    let index = self.parse_nested_expression("optional subscript index")?;
560                    self.consume(&TokenKind::RBracket, "]")?;
561                    expr = spanned(
562                        Node::OptionalSubscriptAccess {
563                            object: Box::new(expr),
564                            index: Box::new(index),
565                        },
566                        Span::merge(start, self.prev_span()),
567                    );
568                    continue;
569                }
570                let start = expr.span;
571                self.advance();
572                expr = spanned(
573                    Node::TryOperator {
574                        operand: Box::new(expr),
575                    },
576                    Span::merge(start, self.prev_span()),
577                );
578            } else {
579                break;
580            }
581        }
582
583        Ok(expr)
584    }
585
586    fn question_starts_ternary_branch(&self) -> bool {
587        // Look at the first non-newline token after `?`. A ternary may wrap
588        // its true-branch onto a new line (`cond ?\n value : other`), so a
589        // newline immediately after `?` must not cause us to misclassify this
590        // as a postfix-`?`.
591        let next = self
592            .tokens
593            .iter()
594            .skip(self.pos + 1)
595            .find(|t| t.kind != TokenKind::Newline)
596            .map(|t| &t.kind);
597        next.is_some_and(Self::token_starts_ternary_branch)
598            && self.question_has_top_level_ternary_colon()
599    }
600
601    fn token_starts_ternary_branch(kind: &TokenKind) -> bool {
602        matches!(
603            kind,
604            TokenKind::Identifier(_)
605                | TokenKind::IntLiteral(_)
606                | TokenKind::FloatLiteral(_)
607                | TokenKind::StringLiteral(_)
608                | TokenKind::RawStringLiteral(_)
609                | TokenKind::InterpolatedString(_)
610                | TokenKind::True
611                | TokenKind::False
612                | TokenKind::Nil
613                | TokenKind::LParen
614                | TokenKind::LBracket
615                | TokenKind::LBrace
616                | TokenKind::Not
617                | TokenKind::Minus
618                | TokenKind::Fn
619                | TokenKind::If
620                | TokenKind::Match
621                | TokenKind::Try
622                | TokenKind::Spawn
623                | TokenKind::Parallel
624                | TokenKind::Retry
625                | TokenKind::Deadline
626                | TokenKind::RequestApproval
627                | TokenKind::DualControl
628                | TokenKind::AskUser
629                | TokenKind::EscalateTo
630                | TokenKind::DurationLiteral(_)
631        )
632    }
633
634    fn question_has_top_level_ternary_colon(&self) -> bool {
635        let mut delimiter_depth = 0usize;
636        // True when the most recent significant top-level token was `?` or
637        // `:` — i.e. we're scanning for the start of a branch and a newline
638        // here is just a wrap, not an end-of-ternary.
639        let mut at_branch_start = true;
640        for (pos, token) in self.tokens.iter().enumerate().skip(self.pos + 1) {
641            if delimiter_depth == 0 {
642                match token.kind {
643                    TokenKind::Colon => return true,
644                    TokenKind::Newline => {
645                        if at_branch_start {
646                            // `?` (or `:`) was the last significant token; this
647                            // newline simply wraps the branch onto a new line.
648                            continue;
649                        }
650                        if self.next_non_newline_continues_ternary_branch(pos + 1) {
651                            continue;
652                        }
653                        return false;
654                    }
655                    TokenKind::RParen
656                    | TokenKind::RBracket
657                    | TokenKind::RBrace
658                    | TokenKind::Eof => {
659                        return false;
660                    }
661                    _ => {
662                        at_branch_start = false;
663                    }
664                }
665            }
666
667            match token.kind {
668                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => {
669                    delimiter_depth += 1;
670                }
671                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
672                    delimiter_depth = delimiter_depth.saturating_sub(1);
673                }
674                TokenKind::Eof => return false,
675                _ => {}
676            }
677        }
678        false
679    }
680
681    fn next_non_newline_continues_ternary_branch(&self, start_pos: usize) -> bool {
682        let Some(kind) = self
683            .tokens
684            .iter()
685            .skip(start_pos)
686            .find(|token| token.kind != TokenKind::Newline)
687            .map(|token| &token.kind)
688        else {
689            return false;
690        };
691        matches!(
692            kind,
693            TokenKind::Colon
694                | TokenKind::Plus
695                | TokenKind::Star
696                | TokenKind::Slash
697                | TokenKind::Percent
698                | TokenKind::Pow
699                | TokenKind::And
700                | TokenKind::Or
701                | TokenKind::Eq
702                | TokenKind::Neq
703                | TokenKind::Lt
704                | TokenKind::Gt
705                | TokenKind::Lte
706                | TokenKind::Gte
707                | TokenKind::NilCoal
708                | TokenKind::Pipe
709                | TokenKind::Dot
710                | TokenKind::QuestionDot
711        )
712    }
713
714    pub(super) fn parse_primary(&mut self) -> Result<SNode, ParserError> {
715        let tok = self.current().ok_or_else(|| ParserError::UnexpectedEof {
716            expected: "expression".into(),
717            span: self.prev_span(),
718        })?;
719        let start = self.current_span();
720
721        match &tok.kind {
722            TokenKind::StringLiteral(s) => {
723                let s = s.clone();
724                self.advance();
725                Ok(spanned(
726                    Node::StringLiteral(s),
727                    Span::merge(start, self.prev_span()),
728                ))
729            }
730            TokenKind::RawStringLiteral(s) => {
731                let s = s.clone();
732                self.advance();
733                Ok(spanned(
734                    Node::RawStringLiteral(s),
735                    Span::merge(start, self.prev_span()),
736                ))
737            }
738            TokenKind::InterpolatedString(segments) => {
739                let segments = segments.clone();
740                self.advance();
741                Ok(spanned(
742                    Node::InterpolatedString(segments),
743                    Span::merge(start, self.prev_span()),
744                ))
745            }
746            TokenKind::IntLiteral(n) => {
747                let n = *n;
748                self.advance();
749                Ok(spanned(
750                    Node::IntLiteral(n),
751                    Span::merge(start, self.prev_span()),
752                ))
753            }
754            TokenKind::FloatLiteral(n) => {
755                let n = *n;
756                self.advance();
757                Ok(spanned(
758                    Node::FloatLiteral(n),
759                    Span::merge(start, self.prev_span()),
760                ))
761            }
762            TokenKind::True => {
763                self.advance();
764                Ok(spanned(
765                    Node::BoolLiteral(true),
766                    Span::merge(start, self.prev_span()),
767                ))
768            }
769            TokenKind::False => {
770                self.advance();
771                Ok(spanned(
772                    Node::BoolLiteral(false),
773                    Span::merge(start, self.prev_span()),
774                ))
775            }
776            TokenKind::Nil => {
777                self.advance();
778                Ok(spanned(
779                    Node::NilLiteral,
780                    Span::merge(start, self.prev_span()),
781                ))
782            }
783            TokenKind::Identifier(name)
784                if name == "cost_route" && self.peek_kind() == Some(&TokenKind::LBrace) =>
785            {
786                self.parse_cost_route()
787            }
788            TokenKind::Identifier(name) => {
789                let name = name.clone();
790                self.advance();
791                Ok(spanned(
792                    Node::Identifier(name),
793                    Span::merge(start, self.prev_span()),
794                ))
795            }
796            TokenKind::LParen => {
797                self.advance();
798                let expr = self.with_nesting("parenthesized expression", |parser| {
799                    let expr = parser.parse_expression()?;
800                    parser.consume(&TokenKind::RParen, ")")?;
801                    Ok(expr)
802                })?;
803                Ok(expr)
804            }
805            TokenKind::LBracket => self.parse_list_literal(),
806            TokenKind::LBrace => self.parse_dict_or_closure(),
807            TokenKind::Parallel => self.parse_parallel(),
808            TokenKind::Retry => self.parse_retry(),
809            TokenKind::If => self.parse_if_else(),
810            TokenKind::Spawn => self.parse_spawn_expr(),
811            TokenKind::RequestApproval => self.parse_hitl_expr(HitlKind::RequestApproval),
812            TokenKind::DualControl => self.parse_hitl_expr(HitlKind::DualControl),
813            TokenKind::AskUser => self.parse_hitl_expr(HitlKind::AskUser),
814            TokenKind::EscalateTo => self.parse_hitl_expr(HitlKind::EscalateTo),
815            TokenKind::DurationLiteral(ms) => {
816                let ms = *ms;
817                self.advance();
818                Ok(spanned(
819                    Node::DurationLiteral(ms),
820                    Span::merge(start, self.prev_span()),
821                ))
822            }
823            TokenKind::Deadline => self.parse_deadline(),
824            TokenKind::Try => self.parse_try_catch(),
825            TokenKind::Match => self.parse_match(),
826            TokenKind::Fn => self.parse_fn_expr(),
827            // Heredoc `<<TAG ... TAG` is only valid inside LLM tool-call JSON;
828            // in source-position expressions, redirect authors to triple-quoted strings.
829            TokenKind::Lt
830                if matches!(self.peek_kind(), Some(&TokenKind::Lt))
831                    && matches!(self.peek_kind_at(2), Some(TokenKind::Identifier(_))) =>
832            {
833                Err(ParserError::Unexpected {
834                    got: "`<<` heredoc-like syntax".to_string(),
835                    expected: "an expression — heredocs are only valid \
836                               inside LLM tool-call argument JSON; \
837                               for multiline strings in source code use \
838                               triple-quoted `\"\"\"...\"\"\"`"
839                        .to_string(),
840                    span: start,
841                })
842            }
843            _ => Err(self.error("expression")),
844        }
845    }
846
847    /// Anonymous function `fn(params) { body }`. Sets `fn_syntax: true` on the
848    /// Closure so the formatter can round-trip the original syntax.
849    pub(super) fn parse_fn_expr(&mut self) -> Result<SNode, ParserError> {
850        let start = self.current_span();
851        self.consume(&TokenKind::Fn, "fn")?;
852        self.consume(&TokenKind::LParen, "(")?;
853        let params = self.parse_typed_param_list()?;
854        self.consume(&TokenKind::RParen, ")")?;
855        let return_type = if self.check(&TokenKind::Arrow) {
856            self.advance();
857            Some(self.parse_nested_type_expr("closure return type")?)
858        } else {
859            None
860        };
861        self.consume(&TokenKind::LBrace, "{")?;
862        let body = self.parse_block()?;
863        self.consume(&TokenKind::RBrace, "}")?;
864        Ok(spanned(
865            Node::Closure {
866                params,
867                return_type,
868                body,
869                fn_syntax: true,
870            },
871            Span::merge(start, self.prev_span()),
872        ))
873    }
874
875    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
876        let start = self.current_span();
877        self.consume(&TokenKind::Spawn, "spawn")?;
878        self.consume(&TokenKind::LBrace, "{")?;
879        let body = self.parse_block()?;
880        self.consume(&TokenKind::RBrace, "}")?;
881        Ok(spanned(
882            Node::SpawnExpr { body },
883            Span::merge(start, self.prev_span()),
884        ))
885    }
886
887    /// Parse a first-class HITL primitive: one of `request_approval`,
888    /// `dual_control`, `ask_user`, `escalate_to`. The keyword has
889    /// already been peeked at; this method consumes it plus the
890    /// parenthesized argument list.
891    ///
892    /// Each argument is either positional (`expr`) or named
893    /// (`name: expr`). The grammar accepts the existing positional
894    /// invocation form so existing scripts and conformance tests
895    /// (e.g. `request_approval("deploy", {quorum: 2, ...})`) keep
896    /// working unchanged. Argument validation (required names,
897    /// duplicates, ordering) is performed by the typechecker.
898    pub(super) fn parse_hitl_expr(&mut self, kind: HitlKind) -> Result<SNode, ParserError> {
899        let start = self.current_span();
900        let kw_token = match kind {
901            HitlKind::RequestApproval => TokenKind::RequestApproval,
902            HitlKind::DualControl => TokenKind::DualControl,
903            HitlKind::AskUser => TokenKind::AskUser,
904            HitlKind::EscalateTo => TokenKind::EscalateTo,
905        };
906        self.consume(&kw_token, kind.as_keyword())?;
907        self.consume(&TokenKind::LParen, "(")?;
908        self.skip_newlines();
909
910        let mut args: Vec<HitlArg> = Vec::new();
911        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
912            let arg_start = self.current_span();
913            // Look ahead two tokens to detect `identifier ":"`. The
914            // identifier itself is parsed as part of the expression so
915            // we keep the dispatch simple: peek for `Identifier` then
916            // a `Colon` to identify a named argument.
917            // `peek_kind_at(0)` is the current token; `peek_kind_at(1)`
918            // is one ahead. A named-arg slot starts with `ident :`.
919            let is_named = matches!(
920                (self.peek_kind_at(0), self.peek_kind_at(1)),
921                (Some(TokenKind::Identifier(_)), Some(TokenKind::Colon))
922            );
923            let (name, value) = if is_named {
924                let Some(TokenKind::Identifier(raw)) = self.peek_kind_at(0).cloned() else {
925                    unreachable!("named arg dispatch already matched Identifier token")
926                };
927                self.advance();
928                self.consume(&TokenKind::Colon, ":")?;
929                self.skip_newlines();
930                let value = self.parse_nested_expression("HITL argument")?;
931                (Some(raw), value)
932            } else {
933                (None, self.parse_nested_expression("HITL argument")?)
934            };
935            let arg_span = Span::merge(arg_start, self.prev_span());
936            args.push(HitlArg {
937                name,
938                value,
939                span: arg_span,
940            });
941            self.skip_newlines();
942            if self.check(&TokenKind::Comma) {
943                self.advance();
944                self.skip_newlines();
945            } else {
946                break;
947            }
948        }
949
950        self.skip_newlines();
951        self.consume(&TokenKind::RParen, ")")?;
952        Ok(spanned(
953            Node::HitlExpr { kind, args },
954            Span::merge(start, self.prev_span()),
955        ))
956    }
957
958    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
959        let start = self.current_span();
960        self.consume(&TokenKind::LBracket, "[")?;
961        let mut elements = Vec::new();
962        self.skip_newlines();
963
964        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
965            if self.check(&TokenKind::Dot) {
966                let saved_pos = self.pos;
967                self.advance();
968                if self.check(&TokenKind::Dot) {
969                    self.advance();
970                    self.consume(&TokenKind::Dot, ".")?;
971                    let spread_start = self.tokens[saved_pos].span;
972                    let expr = self.parse_nested_expression("list spread")?;
973                    elements.push(spanned(
974                        Node::Spread(Box::new(expr)),
975                        Span::merge(spread_start, self.prev_span()),
976                    ));
977                } else {
978                    self.pos = saved_pos;
979                    elements.push(self.parse_nested_expression("list element")?);
980                }
981            } else {
982                elements.push(self.parse_nested_expression("list element")?);
983            }
984            self.skip_newlines();
985            if self.check(&TokenKind::Comma) {
986                self.advance();
987                self.skip_newlines();
988            }
989        }
990
991        self.consume(&TokenKind::RBracket, "]")?;
992        Ok(spanned(
993            Node::ListLiteral(elements),
994            Span::merge(start, self.prev_span()),
995        ))
996    }
997
998    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
999        let start = self.current_span();
1000        self.consume(&TokenKind::LBrace, "{")?;
1001        self.skip_newlines();
1002
1003        if self.check(&TokenKind::RBrace) {
1004            self.advance();
1005            return Ok(spanned(
1006                Node::DictLiteral(Vec::new()),
1007                Span::merge(start, self.prev_span()),
1008            ));
1009        }
1010
1011        // Scan for `->` before the closing `}` to distinguish closure from dict.
1012        let saved = self.pos;
1013        if self.is_closure_lookahead() {
1014            self.pos = saved;
1015            return self.parse_closure_body(start);
1016        }
1017        self.pos = saved;
1018        self.parse_dict_literal(start)
1019    }
1020
1021    /// After seeing `Identifier {`, decide whether the brace block is a
1022    /// struct-construction field list rather than a control-flow block.
1023    /// Struct fields always start with `name:` / `"name":` or `}`.
1024    pub(super) fn is_struct_construct_lookahead(&self, struct_name: &str) -> bool {
1025        if !struct_name
1026            .chars()
1027            .next()
1028            .is_some_and(|ch| ch.is_uppercase())
1029        {
1030            return false;
1031        }
1032
1033        let mut offset = 1;
1034        while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
1035            offset += 1;
1036        }
1037
1038        match self.peek_kind_at(offset) {
1039            Some(TokenKind::RBrace) => true,
1040            Some(TokenKind::Identifier(_)) | Some(TokenKind::StringLiteral(_)) => {
1041                offset += 1;
1042                while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
1043                    offset += 1;
1044                }
1045                matches!(self.peek_kind_at(offset), Some(TokenKind::Colon))
1046            }
1047            _ => false,
1048        }
1049    }
1050
1051    /// Caller must save/restore `pos`; this advances while scanning.
1052    pub(super) fn is_closure_lookahead(&mut self) -> bool {
1053        let mut depth = 0;
1054        while !self.is_at_end() {
1055            if let Some(tok) = self.current() {
1056                match &tok.kind {
1057                    TokenKind::Arrow if depth == 0 => return true,
1058                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
1059                    TokenKind::RBrace if depth == 0 => return false,
1060                    TokenKind::RBrace => depth -= 1,
1061                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
1062                    _ => {}
1063                }
1064                self.advance();
1065            } else {
1066                return false;
1067            }
1068        }
1069        false
1070    }
1071
1072    /// Parse closure params and body (after opening { has been consumed).
1073    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
1074        let params = self.parse_typed_param_list_until_arrow()?;
1075        self.consume(&TokenKind::Arrow, "->")?;
1076        let body = self.parse_block()?;
1077        self.consume(&TokenKind::RBrace, "}")?;
1078        Ok(spanned(
1079            Node::Closure {
1080                params,
1081                return_type: None,
1082                body,
1083                fn_syntax: false,
1084            },
1085            Span::merge(start, self.prev_span()),
1086        ))
1087    }
1088
1089    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
1090    pub(super) fn parse_typed_param_list_until_arrow(
1091        &mut self,
1092    ) -> Result<Vec<TypedParam>, ParserError> {
1093        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
1094    }
1095
1096    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
1097        let entries = self.parse_dict_entries()?;
1098        Ok(spanned(
1099            Node::DictLiteral(entries),
1100            Span::merge(start, self.prev_span()),
1101        ))
1102    }
1103
1104    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
1105        let mut entries = Vec::new();
1106        self.skip_newlines();
1107
1108        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
1109            if self.check(&TokenKind::Dot) {
1110                let saved_pos = self.pos;
1111                self.advance();
1112                if self.check(&TokenKind::Dot) {
1113                    self.advance();
1114                    if self.check(&TokenKind::Dot) {
1115                        self.advance();
1116                        let spread_start = self.tokens[saved_pos].span;
1117                        let expr = self.parse_nested_expression("dict spread")?;
1118                        entries.push(DictEntry {
1119                            key: spanned(Node::NilLiteral, spread_start),
1120                            value: spanned(
1121                                Node::Spread(Box::new(expr)),
1122                                Span::merge(spread_start, self.prev_span()),
1123                            ),
1124                        });
1125                        self.skip_newlines();
1126                        if self.check(&TokenKind::Comma) {
1127                            self.advance();
1128                            self.skip_newlines();
1129                        }
1130                        continue;
1131                    }
1132                    self.pos = saved_pos;
1133                } else {
1134                    self.pos = saved_pos;
1135                }
1136            }
1137            let key = if self.check(&TokenKind::LBracket) {
1138                self.advance();
1139                let k = self.parse_nested_expression("computed dict key")?;
1140                self.consume(&TokenKind::RBracket, "]")?;
1141                k
1142            } else if matches!(
1143                self.current().map(|t| &t.kind),
1144                Some(TokenKind::StringLiteral(_))
1145            ) {
1146                let key_span = self.current_span();
1147                let name =
1148                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
1149                        s.clone()
1150                    } else {
1151                        unreachable!()
1152                    };
1153                self.advance();
1154                spanned(Node::StringLiteral(name), key_span)
1155            } else {
1156                let key_span = self.current_span();
1157                let name = self.consume_identifier_or_keyword("dict key")?;
1158                spanned(Node::StringLiteral(name), key_span)
1159            };
1160            self.consume(&TokenKind::Colon, ":")?;
1161            let value = self.parse_nested_expression("dict value")?;
1162            entries.push(DictEntry { key, value });
1163            self.skip_newlines();
1164            if self.check(&TokenKind::Comma) {
1165                self.advance();
1166                self.skip_newlines();
1167            }
1168        }
1169
1170        self.consume(&TokenKind::RBrace, "}")?;
1171        Ok(entries)
1172    }
1173
1174    /// Parse untyped parameter list (for pipelines, overrides).
1175    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
1176        let mut params = Vec::new();
1177        self.skip_newlines();
1178
1179        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1180            params.push(self.consume_identifier("parameter name")?);
1181            if self.check(&TokenKind::Comma) {
1182                self.advance();
1183                self.skip_newlines();
1184            }
1185        }
1186        Ok(params)
1187    }
1188
1189    /// Parse typed parameter list (for fn declarations).
1190    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
1191        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
1192    }
1193
1194    /// Shared implementation: parse typed params with optional defaults until
1195    /// a terminator token is reached.
1196    pub(super) fn parse_typed_params_until(
1197        &mut self,
1198        is_terminator: impl Fn(&TokenKind) -> bool,
1199    ) -> Result<Vec<TypedParam>, ParserError> {
1200        let mut params = Vec::new();
1201        let mut seen_default = false;
1202        self.skip_newlines();
1203
1204        while !self.is_at_end() {
1205            if let Some(tok) = self.current() {
1206                if is_terminator(&tok.kind) {
1207                    break;
1208                }
1209            } else {
1210                break;
1211            }
1212            let is_rest = if self.check(&TokenKind::Dot) {
1213                let p1 = self.pos + 1;
1214                let p2 = self.pos + 2;
1215                let is_ellipsis = p1 < self.tokens.len()
1216                    && p2 < self.tokens.len()
1217                    && self.tokens[p1].kind == TokenKind::Dot
1218                    && self.tokens[p2].kind == TokenKind::Dot;
1219                if is_ellipsis {
1220                    self.advance();
1221                    self.advance();
1222                    self.advance();
1223                    true
1224                } else {
1225                    false
1226                }
1227            } else {
1228                false
1229            };
1230            let name = self.consume_identifier("parameter name")?;
1231            let type_expr = self.try_parse_type_annotation()?;
1232            let default_value = if self.check(&TokenKind::Assign) {
1233                self.advance();
1234                seen_default = true;
1235                Some(Box::new(self.parse_nested_expression("parameter default")?))
1236            } else {
1237                if seen_default && !is_rest {
1238                    return Err(self.error(
1239                        "Required parameter cannot follow a parameter with a default value",
1240                    ));
1241                }
1242                None
1243            };
1244            if is_rest
1245                && !is_terminator(
1246                    &self
1247                        .current()
1248                        .map(|t| t.kind.clone())
1249                        .unwrap_or(TokenKind::Eof),
1250                )
1251            {
1252                return Err(self.error("Rest parameter must be the last parameter"));
1253            }
1254            params.push(TypedParam {
1255                name,
1256                type_expr,
1257                default_value,
1258                rest: is_rest,
1259            });
1260            if self.check(&TokenKind::Comma) {
1261                self.advance();
1262                self.skip_newlines();
1263            }
1264        }
1265        Ok(params)
1266    }
1267
1268    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
1269        let mut args = Vec::new();
1270        self.skip_newlines();
1271
1272        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1273            if self.check(&TokenKind::Dot) {
1274                let saved_pos = self.pos;
1275                self.advance();
1276                if self.check(&TokenKind::Dot) {
1277                    self.advance();
1278                    self.consume(&TokenKind::Dot, ".")?;
1279                    let spread_start = self.tokens[saved_pos].span;
1280                    let expr = self.parse_nested_expression("spread argument")?;
1281                    args.push(spanned(
1282                        Node::Spread(Box::new(expr)),
1283                        Span::merge(spread_start, self.prev_span()),
1284                    ));
1285                } else {
1286                    self.pos = saved_pos;
1287                    args.push(self.parse_nested_expression("argument")?);
1288                }
1289            } else {
1290                args.push(self.parse_nested_expression("argument")?);
1291            }
1292            self.skip_newlines();
1293            if self.check(&TokenKind::Comma) {
1294                self.advance();
1295                self.skip_newlines();
1296            }
1297        }
1298        Ok(args)
1299    }
1300}