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