Skip to main content

harn_parser/parser/
expressions.rs

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