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                && matches!(&expr.node, Node::Identifier(name) if self.struct_names.contains(name))
426            {
427                let start = expr.span;
428                let struct_name = match expr.node {
429                    Node::Identifier(name) => name,
430                    _ => unreachable!("checked above"),
431                };
432                self.advance();
433                let dict = self.parse_dict_literal(start)?;
434                let fields = match dict.node {
435                    Node::DictLiteral(fields) => fields,
436                    _ => unreachable!("dict parser must return a dict literal"),
437                };
438                expr = spanned(
439                    Node::StructConstruct {
440                        struct_name,
441                        fields,
442                    },
443                    dict.span,
444                );
445            } else if self.check(&TokenKind::LParen) && matches!(expr.node, Node::Identifier(_)) {
446                let start = expr.span;
447                self.advance();
448                let args = self.parse_arg_list()?;
449                self.consume(&TokenKind::RParen, ")")?;
450                if let Node::Identifier(name) = expr.node {
451                    expr = spanned(
452                        Node::FunctionCall { name, args },
453                        Span::merge(start, self.prev_span()),
454                    );
455                }
456            } else if self.check(&TokenKind::Question) {
457                // Postfix try `expr?` vs ternary `expr ? a : b`: if the next token
458                // could start a ternary branch, let parse_ternary handle the `?`.
459                let next_pos = self.pos + 1;
460                let is_ternary = self.tokens.get(next_pos).is_some_and(|t| {
461                    matches!(
462                        t.kind,
463                        TokenKind::Identifier(_)
464                            | TokenKind::IntLiteral(_)
465                            | TokenKind::FloatLiteral(_)
466                            | TokenKind::StringLiteral(_)
467                            | TokenKind::InterpolatedString(_)
468                            | TokenKind::True
469                            | TokenKind::False
470                            | TokenKind::Nil
471                            | TokenKind::LParen
472                            | TokenKind::LBracket
473                            | TokenKind::LBrace
474                            | TokenKind::Not
475                            | TokenKind::Minus
476                            | TokenKind::Fn
477                    )
478                });
479                if is_ternary {
480                    break;
481                }
482                let start = expr.span;
483                self.advance();
484                expr = spanned(
485                    Node::TryOperator {
486                        operand: Box::new(expr),
487                    },
488                    Span::merge(start, self.prev_span()),
489                );
490            } else {
491                break;
492            }
493        }
494
495        Ok(expr)
496    }
497
498    pub(super) fn parse_primary(&mut self) -> Result<SNode, ParserError> {
499        let tok = self.current().ok_or_else(|| ParserError::UnexpectedEof {
500            expected: "expression".into(),
501            span: self.prev_span(),
502        })?;
503        let start = self.current_span();
504
505        match &tok.kind {
506            TokenKind::StringLiteral(s) => {
507                let s = s.clone();
508                self.advance();
509                Ok(spanned(
510                    Node::StringLiteral(s),
511                    Span::merge(start, self.prev_span()),
512                ))
513            }
514            TokenKind::RawStringLiteral(s) => {
515                let s = s.clone();
516                self.advance();
517                Ok(spanned(
518                    Node::RawStringLiteral(s),
519                    Span::merge(start, self.prev_span()),
520                ))
521            }
522            TokenKind::InterpolatedString(segments) => {
523                let segments = segments.clone();
524                self.advance();
525                Ok(spanned(
526                    Node::InterpolatedString(segments),
527                    Span::merge(start, self.prev_span()),
528                ))
529            }
530            TokenKind::IntLiteral(n) => {
531                let n = *n;
532                self.advance();
533                Ok(spanned(
534                    Node::IntLiteral(n),
535                    Span::merge(start, self.prev_span()),
536                ))
537            }
538            TokenKind::FloatLiteral(n) => {
539                let n = *n;
540                self.advance();
541                Ok(spanned(
542                    Node::FloatLiteral(n),
543                    Span::merge(start, self.prev_span()),
544                ))
545            }
546            TokenKind::True => {
547                self.advance();
548                Ok(spanned(
549                    Node::BoolLiteral(true),
550                    Span::merge(start, self.prev_span()),
551                ))
552            }
553            TokenKind::False => {
554                self.advance();
555                Ok(spanned(
556                    Node::BoolLiteral(false),
557                    Span::merge(start, self.prev_span()),
558                ))
559            }
560            TokenKind::Nil => {
561                self.advance();
562                Ok(spanned(
563                    Node::NilLiteral,
564                    Span::merge(start, self.prev_span()),
565                ))
566            }
567            TokenKind::Identifier(name) => {
568                let name = name.clone();
569                self.advance();
570                Ok(spanned(
571                    Node::Identifier(name),
572                    Span::merge(start, self.prev_span()),
573                ))
574            }
575            TokenKind::LParen => {
576                self.advance();
577                let expr = self.parse_expression()?;
578                self.consume(&TokenKind::RParen, ")")?;
579                Ok(expr)
580            }
581            TokenKind::LBracket => self.parse_list_literal(),
582            TokenKind::LBrace => self.parse_dict_or_closure(),
583            TokenKind::Parallel => self.parse_parallel(),
584            TokenKind::Retry => self.parse_retry(),
585            TokenKind::If => self.parse_if_else(),
586            TokenKind::Spawn => self.parse_spawn_expr(),
587            TokenKind::DurationLiteral(ms) => {
588                let ms = *ms;
589                self.advance();
590                Ok(spanned(
591                    Node::DurationLiteral(ms),
592                    Span::merge(start, self.prev_span()),
593                ))
594            }
595            TokenKind::Deadline => self.parse_deadline(),
596            TokenKind::Try => self.parse_try_catch(),
597            TokenKind::Match => self.parse_match(),
598            TokenKind::Fn => self.parse_fn_expr(),
599            // Heredoc `<<TAG ... TAG` is only valid inside LLM tool-call JSON;
600            // in source-position expressions, redirect authors to triple-quoted strings.
601            TokenKind::Lt
602                if matches!(self.peek_kind(), Some(&TokenKind::Lt))
603                    && matches!(self.peek_kind_at(2), Some(TokenKind::Identifier(_))) =>
604            {
605                Err(ParserError::Unexpected {
606                    got: "`<<` heredoc-like syntax".to_string(),
607                    expected: "an expression — heredocs are only valid \
608                               inside LLM tool-call argument JSON; \
609                               for multiline strings in source code use \
610                               triple-quoted `\"\"\"...\"\"\"`"
611                        .to_string(),
612                    span: start,
613                })
614            }
615            _ => Err(self.error("expression")),
616        }
617    }
618
619    /// Anonymous function `fn(params) { body }`. Sets `fn_syntax: true` on the
620    /// Closure so the formatter can round-trip the original syntax.
621    pub(super) fn parse_fn_expr(&mut self) -> Result<SNode, ParserError> {
622        let start = self.current_span();
623        self.consume(&TokenKind::Fn, "fn")?;
624        self.consume(&TokenKind::LParen, "(")?;
625        let params = self.parse_typed_param_list()?;
626        self.consume(&TokenKind::RParen, ")")?;
627        self.consume(&TokenKind::LBrace, "{")?;
628        let body = self.parse_block()?;
629        self.consume(&TokenKind::RBrace, "}")?;
630        Ok(spanned(
631            Node::Closure {
632                params,
633                body,
634                fn_syntax: true,
635            },
636            Span::merge(start, self.prev_span()),
637        ))
638    }
639
640    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
641        let start = self.current_span();
642        self.consume(&TokenKind::Spawn, "spawn")?;
643        self.consume(&TokenKind::LBrace, "{")?;
644        let body = self.parse_block()?;
645        self.consume(&TokenKind::RBrace, "}")?;
646        Ok(spanned(
647            Node::SpawnExpr { body },
648            Span::merge(start, self.prev_span()),
649        ))
650    }
651
652    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
653        let start = self.current_span();
654        self.consume(&TokenKind::LBracket, "[")?;
655        let mut elements = Vec::new();
656        self.skip_newlines();
657
658        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
659            if self.check(&TokenKind::Dot) {
660                let saved_pos = self.pos;
661                self.advance();
662                if self.check(&TokenKind::Dot) {
663                    self.advance();
664                    self.consume(&TokenKind::Dot, ".")?;
665                    let spread_start = self.tokens[saved_pos].span;
666                    let expr = self.parse_expression()?;
667                    elements.push(spanned(
668                        Node::Spread(Box::new(expr)),
669                        Span::merge(spread_start, self.prev_span()),
670                    ));
671                } else {
672                    self.pos = saved_pos;
673                    elements.push(self.parse_expression()?);
674                }
675            } else {
676                elements.push(self.parse_expression()?);
677            }
678            self.skip_newlines();
679            if self.check(&TokenKind::Comma) {
680                self.advance();
681                self.skip_newlines();
682            }
683        }
684
685        self.consume(&TokenKind::RBracket, "]")?;
686        Ok(spanned(
687            Node::ListLiteral(elements),
688            Span::merge(start, self.prev_span()),
689        ))
690    }
691
692    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
693        let start = self.current_span();
694        self.consume(&TokenKind::LBrace, "{")?;
695        self.skip_newlines();
696
697        if self.check(&TokenKind::RBrace) {
698            self.advance();
699            return Ok(spanned(
700                Node::DictLiteral(Vec::new()),
701                Span::merge(start, self.prev_span()),
702            ));
703        }
704
705        // Scan for `->` before the closing `}` to distinguish closure from dict.
706        let saved = self.pos;
707        if self.is_closure_lookahead() {
708            self.pos = saved;
709            return self.parse_closure_body(start);
710        }
711        self.pos = saved;
712        self.parse_dict_literal(start)
713    }
714
715    /// Caller must save/restore `pos`; this advances while scanning.
716    pub(super) fn is_closure_lookahead(&mut self) -> bool {
717        let mut depth = 0;
718        while !self.is_at_end() {
719            if let Some(tok) = self.current() {
720                match &tok.kind {
721                    TokenKind::Arrow if depth == 0 => return true,
722                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
723                    TokenKind::RBrace if depth == 0 => return false,
724                    TokenKind::RBrace => depth -= 1,
725                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
726                    _ => {}
727                }
728                self.advance();
729            } else {
730                return false;
731            }
732        }
733        false
734    }
735
736    /// Parse closure params and body (after opening { has been consumed).
737    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
738        let params = self.parse_typed_param_list_until_arrow()?;
739        self.consume(&TokenKind::Arrow, "->")?;
740        let body = self.parse_block()?;
741        self.consume(&TokenKind::RBrace, "}")?;
742        Ok(spanned(
743            Node::Closure {
744                params,
745                body,
746                fn_syntax: false,
747            },
748            Span::merge(start, self.prev_span()),
749        ))
750    }
751
752    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
753    pub(super) fn parse_typed_param_list_until_arrow(
754        &mut self,
755    ) -> Result<Vec<TypedParam>, ParserError> {
756        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
757    }
758
759    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
760        let entries = self.parse_dict_entries()?;
761        Ok(spanned(
762            Node::DictLiteral(entries),
763            Span::merge(start, self.prev_span()),
764        ))
765    }
766
767    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
768        let mut entries = Vec::new();
769        self.skip_newlines();
770
771        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
772            if self.check(&TokenKind::Dot) {
773                let saved_pos = self.pos;
774                self.advance();
775                if self.check(&TokenKind::Dot) {
776                    self.advance();
777                    if self.check(&TokenKind::Dot) {
778                        self.advance();
779                        let spread_start = self.tokens[saved_pos].span;
780                        let expr = self.parse_expression()?;
781                        entries.push(DictEntry {
782                            key: spanned(Node::NilLiteral, spread_start),
783                            value: spanned(
784                                Node::Spread(Box::new(expr)),
785                                Span::merge(spread_start, self.prev_span()),
786                            ),
787                        });
788                        self.skip_newlines();
789                        if self.check(&TokenKind::Comma) {
790                            self.advance();
791                            self.skip_newlines();
792                        }
793                        continue;
794                    }
795                    self.pos = saved_pos;
796                } else {
797                    self.pos = saved_pos;
798                }
799            }
800            let key = if self.check(&TokenKind::LBracket) {
801                self.advance();
802                let k = self.parse_expression()?;
803                self.consume(&TokenKind::RBracket, "]")?;
804                k
805            } else if matches!(
806                self.current().map(|t| &t.kind),
807                Some(TokenKind::StringLiteral(_))
808            ) {
809                let key_span = self.current_span();
810                let name =
811                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
812                        s.clone()
813                    } else {
814                        unreachable!()
815                    };
816                self.advance();
817                spanned(Node::StringLiteral(name), key_span)
818            } else {
819                let key_span = self.current_span();
820                let name = self.consume_identifier_or_keyword("dict key")?;
821                spanned(Node::StringLiteral(name), key_span)
822            };
823            self.consume(&TokenKind::Colon, ":")?;
824            let value = self.parse_expression()?;
825            entries.push(DictEntry { key, value });
826            self.skip_newlines();
827            if self.check(&TokenKind::Comma) {
828                self.advance();
829                self.skip_newlines();
830            }
831        }
832
833        self.consume(&TokenKind::RBrace, "}")?;
834        Ok(entries)
835    }
836
837    /// Parse untyped parameter list (for pipelines, overrides).
838    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
839        let mut params = Vec::new();
840        self.skip_newlines();
841
842        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
843            params.push(self.consume_identifier("parameter name")?);
844            if self.check(&TokenKind::Comma) {
845                self.advance();
846                self.skip_newlines();
847            }
848        }
849        Ok(params)
850    }
851
852    /// Parse typed parameter list (for fn declarations).
853    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
854        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
855    }
856
857    /// Shared implementation: parse typed params with optional defaults until
858    /// a terminator token is reached.
859    pub(super) fn parse_typed_params_until(
860        &mut self,
861        is_terminator: impl Fn(&TokenKind) -> bool,
862    ) -> Result<Vec<TypedParam>, ParserError> {
863        let mut params = Vec::new();
864        let mut seen_default = false;
865        self.skip_newlines();
866
867        while !self.is_at_end() {
868            if let Some(tok) = self.current() {
869                if is_terminator(&tok.kind) {
870                    break;
871                }
872            } else {
873                break;
874            }
875            let is_rest = if self.check(&TokenKind::Dot) {
876                let p1 = self.pos + 1;
877                let p2 = self.pos + 2;
878                let is_ellipsis = p1 < self.tokens.len()
879                    && p2 < self.tokens.len()
880                    && self.tokens[p1].kind == TokenKind::Dot
881                    && self.tokens[p2].kind == TokenKind::Dot;
882                if is_ellipsis {
883                    self.advance();
884                    self.advance();
885                    self.advance();
886                    true
887                } else {
888                    false
889                }
890            } else {
891                false
892            };
893            let name = self.consume_identifier("parameter name")?;
894            let type_expr = self.try_parse_type_annotation()?;
895            let default_value = if self.check(&TokenKind::Assign) {
896                self.advance();
897                seen_default = true;
898                Some(Box::new(self.parse_expression()?))
899            } else {
900                if seen_default && !is_rest {
901                    return Err(self.error(
902                        "Required parameter cannot follow a parameter with a default value",
903                    ));
904                }
905                None
906            };
907            if is_rest
908                && !is_terminator(
909                    &self
910                        .current()
911                        .map(|t| t.kind.clone())
912                        .unwrap_or(TokenKind::Eof),
913                )
914            {
915                return Err(self.error("Rest parameter must be the last parameter"));
916            }
917            params.push(TypedParam {
918                name,
919                type_expr,
920                default_value,
921                rest: is_rest,
922            });
923            if self.check(&TokenKind::Comma) {
924                self.advance();
925                self.skip_newlines();
926            }
927        }
928        Ok(params)
929    }
930
931    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
932        let mut args = Vec::new();
933        self.skip_newlines();
934
935        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
936            if self.check(&TokenKind::Dot) {
937                let saved_pos = self.pos;
938                self.advance();
939                if self.check(&TokenKind::Dot) {
940                    self.advance();
941                    self.consume(&TokenKind::Dot, ".")?;
942                    let spread_start = self.tokens[saved_pos].span;
943                    let expr = self.parse_expression()?;
944                    args.push(spanned(
945                        Node::Spread(Box::new(expr)),
946                        Span::merge(spread_start, self.prev_span()),
947                    ));
948                } else {
949                    self.pos = saved_pos;
950                    args.push(self.parse_expression()?);
951                }
952            } else {
953                args.push(self.parse_expression()?);
954            }
955            self.skip_newlines();
956            if self.check(&TokenKind::Comma) {
957                self.advance();
958                self.skip_newlines();
959            }
960        }
961        Ok(args)
962    }
963}