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