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            let right = self.parse_range()?;
25            left = spanned(
26                Node::BinaryOp {
27                    op: "|>".into(),
28                    left: Box::new(left),
29                    right: Box::new(right),
30                },
31                Span::merge(start, self.prev_span()),
32            );
33        }
34        Ok(left)
35    }
36
37    pub(super) fn parse_range(&mut self) -> Result<SNode, ParserError> {
38        let left = self.parse_ternary()?;
39        if self.check(&TokenKind::To) {
40            let start = left.span;
41            self.advance();
42            let right = self.parse_ternary()?;
43            let inclusive = if self.check(&TokenKind::Exclusive) {
44                self.advance();
45                false
46            } else {
47                true
48            };
49            return Ok(spanned(
50                Node::RangeExpr {
51                    start: Box::new(left),
52                    end: Box::new(right),
53                    inclusive,
54                },
55                Span::merge(start, self.prev_span()),
56            ));
57        }
58        Ok(left)
59    }
60
61    pub(super) fn parse_ternary(&mut self) -> Result<SNode, ParserError> {
62        let condition = self.parse_logical_or()?;
63        if !self.check(&TokenKind::Question) {
64            return Ok(condition);
65        }
66        let start = condition.span;
67        self.advance(); // skip ?
68        let true_val = self.parse_logical_or()?;
69        self.consume(&TokenKind::Colon, ":")?;
70        let false_val = self.parse_logical_or()?;
71        Ok(spanned(
72            Node::Ternary {
73                condition: Box::new(condition),
74                true_expr: Box::new(true_val),
75                false_expr: Box::new(false_val),
76            },
77            Span::merge(start, self.prev_span()),
78        ))
79    }
80
81    // `??` binds tighter than arithmetic/comparison but looser than `* / % **`,
82    // so `xs?.count ?? 0 > 0` parses as `(xs?.count ?? 0) > 0`.
83    pub(super) fn parse_nil_coalescing(&mut self) -> Result<SNode, ParserError> {
84        let mut left = self.parse_multiplicative()?;
85        while self.check(&TokenKind::NilCoal) {
86            let start = left.span;
87            self.advance();
88            let right = self.parse_multiplicative()?;
89            left = spanned(
90                Node::BinaryOp {
91                    op: "??".into(),
92                    left: Box::new(left),
93                    right: Box::new(right),
94                },
95                Span::merge(start, self.prev_span()),
96            );
97        }
98        Ok(left)
99    }
100
101    pub(super) fn parse_logical_or(&mut self) -> Result<SNode, ParserError> {
102        let mut left = self.parse_logical_and()?;
103        while self.check_skip_newlines(&TokenKind::Or) {
104            let start = left.span;
105            self.advance();
106            let right = self.parse_logical_and()?;
107            left = spanned(
108                Node::BinaryOp {
109                    op: "||".into(),
110                    left: Box::new(left),
111                    right: Box::new(right),
112                },
113                Span::merge(start, self.prev_span()),
114            );
115        }
116        Ok(left)
117    }
118
119    pub(super) fn parse_logical_and(&mut self) -> Result<SNode, ParserError> {
120        let mut left = self.parse_equality()?;
121        while self.check_skip_newlines(&TokenKind::And) {
122            let start = left.span;
123            self.advance();
124            let right = self.parse_equality()?;
125            left = spanned(
126                Node::BinaryOp {
127                    op: "&&".into(),
128                    left: Box::new(left),
129                    right: Box::new(right),
130                },
131                Span::merge(start, self.prev_span()),
132            );
133        }
134        Ok(left)
135    }
136
137    pub(super) fn parse_equality(&mut self) -> Result<SNode, ParserError> {
138        let mut left = self.parse_comparison()?;
139        while self.check(&TokenKind::Eq) || self.check(&TokenKind::Neq) {
140            let start = left.span;
141            let op = if self.check(&TokenKind::Eq) {
142                "=="
143            } else {
144                "!="
145            };
146            self.advance();
147            let right = self.parse_comparison()?;
148            left = spanned(
149                Node::BinaryOp {
150                    op: op.into(),
151                    left: Box::new(left),
152                    right: Box::new(right),
153                },
154                Span::merge(start, self.prev_span()),
155            );
156        }
157        Ok(left)
158    }
159
160    pub(super) fn parse_comparison(&mut self) -> Result<SNode, ParserError> {
161        let mut left = self.parse_additive()?;
162        loop {
163            if self.check(&TokenKind::Lt)
164                || self.check(&TokenKind::Gt)
165                || self.check(&TokenKind::Lte)
166                || self.check(&TokenKind::Gte)
167            {
168                let start = left.span;
169                let op = match self.current().map(|t| &t.kind) {
170                    Some(TokenKind::Lt) => "<",
171                    Some(TokenKind::Gt) => ">",
172                    Some(TokenKind::Lte) => "<=",
173                    Some(TokenKind::Gte) => ">=",
174                    _ => "<",
175                };
176                self.advance();
177                let right = self.parse_additive()?;
178                left = spanned(
179                    Node::BinaryOp {
180                        op: op.into(),
181                        left: Box::new(left),
182                        right: Box::new(right),
183                    },
184                    Span::merge(start, self.prev_span()),
185                );
186            } else if self.check(&TokenKind::In) {
187                let start = left.span;
188                self.advance();
189                let right = self.parse_additive()?;
190                left = spanned(
191                    Node::BinaryOp {
192                        op: "in".into(),
193                        left: Box::new(left),
194                        right: Box::new(right),
195                    },
196                    Span::merge(start, self.prev_span()),
197                );
198            } else if self.check_identifier("not") {
199                let saved = self.pos;
200                self.advance();
201                if self.check(&TokenKind::In) {
202                    let start = left.span;
203                    self.advance();
204                    let right = self.parse_additive()?;
205                    left = spanned(
206                        Node::BinaryOp {
207                            op: "not_in".into(),
208                            left: Box::new(left),
209                            right: Box::new(right),
210                        },
211                        Span::merge(start, self.prev_span()),
212                    );
213                } else {
214                    self.pos = saved;
215                    break;
216                }
217            } else {
218                break;
219            }
220        }
221        Ok(left)
222    }
223
224    pub(super) fn parse_additive(&mut self) -> Result<SNode, ParserError> {
225        let mut left = self.parse_nil_coalescing()?;
226        while self.check_skip_newlines(&TokenKind::Plus) || self.check(&TokenKind::Minus) {
227            let start = left.span;
228            let op = if self.check(&TokenKind::Plus) {
229                "+"
230            } else {
231                "-"
232            };
233            self.advance();
234            let right = self.parse_nil_coalescing()?;
235            left = spanned(
236                Node::BinaryOp {
237                    op: op.into(),
238                    left: Box::new(left),
239                    right: Box::new(right),
240                },
241                Span::merge(start, self.prev_span()),
242            );
243        }
244        Ok(left)
245    }
246
247    pub(super) fn parse_multiplicative(&mut self) -> Result<SNode, ParserError> {
248        let mut left = self.parse_exponent()?;
249        while self.check_skip_newlines(&TokenKind::Star)
250            || self.check_skip_newlines(&TokenKind::Slash)
251            || self.check_skip_newlines(&TokenKind::Percent)
252        {
253            let start = left.span;
254            let op = if self.check(&TokenKind::Star) {
255                "*"
256            } else if self.check(&TokenKind::Slash) {
257                "/"
258            } else {
259                "%"
260            };
261            self.advance();
262            let right = self.parse_exponent()?;
263            left = spanned(
264                Node::BinaryOp {
265                    op: op.into(),
266                    left: Box::new(left),
267                    right: Box::new(right),
268                },
269                Span::merge(start, self.prev_span()),
270            );
271        }
272        Ok(left)
273    }
274
275    pub(super) fn parse_exponent(&mut self) -> Result<SNode, ParserError> {
276        let left = self.parse_unary()?;
277        if !self.check_skip_newlines(&TokenKind::Pow) {
278            return Ok(left);
279        }
280
281        let start = left.span;
282        self.advance();
283        let right = self.parse_exponent()?;
284        Ok(spanned(
285            Node::BinaryOp {
286                op: "**".into(),
287                left: Box::new(left),
288                right: Box::new(right),
289            },
290            Span::merge(start, self.prev_span()),
291        ))
292    }
293
294    pub(super) fn parse_unary(&mut self) -> Result<SNode, ParserError> {
295        if self.check(&TokenKind::Not) {
296            let start = self.current_span();
297            self.advance();
298            let operand = self.parse_unary()?;
299            return Ok(spanned(
300                Node::UnaryOp {
301                    op: "!".into(),
302                    operand: Box::new(operand),
303                },
304                Span::merge(start, self.prev_span()),
305            ));
306        }
307        if self.check(&TokenKind::Minus) {
308            let start = self.current_span();
309            self.advance();
310            let operand = self.parse_unary()?;
311            return Ok(spanned(
312                Node::UnaryOp {
313                    op: "-".into(),
314                    operand: Box::new(operand),
315                },
316                Span::merge(start, self.prev_span()),
317            ));
318        }
319        self.parse_postfix()
320    }
321
322    pub(super) fn parse_postfix(&mut self) -> Result<SNode, ParserError> {
323        let mut expr = self.parse_primary()?;
324
325        loop {
326            if self.check_skip_newlines(&TokenKind::Dot)
327                || self.check_skip_newlines(&TokenKind::QuestionDot)
328            {
329                let optional = self.check(&TokenKind::QuestionDot);
330                let start = expr.span;
331                self.advance();
332                let member = self.consume_identifier_or_keyword("member name")?;
333                if self.check(&TokenKind::LParen) {
334                    self.advance();
335                    let args = self.parse_arg_list()?;
336                    self.consume(&TokenKind::RParen, ")")?;
337                    if optional {
338                        expr = spanned(
339                            Node::OptionalMethodCall {
340                                object: Box::new(expr),
341                                method: member,
342                                args,
343                            },
344                            Span::merge(start, self.prev_span()),
345                        );
346                    } else {
347                        expr = spanned(
348                            Node::MethodCall {
349                                object: Box::new(expr),
350                                method: member,
351                                args,
352                            },
353                            Span::merge(start, self.prev_span()),
354                        );
355                    }
356                } else if optional {
357                    expr = spanned(
358                        Node::OptionalPropertyAccess {
359                            object: Box::new(expr),
360                            property: member,
361                        },
362                        Span::merge(start, self.prev_span()),
363                    );
364                } else {
365                    expr = spanned(
366                        Node::PropertyAccess {
367                            object: Box::new(expr),
368                            property: member,
369                        },
370                        Span::merge(start, self.prev_span()),
371                    );
372                }
373            } else if self.check(&TokenKind::LBracket) {
374                let start = expr.span;
375                self.advance();
376
377                // Disambiguate `[:end]` / `[start:end]` / `[start:]` slices from
378                // `[index]` subscript access.
379                if self.check(&TokenKind::Colon) {
380                    self.advance();
381                    let end_expr = if self.check(&TokenKind::RBracket) {
382                        None
383                    } else {
384                        Some(Box::new(self.parse_expression()?))
385                    };
386                    self.consume(&TokenKind::RBracket, "]")?;
387                    expr = spanned(
388                        Node::SliceAccess {
389                            object: Box::new(expr),
390                            start: None,
391                            end: end_expr,
392                        },
393                        Span::merge(start, self.prev_span()),
394                    );
395                } else {
396                    let index = self.parse_expression()?;
397                    if self.check(&TokenKind::Colon) {
398                        self.advance();
399                        let end_expr = if self.check(&TokenKind::RBracket) {
400                            None
401                        } else {
402                            Some(Box::new(self.parse_expression()?))
403                        };
404                        self.consume(&TokenKind::RBracket, "]")?;
405                        expr = spanned(
406                            Node::SliceAccess {
407                                object: Box::new(expr),
408                                start: Some(Box::new(index)),
409                                end: end_expr,
410                            },
411                            Span::merge(start, self.prev_span()),
412                        );
413                    } else {
414                        self.consume(&TokenKind::RBracket, "]")?;
415                        expr = spanned(
416                            Node::SubscriptAccess {
417                                object: Box::new(expr),
418                                index: Box::new(index),
419                            },
420                            Span::merge(start, self.prev_span()),
421                        );
422                    }
423                }
424            } else if self.check(&TokenKind::LBrace) {
425                let struct_name = match &expr.node {
426                    Node::Identifier(name) if self.is_struct_construct_lookahead(name) => {
427                        Some(name.clone())
428                    }
429                    _ => None,
430                };
431                let Some(struct_name) = struct_name else {
432                    break;
433                };
434                let start = expr.span;
435                self.advance();
436                let dict = self.parse_dict_literal(start)?;
437                let fields = match dict.node {
438                    Node::DictLiteral(fields) => fields,
439                    _ => unreachable!("dict parser must return a dict literal"),
440                };
441                expr = spanned(
442                    Node::StructConstruct {
443                        struct_name,
444                        fields,
445                    },
446                    dict.span,
447                );
448            } else if self.check(&TokenKind::LParen) && matches!(expr.node, Node::Identifier(_)) {
449                let start = expr.span;
450                self.advance();
451                let args = self.parse_arg_list()?;
452                self.consume(&TokenKind::RParen, ")")?;
453                if let Node::Identifier(name) = expr.node {
454                    expr = spanned(
455                        Node::FunctionCall { name, args },
456                        Span::merge(start, self.prev_span()),
457                    );
458                }
459            } else if self.check(&TokenKind::Question) {
460                // Postfix try `expr?` vs ternary `expr ? a : b`: if the next token
461                // could start a ternary branch, let parse_ternary handle the `?`.
462                let next_pos = self.pos + 1;
463                let is_ternary = self.tokens.get(next_pos).is_some_and(|t| {
464                    matches!(
465                        t.kind,
466                        TokenKind::Identifier(_)
467                            | TokenKind::IntLiteral(_)
468                            | TokenKind::FloatLiteral(_)
469                            | TokenKind::StringLiteral(_)
470                            | TokenKind::InterpolatedString(_)
471                            | TokenKind::True
472                            | TokenKind::False
473                            | TokenKind::Nil
474                            | TokenKind::LParen
475                            | TokenKind::LBracket
476                            | TokenKind::LBrace
477                            | TokenKind::Not
478                            | TokenKind::Minus
479                            | TokenKind::Fn
480                    )
481                });
482                if is_ternary {
483                    break;
484                }
485                let start = expr.span;
486                self.advance();
487                expr = spanned(
488                    Node::TryOperator {
489                        operand: Box::new(expr),
490                    },
491                    Span::merge(start, self.prev_span()),
492                );
493            } else {
494                break;
495            }
496        }
497
498        Ok(expr)
499    }
500
501    pub(super) fn parse_primary(&mut self) -> Result<SNode, ParserError> {
502        let tok = self.current().ok_or_else(|| ParserError::UnexpectedEof {
503            expected: "expression".into(),
504            span: self.prev_span(),
505        })?;
506        let start = self.current_span();
507
508        match &tok.kind {
509            TokenKind::StringLiteral(s) => {
510                let s = s.clone();
511                self.advance();
512                Ok(spanned(
513                    Node::StringLiteral(s),
514                    Span::merge(start, self.prev_span()),
515                ))
516            }
517            TokenKind::RawStringLiteral(s) => {
518                let s = s.clone();
519                self.advance();
520                Ok(spanned(
521                    Node::RawStringLiteral(s),
522                    Span::merge(start, self.prev_span()),
523                ))
524            }
525            TokenKind::InterpolatedString(segments) => {
526                let segments = segments.clone();
527                self.advance();
528                Ok(spanned(
529                    Node::InterpolatedString(segments),
530                    Span::merge(start, self.prev_span()),
531                ))
532            }
533            TokenKind::IntLiteral(n) => {
534                let n = *n;
535                self.advance();
536                Ok(spanned(
537                    Node::IntLiteral(n),
538                    Span::merge(start, self.prev_span()),
539                ))
540            }
541            TokenKind::FloatLiteral(n) => {
542                let n = *n;
543                self.advance();
544                Ok(spanned(
545                    Node::FloatLiteral(n),
546                    Span::merge(start, self.prev_span()),
547                ))
548            }
549            TokenKind::True => {
550                self.advance();
551                Ok(spanned(
552                    Node::BoolLiteral(true),
553                    Span::merge(start, self.prev_span()),
554                ))
555            }
556            TokenKind::False => {
557                self.advance();
558                Ok(spanned(
559                    Node::BoolLiteral(false),
560                    Span::merge(start, self.prev_span()),
561                ))
562            }
563            TokenKind::Nil => {
564                self.advance();
565                Ok(spanned(
566                    Node::NilLiteral,
567                    Span::merge(start, self.prev_span()),
568                ))
569            }
570            TokenKind::Identifier(name) => {
571                let name = name.clone();
572                self.advance();
573                Ok(spanned(
574                    Node::Identifier(name),
575                    Span::merge(start, self.prev_span()),
576                ))
577            }
578            TokenKind::LParen => {
579                self.advance();
580                let expr = self.parse_expression()?;
581                self.consume(&TokenKind::RParen, ")")?;
582                Ok(expr)
583            }
584            TokenKind::LBracket => self.parse_list_literal(),
585            TokenKind::LBrace => self.parse_dict_or_closure(),
586            TokenKind::Parallel => self.parse_parallel(),
587            TokenKind::Retry => self.parse_retry(),
588            TokenKind::If => self.parse_if_else(),
589            TokenKind::Spawn => self.parse_spawn_expr(),
590            TokenKind::DurationLiteral(ms) => {
591                let ms = *ms;
592                self.advance();
593                Ok(spanned(
594                    Node::DurationLiteral(ms),
595                    Span::merge(start, self.prev_span()),
596                ))
597            }
598            TokenKind::Deadline => self.parse_deadline(),
599            TokenKind::Try => self.parse_try_catch(),
600            TokenKind::Match => self.parse_match(),
601            TokenKind::Fn => self.parse_fn_expr(),
602            // Heredoc `<<TAG ... TAG` is only valid inside LLM tool-call JSON;
603            // in source-position expressions, redirect authors to triple-quoted strings.
604            TokenKind::Lt
605                if matches!(self.peek_kind(), Some(&TokenKind::Lt))
606                    && matches!(self.peek_kind_at(2), Some(TokenKind::Identifier(_))) =>
607            {
608                Err(ParserError::Unexpected {
609                    got: "`<<` heredoc-like syntax".to_string(),
610                    expected: "an expression — heredocs are only valid \
611                               inside LLM tool-call argument JSON; \
612                               for multiline strings in source code use \
613                               triple-quoted `\"\"\"...\"\"\"`"
614                        .to_string(),
615                    span: start,
616                })
617            }
618            _ => Err(self.error("expression")),
619        }
620    }
621
622    /// Anonymous function `fn(params) { body }`. Sets `fn_syntax: true` on the
623    /// Closure so the formatter can round-trip the original syntax.
624    pub(super) fn parse_fn_expr(&mut self) -> Result<SNode, ParserError> {
625        let start = self.current_span();
626        self.consume(&TokenKind::Fn, "fn")?;
627        self.consume(&TokenKind::LParen, "(")?;
628        let params = self.parse_typed_param_list()?;
629        self.consume(&TokenKind::RParen, ")")?;
630        self.consume(&TokenKind::LBrace, "{")?;
631        let body = self.parse_block()?;
632        self.consume(&TokenKind::RBrace, "}")?;
633        Ok(spanned(
634            Node::Closure {
635                params,
636                body,
637                fn_syntax: true,
638            },
639            Span::merge(start, self.prev_span()),
640        ))
641    }
642
643    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
644        let start = self.current_span();
645        self.consume(&TokenKind::Spawn, "spawn")?;
646        self.consume(&TokenKind::LBrace, "{")?;
647        let body = self.parse_block()?;
648        self.consume(&TokenKind::RBrace, "}")?;
649        Ok(spanned(
650            Node::SpawnExpr { body },
651            Span::merge(start, self.prev_span()),
652        ))
653    }
654
655    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
656        let start = self.current_span();
657        self.consume(&TokenKind::LBracket, "[")?;
658        let mut elements = Vec::new();
659        self.skip_newlines();
660
661        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
662            if self.check(&TokenKind::Dot) {
663                let saved_pos = self.pos;
664                self.advance();
665                if self.check(&TokenKind::Dot) {
666                    self.advance();
667                    self.consume(&TokenKind::Dot, ".")?;
668                    let spread_start = self.tokens[saved_pos].span;
669                    let expr = self.parse_expression()?;
670                    elements.push(spanned(
671                        Node::Spread(Box::new(expr)),
672                        Span::merge(spread_start, self.prev_span()),
673                    ));
674                } else {
675                    self.pos = saved_pos;
676                    elements.push(self.parse_expression()?);
677                }
678            } else {
679                elements.push(self.parse_expression()?);
680            }
681            self.skip_newlines();
682            if self.check(&TokenKind::Comma) {
683                self.advance();
684                self.skip_newlines();
685            }
686        }
687
688        self.consume(&TokenKind::RBracket, "]")?;
689        Ok(spanned(
690            Node::ListLiteral(elements),
691            Span::merge(start, self.prev_span()),
692        ))
693    }
694
695    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
696        let start = self.current_span();
697        self.consume(&TokenKind::LBrace, "{")?;
698        self.skip_newlines();
699
700        if self.check(&TokenKind::RBrace) {
701            self.advance();
702            return Ok(spanned(
703                Node::DictLiteral(Vec::new()),
704                Span::merge(start, self.prev_span()),
705            ));
706        }
707
708        // Scan for `->` before the closing `}` to distinguish closure from dict.
709        let saved = self.pos;
710        if self.is_closure_lookahead() {
711            self.pos = saved;
712            return self.parse_closure_body(start);
713        }
714        self.pos = saved;
715        self.parse_dict_literal(start)
716    }
717
718    /// After seeing `Identifier {`, decide whether the brace block is a
719    /// struct-construction field list rather than a control-flow block.
720    /// Struct fields always start with `name:` / `"name":` or `}`.
721    pub(super) fn is_struct_construct_lookahead(&self, struct_name: &str) -> bool {
722        if !struct_name
723            .chars()
724            .next()
725            .is_some_and(|ch| ch.is_uppercase())
726        {
727            return false;
728        }
729
730        let mut offset = 1;
731        while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
732            offset += 1;
733        }
734
735        match self.peek_kind_at(offset) {
736            Some(TokenKind::RBrace) => true,
737            Some(TokenKind::Identifier(_)) | Some(TokenKind::StringLiteral(_)) => {
738                offset += 1;
739                while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
740                    offset += 1;
741                }
742                matches!(self.peek_kind_at(offset), Some(TokenKind::Colon))
743            }
744            _ => false,
745        }
746    }
747
748    /// Caller must save/restore `pos`; this advances while scanning.
749    pub(super) fn is_closure_lookahead(&mut self) -> bool {
750        let mut depth = 0;
751        while !self.is_at_end() {
752            if let Some(tok) = self.current() {
753                match &tok.kind {
754                    TokenKind::Arrow if depth == 0 => return true,
755                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
756                    TokenKind::RBrace if depth == 0 => return false,
757                    TokenKind::RBrace => depth -= 1,
758                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
759                    _ => {}
760                }
761                self.advance();
762            } else {
763                return false;
764            }
765        }
766        false
767    }
768
769    /// Parse closure params and body (after opening { has been consumed).
770    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
771        let params = self.parse_typed_param_list_until_arrow()?;
772        self.consume(&TokenKind::Arrow, "->")?;
773        let body = self.parse_block()?;
774        self.consume(&TokenKind::RBrace, "}")?;
775        Ok(spanned(
776            Node::Closure {
777                params,
778                body,
779                fn_syntax: false,
780            },
781            Span::merge(start, self.prev_span()),
782        ))
783    }
784
785    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
786    pub(super) fn parse_typed_param_list_until_arrow(
787        &mut self,
788    ) -> Result<Vec<TypedParam>, ParserError> {
789        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
790    }
791
792    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
793        let entries = self.parse_dict_entries()?;
794        Ok(spanned(
795            Node::DictLiteral(entries),
796            Span::merge(start, self.prev_span()),
797        ))
798    }
799
800    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
801        let mut entries = Vec::new();
802        self.skip_newlines();
803
804        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
805            if self.check(&TokenKind::Dot) {
806                let saved_pos = self.pos;
807                self.advance();
808                if self.check(&TokenKind::Dot) {
809                    self.advance();
810                    if self.check(&TokenKind::Dot) {
811                        self.advance();
812                        let spread_start = self.tokens[saved_pos].span;
813                        let expr = self.parse_expression()?;
814                        entries.push(DictEntry {
815                            key: spanned(Node::NilLiteral, spread_start),
816                            value: spanned(
817                                Node::Spread(Box::new(expr)),
818                                Span::merge(spread_start, self.prev_span()),
819                            ),
820                        });
821                        self.skip_newlines();
822                        if self.check(&TokenKind::Comma) {
823                            self.advance();
824                            self.skip_newlines();
825                        }
826                        continue;
827                    }
828                    self.pos = saved_pos;
829                } else {
830                    self.pos = saved_pos;
831                }
832            }
833            let key = if self.check(&TokenKind::LBracket) {
834                self.advance();
835                let k = self.parse_expression()?;
836                self.consume(&TokenKind::RBracket, "]")?;
837                k
838            } else if matches!(
839                self.current().map(|t| &t.kind),
840                Some(TokenKind::StringLiteral(_))
841            ) {
842                let key_span = self.current_span();
843                let name =
844                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
845                        s.clone()
846                    } else {
847                        unreachable!()
848                    };
849                self.advance();
850                spanned(Node::StringLiteral(name), key_span)
851            } else {
852                let key_span = self.current_span();
853                let name = self.consume_identifier_or_keyword("dict key")?;
854                spanned(Node::StringLiteral(name), key_span)
855            };
856            self.consume(&TokenKind::Colon, ":")?;
857            let value = self.parse_expression()?;
858            entries.push(DictEntry { key, value });
859            self.skip_newlines();
860            if self.check(&TokenKind::Comma) {
861                self.advance();
862                self.skip_newlines();
863            }
864        }
865
866        self.consume(&TokenKind::RBrace, "}")?;
867        Ok(entries)
868    }
869
870    /// Parse untyped parameter list (for pipelines, overrides).
871    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
872        let mut params = Vec::new();
873        self.skip_newlines();
874
875        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
876            params.push(self.consume_identifier("parameter name")?);
877            if self.check(&TokenKind::Comma) {
878                self.advance();
879                self.skip_newlines();
880            }
881        }
882        Ok(params)
883    }
884
885    /// Parse typed parameter list (for fn declarations).
886    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
887        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
888    }
889
890    /// Shared implementation: parse typed params with optional defaults until
891    /// a terminator token is reached.
892    pub(super) fn parse_typed_params_until(
893        &mut self,
894        is_terminator: impl Fn(&TokenKind) -> bool,
895    ) -> Result<Vec<TypedParam>, ParserError> {
896        let mut params = Vec::new();
897        let mut seen_default = false;
898        self.skip_newlines();
899
900        while !self.is_at_end() {
901            if let Some(tok) = self.current() {
902                if is_terminator(&tok.kind) {
903                    break;
904                }
905            } else {
906                break;
907            }
908            let is_rest = if self.check(&TokenKind::Dot) {
909                let p1 = self.pos + 1;
910                let p2 = self.pos + 2;
911                let is_ellipsis = p1 < self.tokens.len()
912                    && p2 < self.tokens.len()
913                    && self.tokens[p1].kind == TokenKind::Dot
914                    && self.tokens[p2].kind == TokenKind::Dot;
915                if is_ellipsis {
916                    self.advance();
917                    self.advance();
918                    self.advance();
919                    true
920                } else {
921                    false
922                }
923            } else {
924                false
925            };
926            let name = self.consume_identifier("parameter name")?;
927            let type_expr = self.try_parse_type_annotation()?;
928            let default_value = if self.check(&TokenKind::Assign) {
929                self.advance();
930                seen_default = true;
931                Some(Box::new(self.parse_expression()?))
932            } else {
933                if seen_default && !is_rest {
934                    return Err(self.error(
935                        "Required parameter cannot follow a parameter with a default value",
936                    ));
937                }
938                None
939            };
940            if is_rest
941                && !is_terminator(
942                    &self
943                        .current()
944                        .map(|t| t.kind.clone())
945                        .unwrap_or(TokenKind::Eof),
946                )
947            {
948                return Err(self.error("Rest parameter must be the last parameter"));
949            }
950            params.push(TypedParam {
951                name,
952                type_expr,
953                default_value,
954                rest: is_rest,
955            });
956            if self.check(&TokenKind::Comma) {
957                self.advance();
958                self.skip_newlines();
959            }
960        }
961        Ok(params)
962    }
963
964    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
965        let mut args = Vec::new();
966        self.skip_newlines();
967
968        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
969            if self.check(&TokenKind::Dot) {
970                let saved_pos = self.pos;
971                self.advance();
972                if self.check(&TokenKind::Dot) {
973                    self.advance();
974                    self.consume(&TokenKind::Dot, ".")?;
975                    let spread_start = self.tokens[saved_pos].span;
976                    let expr = self.parse_expression()?;
977                    args.push(spanned(
978                        Node::Spread(Box::new(expr)),
979                        Span::merge(spread_start, self.prev_span()),
980                    ));
981                } else {
982                    self.pos = saved_pos;
983                    args.push(self.parse_expression()?);
984                }
985            } else {
986                args.push(self.parse_expression()?);
987            }
988            self.skip_newlines();
989            if self.check(&TokenKind::Comma) {
990                self.advance();
991                self.skip_newlines();
992            }
993        }
994        Ok(args)
995    }
996}