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