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