Skip to main content

harn_parser/parser/
expressions.rs

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