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::RequestApproval => self.parse_hitl_expr(HitlKind::RequestApproval),
663            TokenKind::DualControl => self.parse_hitl_expr(HitlKind::DualControl),
664            TokenKind::AskUser => self.parse_hitl_expr(HitlKind::AskUser),
665            TokenKind::EscalateTo => self.parse_hitl_expr(HitlKind::EscalateTo),
666            TokenKind::DurationLiteral(ms) => {
667                let ms = *ms;
668                self.advance();
669                Ok(spanned(
670                    Node::DurationLiteral(ms),
671                    Span::merge(start, self.prev_span()),
672                ))
673            }
674            TokenKind::Deadline => self.parse_deadline(),
675            TokenKind::Try => self.parse_try_catch(),
676            TokenKind::Match => self.parse_match(),
677            TokenKind::Fn => self.parse_fn_expr(),
678            // Heredoc `<<TAG ... TAG` is only valid inside LLM tool-call JSON;
679            // in source-position expressions, redirect authors to triple-quoted strings.
680            TokenKind::Lt
681                if matches!(self.peek_kind(), Some(&TokenKind::Lt))
682                    && matches!(self.peek_kind_at(2), Some(TokenKind::Identifier(_))) =>
683            {
684                Err(ParserError::Unexpected {
685                    got: "`<<` heredoc-like syntax".to_string(),
686                    expected: "an expression — heredocs are only valid \
687                               inside LLM tool-call argument JSON; \
688                               for multiline strings in source code use \
689                               triple-quoted `\"\"\"...\"\"\"`"
690                        .to_string(),
691                    span: start,
692                })
693            }
694            _ => Err(self.error("expression")),
695        }
696    }
697
698    /// Anonymous function `fn(params) { body }`. Sets `fn_syntax: true` on the
699    /// Closure so the formatter can round-trip the original syntax.
700    pub(super) fn parse_fn_expr(&mut self) -> Result<SNode, ParserError> {
701        let start = self.current_span();
702        self.consume(&TokenKind::Fn, "fn")?;
703        self.consume(&TokenKind::LParen, "(")?;
704        let params = self.parse_typed_param_list()?;
705        self.consume(&TokenKind::RParen, ")")?;
706        self.consume(&TokenKind::LBrace, "{")?;
707        let body = self.parse_block()?;
708        self.consume(&TokenKind::RBrace, "}")?;
709        Ok(spanned(
710            Node::Closure {
711                params,
712                body,
713                fn_syntax: true,
714            },
715            Span::merge(start, self.prev_span()),
716        ))
717    }
718
719    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
720        let start = self.current_span();
721        self.consume(&TokenKind::Spawn, "spawn")?;
722        self.consume(&TokenKind::LBrace, "{")?;
723        let body = self.parse_block()?;
724        self.consume(&TokenKind::RBrace, "}")?;
725        Ok(spanned(
726            Node::SpawnExpr { body },
727            Span::merge(start, self.prev_span()),
728        ))
729    }
730
731    /// Parse a first-class HITL primitive: one of `request_approval`,
732    /// `dual_control`, `ask_user`, `escalate_to`. The keyword has
733    /// already been peeked at; this method consumes it plus the
734    /// parenthesized argument list.
735    ///
736    /// Each argument is either positional (`expr`) or named
737    /// (`name: expr`). The grammar accepts the existing positional
738    /// invocation form so existing scripts and conformance tests
739    /// (e.g. `request_approval("deploy", {quorum: 2, ...})`) keep
740    /// working unchanged. Argument validation (required names,
741    /// duplicates, ordering) is performed by the typechecker.
742    pub(super) fn parse_hitl_expr(&mut self, kind: HitlKind) -> Result<SNode, ParserError> {
743        let start = self.current_span();
744        let kw_token = match kind {
745            HitlKind::RequestApproval => TokenKind::RequestApproval,
746            HitlKind::DualControl => TokenKind::DualControl,
747            HitlKind::AskUser => TokenKind::AskUser,
748            HitlKind::EscalateTo => TokenKind::EscalateTo,
749        };
750        self.consume(&kw_token, kind.as_keyword())?;
751        self.consume(&TokenKind::LParen, "(")?;
752        self.skip_newlines();
753
754        let mut args: Vec<HitlArg> = Vec::new();
755        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
756            let arg_start = self.current_span();
757            // Look ahead two tokens to detect `identifier ":"`. The
758            // identifier itself is parsed as part of the expression so
759            // we keep the dispatch simple: peek for `Identifier` then
760            // a `Colon` to identify a named argument.
761            // `peek_kind_at(0)` is the current token; `peek_kind_at(1)`
762            // is one ahead. A named-arg slot starts with `ident :`.
763            let is_named = matches!(
764                (self.peek_kind_at(0), self.peek_kind_at(1)),
765                (Some(TokenKind::Identifier(_)), Some(TokenKind::Colon))
766            );
767            let (name, value) = if is_named {
768                let Some(TokenKind::Identifier(raw)) = self.peek_kind_at(0).cloned() else {
769                    unreachable!("named arg dispatch already matched Identifier token")
770                };
771                self.advance();
772                self.consume(&TokenKind::Colon, ":")?;
773                self.skip_newlines();
774                let value = self.parse_expression()?;
775                (Some(raw), value)
776            } else {
777                (None, self.parse_expression()?)
778            };
779            let arg_span = Span::merge(arg_start, self.prev_span());
780            args.push(HitlArg {
781                name,
782                value,
783                span: arg_span,
784            });
785            self.skip_newlines();
786            if self.check(&TokenKind::Comma) {
787                self.advance();
788                self.skip_newlines();
789            } else {
790                break;
791            }
792        }
793
794        self.skip_newlines();
795        self.consume(&TokenKind::RParen, ")")?;
796        Ok(spanned(
797            Node::HitlExpr { kind, args },
798            Span::merge(start, self.prev_span()),
799        ))
800    }
801
802    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
803        let start = self.current_span();
804        self.consume(&TokenKind::LBracket, "[")?;
805        let mut elements = Vec::new();
806        self.skip_newlines();
807
808        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
809            if self.check(&TokenKind::Dot) {
810                let saved_pos = self.pos;
811                self.advance();
812                if self.check(&TokenKind::Dot) {
813                    self.advance();
814                    self.consume(&TokenKind::Dot, ".")?;
815                    let spread_start = self.tokens[saved_pos].span;
816                    let expr = self.parse_expression()?;
817                    elements.push(spanned(
818                        Node::Spread(Box::new(expr)),
819                        Span::merge(spread_start, self.prev_span()),
820                    ));
821                } else {
822                    self.pos = saved_pos;
823                    elements.push(self.parse_expression()?);
824                }
825            } else {
826                elements.push(self.parse_expression()?);
827            }
828            self.skip_newlines();
829            if self.check(&TokenKind::Comma) {
830                self.advance();
831                self.skip_newlines();
832            }
833        }
834
835        self.consume(&TokenKind::RBracket, "]")?;
836        Ok(spanned(
837            Node::ListLiteral(elements),
838            Span::merge(start, self.prev_span()),
839        ))
840    }
841
842    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
843        let start = self.current_span();
844        self.consume(&TokenKind::LBrace, "{")?;
845        self.skip_newlines();
846
847        if self.check(&TokenKind::RBrace) {
848            self.advance();
849            return Ok(spanned(
850                Node::DictLiteral(Vec::new()),
851                Span::merge(start, self.prev_span()),
852            ));
853        }
854
855        // Scan for `->` before the closing `}` to distinguish closure from dict.
856        let saved = self.pos;
857        if self.is_closure_lookahead() {
858            self.pos = saved;
859            return self.parse_closure_body(start);
860        }
861        self.pos = saved;
862        self.parse_dict_literal(start)
863    }
864
865    /// After seeing `Identifier {`, decide whether the brace block is a
866    /// struct-construction field list rather than a control-flow block.
867    /// Struct fields always start with `name:` / `"name":` or `}`.
868    pub(super) fn is_struct_construct_lookahead(&self, struct_name: &str) -> bool {
869        if !struct_name
870            .chars()
871            .next()
872            .is_some_and(|ch| ch.is_uppercase())
873        {
874            return false;
875        }
876
877        let mut offset = 1;
878        while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
879            offset += 1;
880        }
881
882        match self.peek_kind_at(offset) {
883            Some(TokenKind::RBrace) => true,
884            Some(TokenKind::Identifier(_)) | Some(TokenKind::StringLiteral(_)) => {
885                offset += 1;
886                while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
887                    offset += 1;
888                }
889                matches!(self.peek_kind_at(offset), Some(TokenKind::Colon))
890            }
891            _ => false,
892        }
893    }
894
895    /// Caller must save/restore `pos`; this advances while scanning.
896    pub(super) fn is_closure_lookahead(&mut self) -> bool {
897        let mut depth = 0;
898        while !self.is_at_end() {
899            if let Some(tok) = self.current() {
900                match &tok.kind {
901                    TokenKind::Arrow if depth == 0 => return true,
902                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
903                    TokenKind::RBrace if depth == 0 => return false,
904                    TokenKind::RBrace => depth -= 1,
905                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
906                    _ => {}
907                }
908                self.advance();
909            } else {
910                return false;
911            }
912        }
913        false
914    }
915
916    /// Parse closure params and body (after opening { has been consumed).
917    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
918        let params = self.parse_typed_param_list_until_arrow()?;
919        self.consume(&TokenKind::Arrow, "->")?;
920        let body = self.parse_block()?;
921        self.consume(&TokenKind::RBrace, "}")?;
922        Ok(spanned(
923            Node::Closure {
924                params,
925                body,
926                fn_syntax: false,
927            },
928            Span::merge(start, self.prev_span()),
929        ))
930    }
931
932    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
933    pub(super) fn parse_typed_param_list_until_arrow(
934        &mut self,
935    ) -> Result<Vec<TypedParam>, ParserError> {
936        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
937    }
938
939    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
940        let entries = self.parse_dict_entries()?;
941        Ok(spanned(
942            Node::DictLiteral(entries),
943            Span::merge(start, self.prev_span()),
944        ))
945    }
946
947    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
948        let mut entries = Vec::new();
949        self.skip_newlines();
950
951        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
952            if self.check(&TokenKind::Dot) {
953                let saved_pos = self.pos;
954                self.advance();
955                if self.check(&TokenKind::Dot) {
956                    self.advance();
957                    if self.check(&TokenKind::Dot) {
958                        self.advance();
959                        let spread_start = self.tokens[saved_pos].span;
960                        let expr = self.parse_expression()?;
961                        entries.push(DictEntry {
962                            key: spanned(Node::NilLiteral, spread_start),
963                            value: spanned(
964                                Node::Spread(Box::new(expr)),
965                                Span::merge(spread_start, self.prev_span()),
966                            ),
967                        });
968                        self.skip_newlines();
969                        if self.check(&TokenKind::Comma) {
970                            self.advance();
971                            self.skip_newlines();
972                        }
973                        continue;
974                    }
975                    self.pos = saved_pos;
976                } else {
977                    self.pos = saved_pos;
978                }
979            }
980            let key = if self.check(&TokenKind::LBracket) {
981                self.advance();
982                let k = self.parse_expression()?;
983                self.consume(&TokenKind::RBracket, "]")?;
984                k
985            } else if matches!(
986                self.current().map(|t| &t.kind),
987                Some(TokenKind::StringLiteral(_))
988            ) {
989                let key_span = self.current_span();
990                let name =
991                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
992                        s.clone()
993                    } else {
994                        unreachable!()
995                    };
996                self.advance();
997                spanned(Node::StringLiteral(name), key_span)
998            } else {
999                let key_span = self.current_span();
1000                let name = self.consume_identifier_or_keyword("dict key")?;
1001                spanned(Node::StringLiteral(name), key_span)
1002            };
1003            self.consume(&TokenKind::Colon, ":")?;
1004            let value = self.parse_expression()?;
1005            entries.push(DictEntry { key, value });
1006            self.skip_newlines();
1007            if self.check(&TokenKind::Comma) {
1008                self.advance();
1009                self.skip_newlines();
1010            }
1011        }
1012
1013        self.consume(&TokenKind::RBrace, "}")?;
1014        Ok(entries)
1015    }
1016
1017    /// Parse untyped parameter list (for pipelines, overrides).
1018    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
1019        let mut params = Vec::new();
1020        self.skip_newlines();
1021
1022        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1023            params.push(self.consume_identifier("parameter name")?);
1024            if self.check(&TokenKind::Comma) {
1025                self.advance();
1026                self.skip_newlines();
1027            }
1028        }
1029        Ok(params)
1030    }
1031
1032    /// Parse typed parameter list (for fn declarations).
1033    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
1034        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
1035    }
1036
1037    /// Shared implementation: parse typed params with optional defaults until
1038    /// a terminator token is reached.
1039    pub(super) fn parse_typed_params_until(
1040        &mut self,
1041        is_terminator: impl Fn(&TokenKind) -> bool,
1042    ) -> Result<Vec<TypedParam>, ParserError> {
1043        let mut params = Vec::new();
1044        let mut seen_default = false;
1045        self.skip_newlines();
1046
1047        while !self.is_at_end() {
1048            if let Some(tok) = self.current() {
1049                if is_terminator(&tok.kind) {
1050                    break;
1051                }
1052            } else {
1053                break;
1054            }
1055            let is_rest = if self.check(&TokenKind::Dot) {
1056                let p1 = self.pos + 1;
1057                let p2 = self.pos + 2;
1058                let is_ellipsis = p1 < self.tokens.len()
1059                    && p2 < self.tokens.len()
1060                    && self.tokens[p1].kind == TokenKind::Dot
1061                    && self.tokens[p2].kind == TokenKind::Dot;
1062                if is_ellipsis {
1063                    self.advance();
1064                    self.advance();
1065                    self.advance();
1066                    true
1067                } else {
1068                    false
1069                }
1070            } else {
1071                false
1072            };
1073            let name = self.consume_identifier("parameter name")?;
1074            let type_expr = self.try_parse_type_annotation()?;
1075            let default_value = if self.check(&TokenKind::Assign) {
1076                self.advance();
1077                seen_default = true;
1078                Some(Box::new(self.parse_expression()?))
1079            } else {
1080                if seen_default && !is_rest {
1081                    return Err(self.error(
1082                        "Required parameter cannot follow a parameter with a default value",
1083                    ));
1084                }
1085                None
1086            };
1087            if is_rest
1088                && !is_terminator(
1089                    &self
1090                        .current()
1091                        .map(|t| t.kind.clone())
1092                        .unwrap_or(TokenKind::Eof),
1093                )
1094            {
1095                return Err(self.error("Rest parameter must be the last parameter"));
1096            }
1097            params.push(TypedParam {
1098                name,
1099                type_expr,
1100                default_value,
1101                rest: is_rest,
1102            });
1103            if self.check(&TokenKind::Comma) {
1104                self.advance();
1105                self.skip_newlines();
1106            }
1107        }
1108        Ok(params)
1109    }
1110
1111    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
1112        let mut args = Vec::new();
1113        self.skip_newlines();
1114
1115        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1116            if self.check(&TokenKind::Dot) {
1117                let saved_pos = self.pos;
1118                self.advance();
1119                if self.check(&TokenKind::Dot) {
1120                    self.advance();
1121                    self.consume(&TokenKind::Dot, ".")?;
1122                    let spread_start = self.tokens[saved_pos].span;
1123                    let expr = self.parse_expression()?;
1124                    args.push(spanned(
1125                        Node::Spread(Box::new(expr)),
1126                        Span::merge(spread_start, self.prev_span()),
1127                    ));
1128                } else {
1129                    self.pos = saved_pos;
1130                    args.push(self.parse_expression()?);
1131                }
1132            } else {
1133                args.push(self.parse_expression()?);
1134            }
1135            self.skip_newlines();
1136            if self.check(&TokenKind::Comma) {
1137                self.advance();
1138                self.skip_newlines();
1139            }
1140        }
1141        Ok(args)
1142    }
1143}