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