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