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_ternary()?;
70        self.consume(&TokenKind::Colon, ":")?;
71        let false_val = self.parse_ternary()?;
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                if self.question_starts_ternary_branch() {
507                    break;
508                }
509                if matches!(self.peek_kind_at(1), Some(TokenKind::LBracket)) {
510                    let start = expr.span;
511                    self.advance(); // consume ?
512                    self.advance(); // consume [
513                    let index = self.parse_expression()?;
514                    self.consume(&TokenKind::RBracket, "]")?;
515                    expr = spanned(
516                        Node::OptionalSubscriptAccess {
517                            object: Box::new(expr),
518                            index: Box::new(index),
519                        },
520                        Span::merge(start, self.prev_span()),
521                    );
522                    continue;
523                }
524                let start = expr.span;
525                self.advance();
526                expr = spanned(
527                    Node::TryOperator {
528                        operand: Box::new(expr),
529                    },
530                    Span::merge(start, self.prev_span()),
531                );
532            } else {
533                break;
534            }
535        }
536
537        Ok(expr)
538    }
539
540    fn question_starts_ternary_branch(&self) -> bool {
541        self.peek_kind_at(1)
542            .is_some_and(Self::token_starts_ternary_branch)
543            && self.question_has_top_level_ternary_colon()
544    }
545
546    fn token_starts_ternary_branch(kind: &TokenKind) -> bool {
547        matches!(
548            kind,
549            TokenKind::Identifier(_)
550                | TokenKind::IntLiteral(_)
551                | TokenKind::FloatLiteral(_)
552                | TokenKind::StringLiteral(_)
553                | TokenKind::RawStringLiteral(_)
554                | TokenKind::InterpolatedString(_)
555                | TokenKind::True
556                | TokenKind::False
557                | TokenKind::Nil
558                | TokenKind::LParen
559                | TokenKind::LBracket
560                | TokenKind::LBrace
561                | TokenKind::Not
562                | TokenKind::Minus
563                | TokenKind::Fn
564                | TokenKind::If
565                | TokenKind::Match
566                | TokenKind::Try
567                | TokenKind::Spawn
568                | TokenKind::Parallel
569                | TokenKind::Retry
570                | TokenKind::Deadline
571                | TokenKind::RequestApproval
572                | TokenKind::DualControl
573                | TokenKind::AskUser
574                | TokenKind::EscalateTo
575                | TokenKind::DurationLiteral(_)
576        )
577    }
578
579    fn question_has_top_level_ternary_colon(&self) -> bool {
580        let mut delimiter_depth = 0usize;
581        for (pos, token) in self.tokens.iter().enumerate().skip(self.pos + 1) {
582            if delimiter_depth == 0 {
583                match token.kind {
584                    TokenKind::Colon => return true,
585                    TokenKind::Newline => {
586                        if self.next_non_newline_continues_ternary_branch(pos + 1) {
587                            continue;
588                        }
589                        return false;
590                    }
591                    TokenKind::RParen
592                    | TokenKind::RBracket
593                    | TokenKind::RBrace
594                    | TokenKind::Eof => {
595                        return false;
596                    }
597                    _ => {}
598                }
599            }
600
601            match token.kind {
602                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => {
603                    delimiter_depth += 1;
604                }
605                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
606                    delimiter_depth = delimiter_depth.saturating_sub(1);
607                }
608                TokenKind::Eof => return false,
609                _ => {}
610            }
611        }
612        false
613    }
614
615    fn next_non_newline_continues_ternary_branch(&self, start_pos: usize) -> bool {
616        let Some(kind) = self
617            .tokens
618            .iter()
619            .skip(start_pos)
620            .find(|token| token.kind != TokenKind::Newline)
621            .map(|token| &token.kind)
622        else {
623            return false;
624        };
625        matches!(
626            kind,
627            TokenKind::Colon
628                | TokenKind::Plus
629                | TokenKind::Star
630                | TokenKind::Slash
631                | TokenKind::Percent
632                | TokenKind::Pow
633                | TokenKind::And
634                | TokenKind::Or
635                | TokenKind::Eq
636                | TokenKind::Neq
637                | TokenKind::Lt
638                | TokenKind::Gt
639                | TokenKind::Lte
640                | TokenKind::Gte
641                | TokenKind::NilCoal
642                | TokenKind::Pipe
643                | TokenKind::Dot
644                | TokenKind::QuestionDot
645        )
646    }
647
648    pub(super) fn parse_primary(&mut self) -> Result<SNode, ParserError> {
649        let tok = self.current().ok_or_else(|| ParserError::UnexpectedEof {
650            expected: "expression".into(),
651            span: self.prev_span(),
652        })?;
653        let start = self.current_span();
654
655        match &tok.kind {
656            TokenKind::StringLiteral(s) => {
657                let s = s.clone();
658                self.advance();
659                Ok(spanned(
660                    Node::StringLiteral(s),
661                    Span::merge(start, self.prev_span()),
662                ))
663            }
664            TokenKind::RawStringLiteral(s) => {
665                let s = s.clone();
666                self.advance();
667                Ok(spanned(
668                    Node::RawStringLiteral(s),
669                    Span::merge(start, self.prev_span()),
670                ))
671            }
672            TokenKind::InterpolatedString(segments) => {
673                let segments = segments.clone();
674                self.advance();
675                Ok(spanned(
676                    Node::InterpolatedString(segments),
677                    Span::merge(start, self.prev_span()),
678                ))
679            }
680            TokenKind::IntLiteral(n) => {
681                let n = *n;
682                self.advance();
683                Ok(spanned(
684                    Node::IntLiteral(n),
685                    Span::merge(start, self.prev_span()),
686                ))
687            }
688            TokenKind::FloatLiteral(n) => {
689                let n = *n;
690                self.advance();
691                Ok(spanned(
692                    Node::FloatLiteral(n),
693                    Span::merge(start, self.prev_span()),
694                ))
695            }
696            TokenKind::True => {
697                self.advance();
698                Ok(spanned(
699                    Node::BoolLiteral(true),
700                    Span::merge(start, self.prev_span()),
701                ))
702            }
703            TokenKind::False => {
704                self.advance();
705                Ok(spanned(
706                    Node::BoolLiteral(false),
707                    Span::merge(start, self.prev_span()),
708                ))
709            }
710            TokenKind::Nil => {
711                self.advance();
712                Ok(spanned(
713                    Node::NilLiteral,
714                    Span::merge(start, self.prev_span()),
715                ))
716            }
717            TokenKind::Identifier(name)
718                if name == "cost_route" && self.peek_kind() == Some(&TokenKind::LBrace) =>
719            {
720                self.parse_cost_route()
721            }
722            TokenKind::Identifier(name) => {
723                let name = name.clone();
724                self.advance();
725                Ok(spanned(
726                    Node::Identifier(name),
727                    Span::merge(start, self.prev_span()),
728                ))
729            }
730            TokenKind::LParen => {
731                self.advance();
732                let expr = self.parse_expression()?;
733                self.consume(&TokenKind::RParen, ")")?;
734                Ok(expr)
735            }
736            TokenKind::LBracket => self.parse_list_literal(),
737            TokenKind::LBrace => self.parse_dict_or_closure(),
738            TokenKind::Parallel => self.parse_parallel(),
739            TokenKind::Retry => self.parse_retry(),
740            TokenKind::If => self.parse_if_else(),
741            TokenKind::Spawn => self.parse_spawn_expr(),
742            TokenKind::RequestApproval => self.parse_hitl_expr(HitlKind::RequestApproval),
743            TokenKind::DualControl => self.parse_hitl_expr(HitlKind::DualControl),
744            TokenKind::AskUser => self.parse_hitl_expr(HitlKind::AskUser),
745            TokenKind::EscalateTo => self.parse_hitl_expr(HitlKind::EscalateTo),
746            TokenKind::DurationLiteral(ms) => {
747                let ms = *ms;
748                self.advance();
749                Ok(spanned(
750                    Node::DurationLiteral(ms),
751                    Span::merge(start, self.prev_span()),
752                ))
753            }
754            TokenKind::Deadline => self.parse_deadline(),
755            TokenKind::Try => self.parse_try_catch(),
756            TokenKind::Match => self.parse_match(),
757            TokenKind::Fn => self.parse_fn_expr(),
758            // Heredoc `<<TAG ... TAG` is only valid inside LLM tool-call JSON;
759            // in source-position expressions, redirect authors to triple-quoted strings.
760            TokenKind::Lt
761                if matches!(self.peek_kind(), Some(&TokenKind::Lt))
762                    && matches!(self.peek_kind_at(2), Some(TokenKind::Identifier(_))) =>
763            {
764                Err(ParserError::Unexpected {
765                    got: "`<<` heredoc-like syntax".to_string(),
766                    expected: "an expression — heredocs are only valid \
767                               inside LLM tool-call argument JSON; \
768                               for multiline strings in source code use \
769                               triple-quoted `\"\"\"...\"\"\"`"
770                        .to_string(),
771                    span: start,
772                })
773            }
774            _ => Err(self.error("expression")),
775        }
776    }
777
778    /// Anonymous function `fn(params) { body }`. Sets `fn_syntax: true` on the
779    /// Closure so the formatter can round-trip the original syntax.
780    pub(super) fn parse_fn_expr(&mut self) -> Result<SNode, ParserError> {
781        let start = self.current_span();
782        self.consume(&TokenKind::Fn, "fn")?;
783        self.consume(&TokenKind::LParen, "(")?;
784        let params = self.parse_typed_param_list()?;
785        self.consume(&TokenKind::RParen, ")")?;
786        self.consume(&TokenKind::LBrace, "{")?;
787        let body = self.parse_block()?;
788        self.consume(&TokenKind::RBrace, "}")?;
789        Ok(spanned(
790            Node::Closure {
791                params,
792                body,
793                fn_syntax: true,
794            },
795            Span::merge(start, self.prev_span()),
796        ))
797    }
798
799    pub(super) fn parse_spawn_expr(&mut self) -> Result<SNode, ParserError> {
800        let start = self.current_span();
801        self.consume(&TokenKind::Spawn, "spawn")?;
802        self.consume(&TokenKind::LBrace, "{")?;
803        let body = self.parse_block()?;
804        self.consume(&TokenKind::RBrace, "}")?;
805        Ok(spanned(
806            Node::SpawnExpr { body },
807            Span::merge(start, self.prev_span()),
808        ))
809    }
810
811    /// Parse a first-class HITL primitive: one of `request_approval`,
812    /// `dual_control`, `ask_user`, `escalate_to`. The keyword has
813    /// already been peeked at; this method consumes it plus the
814    /// parenthesized argument list.
815    ///
816    /// Each argument is either positional (`expr`) or named
817    /// (`name: expr`). The grammar accepts the existing positional
818    /// invocation form so existing scripts and conformance tests
819    /// (e.g. `request_approval("deploy", {quorum: 2, ...})`) keep
820    /// working unchanged. Argument validation (required names,
821    /// duplicates, ordering) is performed by the typechecker.
822    pub(super) fn parse_hitl_expr(&mut self, kind: HitlKind) -> Result<SNode, ParserError> {
823        let start = self.current_span();
824        let kw_token = match kind {
825            HitlKind::RequestApproval => TokenKind::RequestApproval,
826            HitlKind::DualControl => TokenKind::DualControl,
827            HitlKind::AskUser => TokenKind::AskUser,
828            HitlKind::EscalateTo => TokenKind::EscalateTo,
829        };
830        self.consume(&kw_token, kind.as_keyword())?;
831        self.consume(&TokenKind::LParen, "(")?;
832        self.skip_newlines();
833
834        let mut args: Vec<HitlArg> = Vec::new();
835        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
836            let arg_start = self.current_span();
837            // Look ahead two tokens to detect `identifier ":"`. The
838            // identifier itself is parsed as part of the expression so
839            // we keep the dispatch simple: peek for `Identifier` then
840            // a `Colon` to identify a named argument.
841            // `peek_kind_at(0)` is the current token; `peek_kind_at(1)`
842            // is one ahead. A named-arg slot starts with `ident :`.
843            let is_named = matches!(
844                (self.peek_kind_at(0), self.peek_kind_at(1)),
845                (Some(TokenKind::Identifier(_)), Some(TokenKind::Colon))
846            );
847            let (name, value) = if is_named {
848                let Some(TokenKind::Identifier(raw)) = self.peek_kind_at(0).cloned() else {
849                    unreachable!("named arg dispatch already matched Identifier token")
850                };
851                self.advance();
852                self.consume(&TokenKind::Colon, ":")?;
853                self.skip_newlines();
854                let value = self.parse_expression()?;
855                (Some(raw), value)
856            } else {
857                (None, self.parse_expression()?)
858            };
859            let arg_span = Span::merge(arg_start, self.prev_span());
860            args.push(HitlArg {
861                name,
862                value,
863                span: arg_span,
864            });
865            self.skip_newlines();
866            if self.check(&TokenKind::Comma) {
867                self.advance();
868                self.skip_newlines();
869            } else {
870                break;
871            }
872        }
873
874        self.skip_newlines();
875        self.consume(&TokenKind::RParen, ")")?;
876        Ok(spanned(
877            Node::HitlExpr { kind, args },
878            Span::merge(start, self.prev_span()),
879        ))
880    }
881
882    pub(super) fn parse_list_literal(&mut self) -> Result<SNode, ParserError> {
883        let start = self.current_span();
884        self.consume(&TokenKind::LBracket, "[")?;
885        let mut elements = Vec::new();
886        self.skip_newlines();
887
888        while !self.is_at_end() && !self.check(&TokenKind::RBracket) {
889            if self.check(&TokenKind::Dot) {
890                let saved_pos = self.pos;
891                self.advance();
892                if self.check(&TokenKind::Dot) {
893                    self.advance();
894                    self.consume(&TokenKind::Dot, ".")?;
895                    let spread_start = self.tokens[saved_pos].span;
896                    let expr = self.parse_expression()?;
897                    elements.push(spanned(
898                        Node::Spread(Box::new(expr)),
899                        Span::merge(spread_start, self.prev_span()),
900                    ));
901                } else {
902                    self.pos = saved_pos;
903                    elements.push(self.parse_expression()?);
904                }
905            } else {
906                elements.push(self.parse_expression()?);
907            }
908            self.skip_newlines();
909            if self.check(&TokenKind::Comma) {
910                self.advance();
911                self.skip_newlines();
912            }
913        }
914
915        self.consume(&TokenKind::RBracket, "]")?;
916        Ok(spanned(
917            Node::ListLiteral(elements),
918            Span::merge(start, self.prev_span()),
919        ))
920    }
921
922    pub(super) fn parse_dict_or_closure(&mut self) -> Result<SNode, ParserError> {
923        let start = self.current_span();
924        self.consume(&TokenKind::LBrace, "{")?;
925        self.skip_newlines();
926
927        if self.check(&TokenKind::RBrace) {
928            self.advance();
929            return Ok(spanned(
930                Node::DictLiteral(Vec::new()),
931                Span::merge(start, self.prev_span()),
932            ));
933        }
934
935        // Scan for `->` before the closing `}` to distinguish closure from dict.
936        let saved = self.pos;
937        if self.is_closure_lookahead() {
938            self.pos = saved;
939            return self.parse_closure_body(start);
940        }
941        self.pos = saved;
942        self.parse_dict_literal(start)
943    }
944
945    /// After seeing `Identifier {`, decide whether the brace block is a
946    /// struct-construction field list rather than a control-flow block.
947    /// Struct fields always start with `name:` / `"name":` or `}`.
948    pub(super) fn is_struct_construct_lookahead(&self, struct_name: &str) -> bool {
949        if !struct_name
950            .chars()
951            .next()
952            .is_some_and(|ch| ch.is_uppercase())
953        {
954            return false;
955        }
956
957        let mut offset = 1;
958        while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
959            offset += 1;
960        }
961
962        match self.peek_kind_at(offset) {
963            Some(TokenKind::RBrace) => true,
964            Some(TokenKind::Identifier(_)) | Some(TokenKind::StringLiteral(_)) => {
965                offset += 1;
966                while matches!(self.peek_kind_at(offset), Some(TokenKind::Newline)) {
967                    offset += 1;
968                }
969                matches!(self.peek_kind_at(offset), Some(TokenKind::Colon))
970            }
971            _ => false,
972        }
973    }
974
975    /// Caller must save/restore `pos`; this advances while scanning.
976    pub(super) fn is_closure_lookahead(&mut self) -> bool {
977        let mut depth = 0;
978        while !self.is_at_end() {
979            if let Some(tok) = self.current() {
980                match &tok.kind {
981                    TokenKind::Arrow if depth == 0 => return true,
982                    TokenKind::LBrace | TokenKind::LParen | TokenKind::LBracket => depth += 1,
983                    TokenKind::RBrace if depth == 0 => return false,
984                    TokenKind::RBrace => depth -= 1,
985                    TokenKind::RParen | TokenKind::RBracket if depth > 0 => depth -= 1,
986                    _ => {}
987                }
988                self.advance();
989            } else {
990                return false;
991            }
992        }
993        false
994    }
995
996    /// Parse closure params and body (after opening { has been consumed).
997    pub(super) fn parse_closure_body(&mut self, start: Span) -> Result<SNode, ParserError> {
998        let params = self.parse_typed_param_list_until_arrow()?;
999        self.consume(&TokenKind::Arrow, "->")?;
1000        let body = self.parse_block()?;
1001        self.consume(&TokenKind::RBrace, "}")?;
1002        Ok(spanned(
1003            Node::Closure {
1004                params,
1005                body,
1006                fn_syntax: false,
1007            },
1008            Span::merge(start, self.prev_span()),
1009        ))
1010    }
1011
1012    /// Parse typed params until we see ->. Handles: `x`, `x: int`, `x, y`, `x: int, y: string`.
1013    pub(super) fn parse_typed_param_list_until_arrow(
1014        &mut self,
1015    ) -> Result<Vec<TypedParam>, ParserError> {
1016        self.parse_typed_params_until(|tok| tok == &TokenKind::Arrow)
1017    }
1018
1019    pub(super) fn parse_dict_literal(&mut self, start: Span) -> Result<SNode, ParserError> {
1020        let entries = self.parse_dict_entries()?;
1021        Ok(spanned(
1022            Node::DictLiteral(entries),
1023            Span::merge(start, self.prev_span()),
1024        ))
1025    }
1026
1027    pub(super) fn parse_dict_entries(&mut self) -> Result<Vec<DictEntry>, ParserError> {
1028        let mut entries = Vec::new();
1029        self.skip_newlines();
1030
1031        while !self.is_at_end() && !self.check(&TokenKind::RBrace) {
1032            if self.check(&TokenKind::Dot) {
1033                let saved_pos = self.pos;
1034                self.advance();
1035                if self.check(&TokenKind::Dot) {
1036                    self.advance();
1037                    if self.check(&TokenKind::Dot) {
1038                        self.advance();
1039                        let spread_start = self.tokens[saved_pos].span;
1040                        let expr = self.parse_expression()?;
1041                        entries.push(DictEntry {
1042                            key: spanned(Node::NilLiteral, spread_start),
1043                            value: spanned(
1044                                Node::Spread(Box::new(expr)),
1045                                Span::merge(spread_start, self.prev_span()),
1046                            ),
1047                        });
1048                        self.skip_newlines();
1049                        if self.check(&TokenKind::Comma) {
1050                            self.advance();
1051                            self.skip_newlines();
1052                        }
1053                        continue;
1054                    }
1055                    self.pos = saved_pos;
1056                } else {
1057                    self.pos = saved_pos;
1058                }
1059            }
1060            let key = if self.check(&TokenKind::LBracket) {
1061                self.advance();
1062                let k = self.parse_expression()?;
1063                self.consume(&TokenKind::RBracket, "]")?;
1064                k
1065            } else if matches!(
1066                self.current().map(|t| &t.kind),
1067                Some(TokenKind::StringLiteral(_))
1068            ) {
1069                let key_span = self.current_span();
1070                let name =
1071                    if let Some(TokenKind::StringLiteral(s)) = self.current().map(|t| &t.kind) {
1072                        s.clone()
1073                    } else {
1074                        unreachable!()
1075                    };
1076                self.advance();
1077                spanned(Node::StringLiteral(name), key_span)
1078            } else {
1079                let key_span = self.current_span();
1080                let name = self.consume_identifier_or_keyword("dict key")?;
1081                spanned(Node::StringLiteral(name), key_span)
1082            };
1083            self.consume(&TokenKind::Colon, ":")?;
1084            let value = self.parse_expression()?;
1085            entries.push(DictEntry { key, value });
1086            self.skip_newlines();
1087            if self.check(&TokenKind::Comma) {
1088                self.advance();
1089                self.skip_newlines();
1090            }
1091        }
1092
1093        self.consume(&TokenKind::RBrace, "}")?;
1094        Ok(entries)
1095    }
1096
1097    /// Parse untyped parameter list (for pipelines, overrides).
1098    pub(super) fn parse_param_list(&mut self) -> Result<Vec<String>, ParserError> {
1099        let mut params = Vec::new();
1100        self.skip_newlines();
1101
1102        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1103            params.push(self.consume_identifier("parameter name")?);
1104            if self.check(&TokenKind::Comma) {
1105                self.advance();
1106                self.skip_newlines();
1107            }
1108        }
1109        Ok(params)
1110    }
1111
1112    /// Parse typed parameter list (for fn declarations).
1113    pub(super) fn parse_typed_param_list(&mut self) -> Result<Vec<TypedParam>, ParserError> {
1114        self.parse_typed_params_until(|tok| tok == &TokenKind::RParen)
1115    }
1116
1117    /// Shared implementation: parse typed params with optional defaults until
1118    /// a terminator token is reached.
1119    pub(super) fn parse_typed_params_until(
1120        &mut self,
1121        is_terminator: impl Fn(&TokenKind) -> bool,
1122    ) -> Result<Vec<TypedParam>, ParserError> {
1123        let mut params = Vec::new();
1124        let mut seen_default = false;
1125        self.skip_newlines();
1126
1127        while !self.is_at_end() {
1128            if let Some(tok) = self.current() {
1129                if is_terminator(&tok.kind) {
1130                    break;
1131                }
1132            } else {
1133                break;
1134            }
1135            let is_rest = if self.check(&TokenKind::Dot) {
1136                let p1 = self.pos + 1;
1137                let p2 = self.pos + 2;
1138                let is_ellipsis = p1 < self.tokens.len()
1139                    && p2 < self.tokens.len()
1140                    && self.tokens[p1].kind == TokenKind::Dot
1141                    && self.tokens[p2].kind == TokenKind::Dot;
1142                if is_ellipsis {
1143                    self.advance();
1144                    self.advance();
1145                    self.advance();
1146                    true
1147                } else {
1148                    false
1149                }
1150            } else {
1151                false
1152            };
1153            let name = self.consume_identifier("parameter name")?;
1154            let type_expr = self.try_parse_type_annotation()?;
1155            let default_value = if self.check(&TokenKind::Assign) {
1156                self.advance();
1157                seen_default = true;
1158                Some(Box::new(self.parse_expression()?))
1159            } else {
1160                if seen_default && !is_rest {
1161                    return Err(self.error(
1162                        "Required parameter cannot follow a parameter with a default value",
1163                    ));
1164                }
1165                None
1166            };
1167            if is_rest
1168                && !is_terminator(
1169                    &self
1170                        .current()
1171                        .map(|t| t.kind.clone())
1172                        .unwrap_or(TokenKind::Eof),
1173                )
1174            {
1175                return Err(self.error("Rest parameter must be the last parameter"));
1176            }
1177            params.push(TypedParam {
1178                name,
1179                type_expr,
1180                default_value,
1181                rest: is_rest,
1182            });
1183            if self.check(&TokenKind::Comma) {
1184                self.advance();
1185                self.skip_newlines();
1186            }
1187        }
1188        Ok(params)
1189    }
1190
1191    pub(super) fn parse_arg_list(&mut self) -> Result<Vec<SNode>, ParserError> {
1192        let mut args = Vec::new();
1193        self.skip_newlines();
1194
1195        while !self.is_at_end() && !self.check(&TokenKind::RParen) {
1196            if self.check(&TokenKind::Dot) {
1197                let saved_pos = self.pos;
1198                self.advance();
1199                if self.check(&TokenKind::Dot) {
1200                    self.advance();
1201                    self.consume(&TokenKind::Dot, ".")?;
1202                    let spread_start = self.tokens[saved_pos].span;
1203                    let expr = self.parse_expression()?;
1204                    args.push(spanned(
1205                        Node::Spread(Box::new(expr)),
1206                        Span::merge(spread_start, self.prev_span()),
1207                    ));
1208                } else {
1209                    self.pos = saved_pos;
1210                    args.push(self.parse_expression()?);
1211                }
1212            } else {
1213                args.push(self.parse_expression()?);
1214            }
1215            self.skip_newlines();
1216            if self.check(&TokenKind::Comma) {
1217                self.advance();
1218                self.skip_newlines();
1219            }
1220        }
1221        Ok(args)
1222    }
1223}