Skip to main content

stryke/
parser.rs

1use crate::ast::*;
2use crate::error::{ErrorKind, PerlError, PerlResult};
3use crate::interpreter::Interpreter;
4use crate::lexer::{Lexer, LITERAL_DOLLAR_IN_DQUOTE};
5use crate::token::Token;
6
7/// True when `[` after `expr` is chained array access (`$r->{k}[0]`, `$a[1][2]`, `$$r[0]`).
8/// False for `(sort ...)[0]` / `@{ ... }[i]` — those slice a list value, not an array ref container.
9fn postfix_lbracket_is_arrow_container(expr: &Expr) -> bool {
10    matches!(
11        expr.kind,
12        ExprKind::ArrayElement { .. }
13            | ExprKind::HashElement { .. }
14            | ExprKind::ArrowDeref { .. }
15            | ExprKind::Deref {
16                kind: Sigil::Scalar,
17                ..
18            }
19    )
20}
21
22fn destructure_stmt_from_var_decls(keyword: &str, decls: Vec<VarDecl>, line: usize) -> Statement {
23    let kind = match keyword {
24        "my" => StmtKind::My(decls),
25        "mysync" => StmtKind::MySync(decls),
26        "our" => StmtKind::Our(decls),
27        "local" => StmtKind::Local(decls),
28        "state" => StmtKind::State(decls),
29        _ => unreachable!("parse_my_our_local keyword"),
30    };
31    Statement {
32        label: None,
33        kind,
34        line,
35    }
36}
37
38fn destructure_stmt_die_string(line: usize, msg: &str) -> Statement {
39    Statement {
40        label: None,
41        kind: StmtKind::Expression(Expr {
42            kind: ExprKind::Die(vec![Expr {
43                kind: ExprKind::String(msg.to_string()),
44                line,
45            }]),
46            line,
47        }),
48        line,
49    }
50}
51
52fn destructure_stmt_unless_die(line: usize, cond: Expr, msg: &str) -> Statement {
53    Statement {
54        label: None,
55        kind: StmtKind::Unless {
56            condition: cond,
57            body: vec![destructure_stmt_die_string(line, msg)],
58            else_block: None,
59        },
60        line,
61    }
62}
63
64fn destructure_expr_scalar_tmp(name: &str, line: usize) -> Expr {
65    Expr {
66        kind: ExprKind::ScalarVar(name.to_string()),
67        line,
68    }
69}
70
71fn destructure_expr_array_len(tmp: &str, line: usize) -> Expr {
72    Expr {
73        kind: ExprKind::Deref {
74            expr: Box::new(destructure_expr_scalar_tmp(tmp, line)),
75            kind: Sigil::Array,
76        },
77        line,
78    }
79}
80
81pub struct Parser {
82    tokens: Vec<(Token, usize)>,
83    pos: usize,
84    /// Monotonic slot id for `rate_limit(...)` sliding-window state in the interpreter.
85    next_rate_limit_slot: u32,
86    /// When > 0, `expr` `(` is not parsed as [`ExprKind::IndirectCall`] — e.g. `sort $k (1)` must
87    /// treat `(1)` as the sort list, not `$k(1)`.
88    suppress_indirect_paren_call: u32,
89    /// When > 0, the current expression is being parsed as the RHS of `|>`
90    /// (pipe-forward). Builtins that normally require a list/string/second arg
91    /// (`map`, `grep`, `sort`, `join`, `reverse` / `reversed`, `split`, …) may accept a
92    /// placeholder when this flag is set, because [`Self::pipe_forward_apply`]
93    /// will substitute the piped value in afterwards.
94    pipe_rhs_depth: u32,
95    /// When > 0, [`Self::parse_pipe_forward`] will **not** consume a trailing `|>`
96    /// and leaves it for an outer parser instead. Bumped while parsing paren-less
97    /// arg lists (`parse_list_until_terminator`, paren-less method args, `map`/`grep`
98    /// LIST, …) so `@a |> head 2 |> join "-"` chains left-associatively as
99    /// `(@a |> head 2) |> join "-"` instead of `head` swallowing the outer `|>`
100    /// as part of its first arg. Reset to 0 on entry to any parenthesized
101    /// arg list (`parse_arg_list`) so `head(2 |> foo, 3)` still works.
102    no_pipe_forward_depth: u32,
103    /// When > 0, `{` after a scalar / scalar deref is not `%hash{key}` / `->{}`, so
104    /// `if let` / `while let` scrutinees can be followed by `{ ... }`.
105    suppress_scalar_hash_brace: u32,
106    /// Counter for `while let` / similar desugar temps (`$__while_let_0`, …).
107    next_desugar_tmp: u32,
108    /// Source path for [`PerlError`] (matches lexer / `parse_with_file`).
109    error_file: String,
110    /// User-declared sub names (for allowing UDF to shadow stryke extensions in compat mode).
111    declared_subs: std::collections::HashSet<String>,
112    /// When > 0, `parse_named_expr` will not consume following barewords as paren-less
113    /// function arguments. Used by thread macro to prevent `t Color::Red p` from
114    /// interpreting `p` as an argument to the enum constructor instead of a stage.
115    suppress_parenless_call: u32,
116    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
117    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
118    suppress_slash_as_div: u32,
119    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
120    /// Used by thread macro to prevent `/m/` from being misparsed.
121    pub suppress_m_regex: u32,
122}
123
124impl Parser {
125    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
126        Self::new_with_file(tokens, "-e")
127    }
128
129    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
130        Self {
131            tokens,
132            pos: 0,
133            next_rate_limit_slot: 0,
134            suppress_indirect_paren_call: 0,
135            pipe_rhs_depth: 0,
136            no_pipe_forward_depth: 0,
137            suppress_scalar_hash_brace: 0,
138            next_desugar_tmp: 0,
139            error_file: file.into(),
140            declared_subs: std::collections::HashSet::new(),
141            suppress_parenless_call: 0,
142            suppress_slash_as_div: 0,
143            suppress_m_regex: 0,
144        }
145    }
146
147    fn alloc_desugar_tmp(&mut self) -> u32 {
148        let n = self.next_desugar_tmp;
149        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
150        n
151    }
152
153    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
154    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
155    /// placeholder list instead of erroring on a missing operand.
156    #[inline]
157    fn in_pipe_rhs(&self) -> bool {
158        self.pipe_rhs_depth > 0
159    }
160
161    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
162    fn pipe_supplies_slurped_list_operand(&self) -> bool {
163        self.in_pipe_rhs()
164            && matches!(
165                self.peek(),
166                Token::Semicolon
167                    | Token::RBrace
168                    | Token::RParen
169                    | Token::Eof
170                    | Token::Comma
171                    | Token::PipeForward
172            )
173    }
174
175    /// Empty placeholder list used as a stand-in for the list operand of
176    /// list-taking builtins when they appear on the RHS of `|>`.
177    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
178    /// value at desugar time, so the placeholder is never evaluated.
179    #[inline]
180    fn pipe_placeholder_list(&self, line: usize) -> Expr {
181        Expr {
182            kind: ExprKind::List(vec![]),
183            line,
184        }
185    }
186
187    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
188    ///
189    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
190    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
191    /// element instead of stringifying the bareword.  Non-bareword expressions
192    /// pass through unchanged.
193    ///
194    /// Also injects `$_` into known builtins that were parsed with zero
195    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
196    /// topic variable instead of being no-ops.
197    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
198        let line = expr.line;
199        let topic = || Expr {
200            kind: ExprKind::ScalarVar("_".into()),
201            line,
202        };
203        match expr.kind {
204            ExprKind::Bareword(ref name) => Expr {
205                kind: ExprKind::FuncCall {
206                    name: name.clone(),
207                    args: vec![topic()],
208                },
209                line,
210            },
211            // Builtins that take Vec<Expr> args — inject $_ when empty.
212            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
213                kind: ExprKind::Unlink(vec![topic()]),
214                line,
215            },
216            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
217                kind: ExprKind::Chmod(vec![topic()]),
218                line,
219            },
220            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
221            ExprKind::Stat(_) => expr,
222            ExprKind::Lstat(_) => expr,
223            ExprKind::Readlink(_) => expr,
224            // rev with empty list should use $_
225            ExprKind::ScalarReverse(ref inner) => {
226                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
227                    Expr {
228                        kind: ExprKind::ScalarReverse(Box::new(topic())),
229                        line,
230                    }
231                } else {
232                    expr
233                }
234            }
235            _ => expr,
236        }
237    }
238
239    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
240    /// duration, so any trailing `|>` is left to the enclosing parser instead
241    /// of being absorbed into this sub-expression. Used by paren-less arg
242    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
243    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
244    /// left-associatively instead of letting `head`'s first arg swallow the
245    /// outer `|>`. The counter is restored on both success and error paths.
246    fn parse_assign_expr_stop_at_pipe(&mut self) -> PerlResult<Expr> {
247        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
248        let r = self.parse_assign_expr();
249        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
250        r
251    }
252
253    fn syntax_err(&self, message: impl Into<String>, line: usize) -> PerlError {
254        PerlError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
255    }
256
257    fn alloc_rate_limit_slot(&mut self) -> u32 {
258        let s = self.next_rate_limit_slot;
259        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
260        s
261    }
262
263    fn peek(&self) -> &Token {
264        self.tokens
265            .get(self.pos)
266            .map(|(t, _)| t)
267            .unwrap_or(&Token::Eof)
268    }
269
270    fn peek_line(&self) -> usize {
271        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
272    }
273
274    fn peek_at(&self, offset: usize) -> &Token {
275        self.tokens
276            .get(self.pos + offset)
277            .map(|(t, _)| t)
278            .unwrap_or(&Token::Eof)
279    }
280
281    fn advance(&mut self) -> (Token, usize) {
282        let tok = self
283            .tokens
284            .get(self.pos)
285            .cloned()
286            .unwrap_or((Token::Eof, 0));
287        self.pos += 1;
288        tok
289    }
290
291    /// Line number of the most recently consumed token (the token at `pos - 1`).
292    fn prev_line(&self) -> usize {
293        if self.pos > 0 {
294            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
295        } else {
296            0
297        }
298    }
299
300    fn expect(&mut self, expected: &Token) -> PerlResult<usize> {
301        let (tok, line) = self.advance();
302        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
303            Ok(line)
304        } else {
305            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
306        }
307    }
308
309    fn eat(&mut self, expected: &Token) -> bool {
310        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
311            self.advance();
312            true
313        } else {
314            false
315        }
316    }
317
318    fn at_eof(&self) -> bool {
319        matches!(self.peek(), Token::Eof)
320    }
321
322    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
323    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
324        matches!(
325            tok,
326            Token::RParen
327                | Token::Semicolon
328                | Token::Comma
329                | Token::RBrace
330                | Token::Eof
331                | Token::LogAnd
332                | Token::LogOr
333                | Token::LogAndWord
334                | Token::LogOrWord
335                | Token::PipeForward
336        )
337    }
338
339    /// True when the next token is a statement-starting keyword on a *different*
340    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
341    /// import lists when semicolons are omitted (stryke extension).
342    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
343        // Semicolons-optional is a stryke extension; in compat mode, require them.
344        if crate::compat_mode() {
345            return false;
346        }
347        if self.peek_line() == stmt_line {
348            return false;
349        }
350        matches!(
351            self.peek(),
352            Token::Ident(ref kw) if matches!(kw.as_str(),
353                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
354                | "if" | "unless" | "while" | "until" | "for" | "foreach"
355                | "return" | "last" | "next" | "redo" | "package" | "require"
356                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
357            )
358        )
359    }
360
361    // ── Top level ──
362
363    pub fn parse_program(&mut self) -> PerlResult<Program> {
364        let statements = self.parse_statements()?;
365        Ok(Program { statements })
366    }
367
368    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
369    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
370        let mut statements = Vec::new();
371        while !self.at_eof() {
372            if matches!(self.peek(), Token::Semicolon) {
373                let line = self.peek_line();
374                self.advance();
375                statements.push(Statement {
376                    label: None,
377                    kind: StmtKind::Empty,
378                    line,
379                });
380                continue;
381            }
382            statements.push(self.parse_statement()?);
383        }
384        Ok(statements)
385    }
386
387    // ── Statements ──
388
389    fn parse_statement(&mut self) -> PerlResult<Statement> {
390        let line = self.peek_line();
391
392        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
393        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
394        let label = match self.peek().clone() {
395            Token::Ident(_) => {
396                if matches!(self.peek_at(1), Token::Colon)
397                    && !matches!(self.peek_at(2), Token::Colon)
398                {
399                    let (tok, _) = self.advance();
400                    let l = match tok {
401                        Token::Ident(l) => l,
402                        _ => unreachable!(),
403                    };
404                    self.advance(); // ':'
405                    Some(l)
406                } else {
407                    None
408                }
409            }
410            _ => None,
411        };
412
413        let mut stmt = match self.peek().clone() {
414            Token::FormatDecl { .. } => {
415                let tok_line = self.peek_line();
416                let (tok, _) = self.advance();
417                match tok {
418                    Token::FormatDecl { name, lines } => Statement {
419                        label: label.clone(),
420                        kind: StmtKind::FormatDecl { name, lines },
421                        line: tok_line,
422                    },
423                    _ => unreachable!(),
424                }
425            }
426            Token::Ident(ref kw) => match kw.as_str() {
427                "if" => self.parse_if()?,
428                "unless" => self.parse_unless()?,
429                "while" => {
430                    let mut s = self.parse_while()?;
431                    if let StmtKind::While {
432                        label: ref mut lbl, ..
433                    } = s.kind
434                    {
435                        *lbl = label.clone();
436                    }
437                    s
438                }
439                "until" => {
440                    let mut s = self.parse_until()?;
441                    if let StmtKind::Until {
442                        label: ref mut lbl, ..
443                    } = s.kind
444                    {
445                        *lbl = label.clone();
446                    }
447                    s
448                }
449                "for" => {
450                    let mut s = self.parse_for_or_foreach()?;
451                    match s.kind {
452                        StmtKind::For {
453                            label: ref mut lbl, ..
454                        }
455                        | StmtKind::Foreach {
456                            label: ref mut lbl, ..
457                        } => *lbl = label.clone(),
458                        _ => {}
459                    }
460                    s
461                }
462                "foreach" => {
463                    let mut s = self.parse_foreach()?;
464                    if let StmtKind::Foreach {
465                        label: ref mut lbl, ..
466                    } = s.kind
467                    {
468                        *lbl = label.clone();
469                    }
470                    s
471                }
472                "sub" | "fn" => self.parse_sub_decl()?,
473                "struct" => {
474                    if crate::compat_mode() {
475                        return Err(self.syntax_err(
476                            "`struct` is a stryke extension (disabled by --compat)",
477                            self.peek_line(),
478                        ));
479                    }
480                    self.parse_struct_decl()?
481                }
482                "enum" => {
483                    if crate::compat_mode() {
484                        return Err(self.syntax_err(
485                            "`enum` is a stryke extension (disabled by --compat)",
486                            self.peek_line(),
487                        ));
488                    }
489                    self.parse_enum_decl()?
490                }
491                "class" => {
492                    if crate::compat_mode() {
493                        // TODO: parse Perl 5.38 class syntax with :isa()
494                        return Err(self.syntax_err(
495                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
496                            self.peek_line(),
497                        ));
498                    }
499                    self.parse_class_decl(false, false)?
500                }
501                "abstract" => {
502                    self.advance(); // abstract
503                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
504                        return Err(self.syntax_err(
505                            "`abstract` must be followed by `class`",
506                            self.peek_line(),
507                        ));
508                    }
509                    self.parse_class_decl(true, false)?
510                }
511                "final" => {
512                    self.advance(); // final
513                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
514                        return Err(self
515                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
516                    }
517                    self.parse_class_decl(false, true)?
518                }
519                "trait" => {
520                    if crate::compat_mode() {
521                        return Err(self.syntax_err(
522                            "`trait` is a stryke extension (disabled by --compat)",
523                            self.peek_line(),
524                        ));
525                    }
526                    self.parse_trait_decl()?
527                }
528                "my" => self.parse_my_our_local("my", false)?,
529                "state" => self.parse_my_our_local("state", false)?,
530                "mysync" => {
531                    if crate::compat_mode() {
532                        return Err(self.syntax_err(
533                            "`mysync` is a stryke extension (disabled by --compat)",
534                            self.peek_line(),
535                        ));
536                    }
537                    self.parse_my_our_local("mysync", false)?
538                }
539                "frozen" | "const" => {
540                    let leading = kw.as_str().to_string();
541                    if crate::compat_mode() {
542                        return Err(self.syntax_err(
543                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
544                            self.peek_line(),
545                        ));
546                    }
547                    // `frozen my $x = val;` / `const my $x = val;` — the
548                    // two spellings are interchangeable (`const` is the
549                    // more-familiar name for new users). Expects `my`
550                    // to follow.
551                    self.advance(); // consume "frozen"/"const"
552                    if let Token::Ident(ref kw) = self.peek().clone() {
553                        if kw == "my" {
554                            let mut stmt = self.parse_my_our_local("my", false)?;
555                            if let StmtKind::My(ref mut decls) = stmt.kind {
556                                for decl in decls.iter_mut() {
557                                    decl.frozen = true;
558                                }
559                            }
560                            stmt
561                        } else {
562                            return Err(self.syntax_err(
563                                format!("Expected 'my' after '{leading}'"),
564                                self.peek_line(),
565                            ));
566                        }
567                    } else {
568                        return Err(self.syntax_err(
569                            format!("Expected 'my' after '{leading}'"),
570                            self.peek_line(),
571                        ));
572                    }
573                }
574                "typed" => {
575                    if crate::compat_mode() {
576                        return Err(self.syntax_err(
577                            "`typed` is a stryke extension (disabled by --compat)",
578                            self.peek_line(),
579                        ));
580                    }
581                    self.advance();
582                    if let Token::Ident(ref kw) = self.peek().clone() {
583                        if kw == "my" {
584                            self.parse_my_our_local("my", true)?
585                        } else {
586                            return Err(
587                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
588                            );
589                        }
590                    } else {
591                        return Err(
592                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
593                        );
594                    }
595                }
596                "our" => self.parse_my_our_local("our", false)?,
597                "local" => self.parse_my_our_local("local", false)?,
598                "package" => self.parse_package()?,
599                "use" => self.parse_use()?,
600                "no" => self.parse_no()?,
601                "return" => self.parse_return()?,
602                "last" => {
603                    self.advance();
604                    let lbl = if let Token::Ident(ref s) = self.peek() {
605                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
606                            let (Token::Ident(l), _) = self.advance() else {
607                                unreachable!()
608                            };
609                            Some(l)
610                        } else {
611                            None
612                        }
613                    } else {
614                        None
615                    };
616                    let stmt = Statement {
617                        label: None,
618                        kind: StmtKind::Last(lbl.or(label.clone())),
619                        line,
620                    };
621                    self.parse_stmt_postfix_modifier(stmt)?
622                }
623                "next" => {
624                    self.advance();
625                    let lbl = if let Token::Ident(ref s) = self.peek() {
626                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
627                            let (Token::Ident(l), _) = self.advance() else {
628                                unreachable!()
629                            };
630                            Some(l)
631                        } else {
632                            None
633                        }
634                    } else {
635                        None
636                    };
637                    let stmt = Statement {
638                        label: None,
639                        kind: StmtKind::Next(lbl.or(label.clone())),
640                        line,
641                    };
642                    self.parse_stmt_postfix_modifier(stmt)?
643                }
644                "redo" => {
645                    self.advance();
646                    self.eat(&Token::Semicolon);
647                    Statement {
648                        label: None,
649                        kind: StmtKind::Redo(label.clone()),
650                        line,
651                    }
652                }
653                "BEGIN" => {
654                    self.advance();
655                    let block = self.parse_block()?;
656                    Statement {
657                        label: None,
658                        kind: StmtKind::Begin(block),
659                        line,
660                    }
661                }
662                "END" => {
663                    self.advance();
664                    let block = self.parse_block()?;
665                    Statement {
666                        label: None,
667                        kind: StmtKind::End(block),
668                        line,
669                    }
670                }
671                "UNITCHECK" => {
672                    self.advance();
673                    let block = self.parse_block()?;
674                    Statement {
675                        label: None,
676                        kind: StmtKind::UnitCheck(block),
677                        line,
678                    }
679                }
680                "CHECK" => {
681                    self.advance();
682                    let block = self.parse_block()?;
683                    Statement {
684                        label: None,
685                        kind: StmtKind::Check(block),
686                        line,
687                    }
688                }
689                "INIT" => {
690                    self.advance();
691                    let block = self.parse_block()?;
692                    Statement {
693                        label: None,
694                        kind: StmtKind::Init(block),
695                        line,
696                    }
697                }
698                "goto" => {
699                    self.advance();
700                    let target = self.parse_expression()?;
701                    let stmt = Statement {
702                        label: None,
703                        kind: StmtKind::Goto {
704                            target: Box::new(target),
705                        },
706                        line,
707                    };
708                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
709                    self.parse_stmt_postfix_modifier(stmt)?
710                }
711                "continue" => {
712                    self.advance();
713                    let block = self.parse_block()?;
714                    Statement {
715                        label: None,
716                        kind: StmtKind::Continue(block),
717                        line,
718                    }
719                }
720                "try" => self.parse_try_catch()?,
721                "defer" => self.parse_defer_stmt()?,
722                "tie" => self.parse_tie_stmt()?,
723                "given" => self.parse_given()?,
724                "when" => self.parse_when_stmt()?,
725                "default" => self.parse_default_stmt()?,
726                "eval_timeout" => self.parse_eval_timeout()?,
727                "do" => {
728                    if matches!(self.peek_at(1), Token::LBrace) {
729                        self.advance();
730                        let body = self.parse_block()?;
731                        if let Token::Ident(ref w) = self.peek().clone() {
732                            if w == "while" {
733                                self.advance();
734                                self.expect(&Token::LParen)?;
735                                let mut condition = self.parse_expression()?;
736                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
737                                self.expect(&Token::RParen)?;
738                                self.eat(&Token::Semicolon);
739                                Statement {
740                                    label: label.clone(),
741                                    kind: StmtKind::DoWhile { body, condition },
742                                    line,
743                                }
744                            } else {
745                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
746                                let inner = Expr {
747                                    kind: ExprKind::CodeRef {
748                                        params: vec![],
749                                        body,
750                                    },
751                                    line: inner_line,
752                                };
753                                let expr = Expr {
754                                    kind: ExprKind::Do(Box::new(inner)),
755                                    line,
756                                };
757                                let stmt = Statement {
758                                    label: label.clone(),
759                                    kind: StmtKind::Expression(expr),
760                                    line,
761                                };
762                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
763                                self.parse_stmt_postfix_modifier(stmt)?
764                            }
765                        } else {
766                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
767                            let inner = Expr {
768                                kind: ExprKind::CodeRef {
769                                    params: vec![],
770                                    body,
771                                },
772                                line: inner_line,
773                            };
774                            let expr = Expr {
775                                kind: ExprKind::Do(Box::new(inner)),
776                                line,
777                            };
778                            let stmt = Statement {
779                                label: label.clone(),
780                                kind: StmtKind::Expression(expr),
781                                line,
782                            };
783                            self.parse_stmt_postfix_modifier(stmt)?
784                        }
785                    } else {
786                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
787                            let stmt = self.maybe_postfix_modifier(expr)?;
788                            self.parse_stmt_postfix_modifier(stmt)?
789                        } else {
790                            let expr = self.parse_expression()?;
791                            let stmt = self.maybe_postfix_modifier(expr)?;
792                            self.parse_stmt_postfix_modifier(stmt)?
793                        }
794                    }
795                }
796                _ => {
797                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
798                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
799                        let stmt = self.maybe_postfix_modifier(expr)?;
800                        self.parse_stmt_postfix_modifier(stmt)?
801                    } else {
802                        let expr = self.parse_expression()?;
803                        let stmt = self.maybe_postfix_modifier(expr)?;
804                        self.parse_stmt_postfix_modifier(stmt)?
805                    }
806                }
807            },
808            Token::LBrace => {
809                let block = self.parse_block()?;
810                let stmt = Statement {
811                    label: None,
812                    kind: StmtKind::Block(block),
813                    line,
814                };
815                // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
816                self.parse_stmt_postfix_modifier(stmt)?
817            }
818            _ => {
819                let expr = self.parse_expression()?;
820                let stmt = self.maybe_postfix_modifier(expr)?;
821                self.parse_stmt_postfix_modifier(stmt)?
822            }
823        };
824
825        stmt.label = label;
826        Ok(stmt)
827    }
828
829    /// Handle postfix if/unless on statement-level keywords like last/next.
830    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
831        let line = stmt.line;
832        // Implicit semicolon: a modifier keyword on a new line is a new
833        // statement, not a postfix modifier.  This prevents semicolon-less
834        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
835        // as `my $x = "val" if ($x) { ... }`.
836        if self.peek_line() > self.prev_line() {
837            self.eat(&Token::Semicolon);
838            return Ok(stmt);
839        }
840        if let Token::Ident(ref kw) = self.peek().clone() {
841            match kw.as_str() {
842                "if" => {
843                    self.advance();
844                    let mut cond = self.parse_expression()?;
845                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
846                    self.eat(&Token::Semicolon);
847                    return Ok(Statement {
848                        label: None,
849                        kind: StmtKind::If {
850                            condition: cond,
851                            body: vec![stmt],
852                            elsifs: vec![],
853                            else_block: None,
854                        },
855                        line,
856                    });
857                }
858                "unless" => {
859                    self.advance();
860                    let mut cond = self.parse_expression()?;
861                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
862                    self.eat(&Token::Semicolon);
863                    return Ok(Statement {
864                        label: None,
865                        kind: StmtKind::Unless {
866                            condition: cond,
867                            body: vec![stmt],
868                            else_block: None,
869                        },
870                        line,
871                    });
872                }
873                "while" | "until" | "for" | "foreach" => {
874                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
875                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
876                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
877                        let out = self.maybe_postfix_modifier(expr)?;
878                        self.eat(&Token::Semicolon);
879                        return Ok(out);
880                    }
881                    return Err(self.syntax_err(
882                        format!("postfix `{}` is not supported on this statement form", kw),
883                        self.peek_line(),
884                    ));
885                }
886                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
887                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
888                    let line = stmt.line;
889                    let block = self.stmt_into_parallel_block(stmt)?;
890                    let which = kw.as_str();
891                    self.advance();
892                    self.eat(&Token::Comma);
893                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
894                    self.eat(&Token::Semicolon);
895                    let list = Box::new(list);
896                    let progress = progress.map(Box::new);
897                    let kind = match which {
898                        "pmap" => ExprKind::PMapExpr {
899                            block,
900                            list,
901                            progress,
902                            flat_outputs: false,
903                            on_cluster: None,
904                        },
905                        "pflat_map" => ExprKind::PMapExpr {
906                            block,
907                            list,
908                            progress,
909                            flat_outputs: true,
910                            on_cluster: None,
911                        },
912                        "pgrep" => ExprKind::PGrepExpr {
913                            block,
914                            list,
915                            progress,
916                        },
917                        "pfor" => ExprKind::PForExpr {
918                            block,
919                            list,
920                            progress,
921                        },
922                        "preduce" => ExprKind::PReduceExpr {
923                            block,
924                            list,
925                            progress,
926                        },
927                        "pcache" => ExprKind::PcacheExpr {
928                            block,
929                            list,
930                            progress,
931                        },
932                        _ => unreachable!(),
933                    };
934                    return Ok(Statement {
935                        label: None,
936                        kind: StmtKind::Expression(Expr { kind, line }),
937                        line,
938                    });
939                }
940                _ => {}
941            }
942        }
943        self.eat(&Token::Semicolon);
944        Ok(stmt)
945    }
946
947    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
948    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
949    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
950        let line = stmt.line;
951        match stmt.kind {
952            StmtKind::Block(block) => Ok(block),
953            StmtKind::Expression(expr) => {
954                if let ExprKind::Do(ref inner) = expr.kind {
955                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
956                        return Ok(body.clone());
957                    }
958                }
959                Ok(vec![Statement {
960                    label: None,
961                    kind: StmtKind::Expression(expr),
962                    line,
963                }])
964            }
965            _ => Err(self.syntax_err(
966                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
967                line,
968            )),
969        }
970    }
971
972    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
973    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`](ExprKind::Do)([`CodeRef`](ExprKind::CodeRef))).
974    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
975        match stmt.kind {
976            StmtKind::Expression(expr) => Some(expr),
977            StmtKind::Block(block) => {
978                let line = stmt.line;
979                let inner = Expr {
980                    kind: ExprKind::CodeRef {
981                        params: vec![],
982                        body: block,
983                    },
984                    line,
985                };
986                Some(Expr {
987                    kind: ExprKind::Do(Box::new(inner)),
988                    line,
989                })
990            }
991            _ => None,
992        }
993    }
994
995    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
996    /// (same set as [`parse_list_until_terminator`]).
997    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
998        matches!(
999            self.peek(),
1000            Token::Ident(ref kw)
1001                if matches!(
1002                    kw.as_str(),
1003                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1004                )
1005        )
1006    }
1007
1008    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1009        let line = expr.line;
1010        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1011        if self.peek_line() > self.prev_line() {
1012            return Ok(Statement {
1013                label: None,
1014                kind: StmtKind::Expression(expr),
1015                line,
1016            });
1017        }
1018        match self.peek() {
1019            Token::Ident(ref kw) => match kw.as_str() {
1020                "if" => {
1021                    self.advance();
1022                    let cond = self.parse_expression()?;
1023                    Ok(Statement {
1024                        label: None,
1025                        kind: StmtKind::Expression(Expr {
1026                            kind: ExprKind::PostfixIf {
1027                                expr: Box::new(expr),
1028                                condition: Box::new(cond),
1029                            },
1030                            line,
1031                        }),
1032                        line,
1033                    })
1034                }
1035                "unless" => {
1036                    self.advance();
1037                    let cond = self.parse_expression()?;
1038                    Ok(Statement {
1039                        label: None,
1040                        kind: StmtKind::Expression(Expr {
1041                            kind: ExprKind::PostfixUnless {
1042                                expr: Box::new(expr),
1043                                condition: Box::new(cond),
1044                            },
1045                            line,
1046                        }),
1047                        line,
1048                    })
1049                }
1050                "while" => {
1051                    self.advance();
1052                    let cond = self.parse_expression()?;
1053                    Ok(Statement {
1054                        label: None,
1055                        kind: StmtKind::Expression(Expr {
1056                            kind: ExprKind::PostfixWhile {
1057                                expr: Box::new(expr),
1058                                condition: Box::new(cond),
1059                            },
1060                            line,
1061                        }),
1062                        line,
1063                    })
1064                }
1065                "until" => {
1066                    self.advance();
1067                    let cond = self.parse_expression()?;
1068                    Ok(Statement {
1069                        label: None,
1070                        kind: StmtKind::Expression(Expr {
1071                            kind: ExprKind::PostfixUntil {
1072                                expr: Box::new(expr),
1073                                condition: Box::new(cond),
1074                            },
1075                            line,
1076                        }),
1077                        line,
1078                    })
1079                }
1080                "for" | "foreach" => {
1081                    self.advance();
1082                    let list = self.parse_expression()?;
1083                    Ok(Statement {
1084                        label: None,
1085                        kind: StmtKind::Expression(Expr {
1086                            kind: ExprKind::PostfixForeach {
1087                                expr: Box::new(expr),
1088                                list: Box::new(list),
1089                            },
1090                            line,
1091                        }),
1092                        line,
1093                    })
1094                }
1095                _ => Ok(Statement {
1096                    label: None,
1097                    kind: StmtKind::Expression(expr),
1098                    line,
1099                }),
1100            },
1101            _ => Ok(Statement {
1102                label: None,
1103                kind: StmtKind::Expression(expr),
1104                line,
1105            }),
1106        }
1107    }
1108
1109    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1110    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1111        let saved = self.pos;
1112        let line = self.peek_line();
1113        let mut name = match self.peek() {
1114            Token::Ident(n) => n.clone(),
1115            _ => return None,
1116        };
1117        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1118        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1119            return None;
1120        }
1121        self.advance();
1122        while self.eat(&Token::PackageSep) {
1123            match self.advance() {
1124                (Token::Ident(part), _) => {
1125                    name = format!("{}::{}", name, part);
1126                }
1127                _ => {
1128                    self.pos = saved;
1129                    return None;
1130                }
1131            }
1132        }
1133        match self.peek() {
1134            Token::Semicolon | Token::RBrace => Some(Expr {
1135                kind: ExprKind::FuncCall { name, args: vec![] },
1136                line,
1137            }),
1138            _ => {
1139                self.pos = saved;
1140                None
1141            }
1142        }
1143    }
1144
1145    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1146    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1147        !matches!(
1148            name,
1149            "__FILE__"
1150                | "__LINE__"
1151                | "abs"
1152                | "async"
1153                | "spawn"
1154                | "atan2"
1155                | "await"
1156                | "barrier"
1157                | "bless"
1158                | "caller"
1159                | "capture"
1160                | "cat"
1161                | "chdir"
1162                | "chmod"
1163                | "chomp"
1164                | "chop"
1165                | "chr"
1166                | "chown"
1167                | "closedir"
1168                | "close"
1169                | "collect"
1170                | "cos"
1171                | "crypt"
1172                | "defined"
1173                | "dec"
1174                | "delete"
1175                | "die"
1176                | "deque"
1177                | "do"
1178                | "each"
1179                | "eof"
1180                | "fore"
1181                | "eval"
1182                | "exec"
1183                | "exists"
1184                | "exit"
1185                | "exp"
1186                | "fan"
1187                | "fan_cap"
1188                | "fc"
1189                | "fetch_url"
1190                | "d"
1191                | "dirs"
1192                | "dr"
1193                | "f"
1194                | "files"
1195                | "filesf"
1196                | "filter"
1197                | "fr"
1198                | "getcwd"
1199                | "glob_par"
1200                | "par_sed"
1201                | "glob"
1202                | "grep"
1203                | "greps"
1204                | "heap"
1205                | "hex"
1206                | "inc"
1207                | "index"
1208                | "int"
1209                | "join"
1210                | "keys"
1211                | "lcfirst"
1212                | "lc"
1213                | "length"
1214                | "link"
1215                | "log"
1216                | "lstat"
1217                | "map"
1218                | "flat_map"
1219                | "maps"
1220                | "flat_maps"
1221                | "flatten"
1222                | "frequencies"
1223                | "freq"
1224                | "interleave"
1225                | "ddump"
1226                | "stringify"
1227                | "str"
1228                | "s"
1229                | "input"
1230                | "lines"
1231                | "words"
1232                | "chars"
1233                | "digits"
1234                | "sentences"
1235                | "paragraphs"
1236                | "sections"
1237                | "numbers"
1238                | "graphemes"
1239                | "columns"
1240                | "trim"
1241                | "avg"
1242                | "top"
1243                | "pager"
1244                | "pg"
1245                | "less"
1246                | "count_by"
1247                | "to_file"
1248                | "to_json"
1249                | "to_csv"
1250                | "grep_v"
1251                | "select_keys"
1252                | "pluck"
1253                | "clamp"
1254                | "normalize"
1255                | "stddev"
1256                | "squared"
1257                | "square"
1258                | "cubed"
1259                | "cube"
1260                | "expt"
1261                | "pow"
1262                | "pw"
1263                | "snake_case"
1264                | "camel_case"
1265                | "kebab_case"
1266                | "to_toml"
1267                | "to_yaml"
1268                | "to_xml"
1269                | "to_html"
1270                | "to_markdown"
1271                | "xopen"
1272                | "clip"
1273                | "paste"
1274                | "to_table"
1275                | "sparkline"
1276                | "bar_chart"
1277                | "flame"
1278                | "set"
1279                | "list_count"
1280                | "list_size"
1281                | "count"
1282                | "size"
1283                | "cnt"
1284                | "len"
1285                | "all"
1286                | "any"
1287                | "none"
1288                | "take_while"
1289                | "drop_while"
1290                | "skip_while"
1291                | "skip"
1292                | "first_or"
1293                | "tap"
1294                | "peek"
1295                | "partition"
1296                | "min_by"
1297                | "max_by"
1298                | "zip_with"
1299                | "group_by"
1300                | "chunk_by"
1301                | "with_index"
1302                | "puniq"
1303                | "pfirst"
1304                | "pany"
1305                | "uniq"
1306                | "distinct"
1307                | "shuffle"
1308                | "shuffled"
1309                | "chunked"
1310                | "windowed"
1311                | "match"
1312                | "mkdir"
1313                | "every"
1314                | "gen"
1315                | "oct"
1316                | "open"
1317                | "p"
1318                | "opendir"
1319                | "ord"
1320                | "par_lines"
1321                | "par_walk"
1322                | "pipe"
1323                | "pipes"
1324                | "block_devices"
1325                | "char_devices"
1326                | "rate_limit"
1327                | "retry"
1328                | "pcache"
1329                | "pchannel"
1330                | "pfor"
1331                | "pgrep"
1332                | "pipeline"
1333                | "pmap_chunked"
1334                | "pmap_reduce"
1335                | "pmap_on"
1336                | "pflat_map_on"
1337                | "pmap"
1338                | "pflat_map"
1339                | "pop"
1340                | "pos"
1341                | "ppool"
1342                | "preduce_init"
1343                | "preduce"
1344                | "pselect"
1345                | "printf"
1346                | "print"
1347                | "pr"
1348                | "psort"
1349                | "push"
1350                | "pwatch"
1351                | "rand"
1352                | "readdir"
1353                | "readlink"
1354                | "reduce"
1355                | "fold"
1356                | "inject"
1357                | "first"
1358                | "detect"
1359                | "find"
1360                | "find_all"
1361                | "ref"
1362                | "rename"
1363                | "require"
1364                | "rev"
1365                | "reverse"
1366                | "reversed"
1367                | "rewinddir"
1368                | "rindex"
1369                | "rmdir"
1370                | "rm"
1371                | "say"
1372                | "scalar"
1373                | "seekdir"
1374                | "shift"
1375                | "sin"
1376                | "slurp"
1377                | "sockets"
1378                | "sort"
1379                | "splice"
1380                | "split"
1381                | "sprintf"
1382                | "sqrt"
1383                | "srand"
1384                | "stat"
1385                | "study"
1386                | "substr"
1387                | "symlink"
1388                | "sym_links"
1389                | "system"
1390                | "telldir"
1391                | "timer"
1392                | "trace"
1393                | "ucfirst"
1394                | "uc"
1395                | "undef"
1396                | "umask"
1397                | "unlink"
1398                | "unshift"
1399                | "utime"
1400                | "values"
1401                | "wantarray"
1402                | "warn"
1403                | "watch"
1404                | "yield"
1405                | "sub"
1406        )
1407    }
1408
1409    fn parse_block(&mut self) -> PerlResult<Block> {
1410        self.expect(&Token::LBrace)?;
1411        let mut stmts = Vec::new();
1412        // `{ |$a, $b| body }` — Ruby-style block params.
1413        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1414        // or `my $p = $_N` for positional N≥3.
1415        if let Some(param_stmts) = self.try_parse_block_params()? {
1416            stmts.extend(param_stmts);
1417        }
1418        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1419            if self.eat(&Token::Semicolon) {
1420                continue;
1421            }
1422            stmts.push(self.parse_statement()?);
1423        }
1424        self.expect(&Token::RBrace)?;
1425        Self::default_topic_for_sole_bareword(&mut stmts);
1426        Ok(stmts)
1427    }
1428
1429    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1430    /// Returns `None` if the leading `|` is not block-param syntax.
1431    /// When successful, returns `my $var = <implicit>` assignment statements
1432    /// that alias the block's positional arguments.
1433    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1434        if !matches!(self.peek(), Token::BitOr) {
1435            return Ok(None);
1436        }
1437        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1438        let mut i = 1; // skip the opening `|`
1439        loop {
1440            match self.peek_at(i) {
1441                Token::ScalarVar(_) => i += 1,
1442                _ => return Ok(None), // not `|$var...|`
1443            }
1444            match self.peek_at(i) {
1445                Token::BitOr => break,  // closing `|`
1446                Token::Comma => i += 1, // more params
1447                _ => return Ok(None),   // not block params
1448            }
1449        }
1450        // Confirmed — consume and build assignments.
1451        let line = self.peek_line();
1452        self.advance(); // eat opening `|`
1453        let mut names = Vec::new();
1454        loop {
1455            if let Token::ScalarVar(ref name) = self.peek().clone() {
1456                names.push(name.clone());
1457                self.advance();
1458            }
1459            if self.eat(&Token::BitOr) {
1460                break;
1461            }
1462            self.expect(&Token::Comma)?;
1463        }
1464        // Generate `my $name = <source>` for each param.
1465        // 1 param  → source is `$_` (map/grep/each/for topic)
1466        // 2 params → sources are `$a`, `$b` (sort/reduce)
1467        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1468        let sources: Vec<&str> = match names.len() {
1469            1 => vec!["_"],
1470            2 => vec!["a", "b"],
1471            n => {
1472                // Can't return borrowed from a generated vec, handle below.
1473                let _ = n;
1474                vec![] // sentinel — handled in the else branch
1475            }
1476        };
1477        let mut stmts = Vec::with_capacity(names.len());
1478        if !sources.is_empty() {
1479            for (name, src) in names.iter().zip(sources.iter()) {
1480                stmts.push(Statement {
1481                    label: None,
1482                    kind: StmtKind::My(vec![VarDecl {
1483                        sigil: Sigil::Scalar,
1484                        name: name.clone(),
1485                        initializer: Some(Expr {
1486                            kind: ExprKind::ScalarVar(src.to_string()),
1487                            line,
1488                        }),
1489                        frozen: false,
1490                        type_annotation: None,
1491                    }]),
1492                    line,
1493                });
1494            }
1495        } else {
1496            // N≥3: positional `$_`, `$_1`, `$_2`, …
1497            for (idx, name) in names.iter().enumerate() {
1498                let src = if idx == 0 {
1499                    "_".to_string()
1500                } else {
1501                    format!("_{idx}")
1502                };
1503                stmts.push(Statement {
1504                    label: None,
1505                    kind: StmtKind::My(vec![VarDecl {
1506                        sigil: Sigil::Scalar,
1507                        name: name.clone(),
1508                        initializer: Some(Expr {
1509                            kind: ExprKind::ScalarVar(src),
1510                            line,
1511                        }),
1512                        frozen: false,
1513                        type_annotation: None,
1514                    }]),
1515                    line,
1516                });
1517            }
1518        }
1519        Ok(Some(stmts))
1520    }
1521
1522    /// Block shorthand: when the body is literally one bare builtin call
1523    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1524    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1525    ///
1526    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1527    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1528    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1529    /// empty args and return the wrong value. This rewrite levels the
1530    /// playing field at parse time — no per-builtin handling needed.
1531    ///
1532    /// Narrow by design: fires only when the block has *exactly one*
1533    /// expression statement whose sole content is a known-bareword call
1534    /// with zero args. Multi-statement blocks and blocks with any other
1535    /// content are untouched.
1536    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1537        let [only] = stmts else { return };
1538        let StmtKind::Expression(ref mut expr) = only.kind else {
1539            return;
1540        };
1541        let topic_line = expr.line;
1542        let topic_arg = || Expr {
1543            kind: ExprKind::ScalarVar("_".to_string()),
1544            line: topic_line,
1545        };
1546        match expr.kind {
1547            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1548            ExprKind::FuncCall {
1549                ref name,
1550                ref mut args,
1551            } if args.is_empty()
1552                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1553            {
1554                args.push(topic_arg());
1555            }
1556            // Lone bareword (the parser sometimes keeps a bareword as a
1557            // `Bareword` node instead of a zero-arg `FuncCall` —
1558            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1559            ExprKind::Bareword(ref name)
1560                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1561            {
1562                let n = name.clone();
1563                expr.kind = ExprKind::FuncCall {
1564                    name: n,
1565                    args: vec![topic_arg()],
1566                };
1567            }
1568            _ => {}
1569        }
1570    }
1571
1572    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1573    /// Desugars to a `defer__internal(sub { BLOCK })` function call that the compiler
1574    /// handles specially by emitting Op::DeferBlock.
1575    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1576        let line = self.peek_line();
1577        self.advance(); // defer
1578        let body = self.parse_block()?;
1579        self.eat(&Token::Semicolon);
1580        // Desugar: defer { BLOCK } → defer__internal(sub { BLOCK })
1581        let coderef = Expr {
1582            kind: ExprKind::CodeRef {
1583                params: vec![],
1584                body,
1585            },
1586            line,
1587        };
1588        Ok(Statement {
1589            label: None,
1590            kind: StmtKind::Expression(Expr {
1591                kind: ExprKind::FuncCall {
1592                    name: "defer__internal".to_string(),
1593                    args: vec![coderef],
1594                },
1595                line,
1596            }),
1597            line,
1598        })
1599    }
1600
1601    /// `try { } catch ($err) { }` with optional `finally { }`
1602    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
1603        let line = self.peek_line();
1604        self.advance(); // try
1605        let try_block = self.parse_block()?;
1606        match self.peek() {
1607            Token::Ident(ref k) if k == "catch" => {
1608                self.advance();
1609            }
1610            _ => {
1611                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
1612            }
1613        }
1614        self.expect(&Token::LParen)?;
1615        let catch_var = self.parse_scalar_var_name()?;
1616        self.expect(&Token::RParen)?;
1617        let catch_block = self.parse_block()?;
1618        let finally_block = match self.peek() {
1619            Token::Ident(ref k) if k == "finally" => {
1620                self.advance();
1621                Some(self.parse_block()?)
1622            }
1623            _ => None,
1624        };
1625        self.eat(&Token::Semicolon);
1626        Ok(Statement {
1627            label: None,
1628            kind: StmtKind::TryCatch {
1629                try_block,
1630                catch_var,
1631                catch_block,
1632                finally_block,
1633            },
1634            line,
1635        })
1636    }
1637
1638    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
1639    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
1640    ///
1641    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
1642    /// is not parsed from tokens — using `parse_unary()` there lets the first
1643    /// bareword greedily consume the next token as its arg, which misparses
1644    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
1645    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
1646    /// token through the stage loop, and wrap the resulting chain in a
1647    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
1648    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
1649    fn parse_thread_macro(&mut self, _line: usize) -> PerlResult<Expr> {
1650        let pipe_rhs_wrap = self.in_pipe_rhs();
1651        let mut result = if pipe_rhs_wrap {
1652            Expr {
1653                kind: ExprKind::ArrayElement {
1654                    array: "_".to_string(),
1655                    index: Box::new(Expr {
1656                        kind: ExprKind::Integer(0),
1657                        line: _line,
1658                    }),
1659                },
1660                line: _line,
1661            }
1662        } else {
1663            // Suppress paren-less function calls so `t Color::Red p` parses
1664            // the enum variant without consuming `p` as an argument.
1665            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
1666            let expr = self.parse_thread_input();
1667            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
1668            expr?
1669        };
1670
1671        // Parse stages until we hit a statement terminator
1672        loop {
1673            // Check for terminators - |> ends thread and allows piping the result
1674            match self.peek() {
1675                Token::Semicolon
1676                | Token::Newline
1677                | Token::RBrace
1678                | Token::RParen
1679                | Token::RBracket
1680                | Token::PipeForward
1681                | Token::Eof => break,
1682                _ => {}
1683            }
1684
1685            let stage_line = self.peek_line();
1686
1687            // Parse a stage and apply it to result via pipe
1688            match self.peek().clone() {
1689                // `>{ block }` — standalone anonymous block (sugar for sub { })
1690                Token::ArrowBrace => {
1691                    self.advance(); // consume `>{`
1692                    let mut stmts = Vec::new();
1693                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1694                        if self.eat(&Token::Semicolon) {
1695                            continue;
1696                        }
1697                        stmts.push(self.parse_statement()?);
1698                    }
1699                    self.expect(&Token::RBrace)?;
1700                    let code_ref = Expr {
1701                        kind: ExprKind::CodeRef {
1702                            params: vec![],
1703                            body: stmts,
1704                        },
1705                        line: stage_line,
1706                    };
1707                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1708                }
1709                // `sub { block }` or `fn { block }` — explicit anonymous block
1710                Token::Ident(ref name) if name == "sub" || name == "fn" => {
1711                    self.advance(); // consume `sub`
1712                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
1713                    let body = self.parse_block()?;
1714                    let code_ref = Expr {
1715                        kind: ExprKind::CodeRef { params, body },
1716                        line: stage_line,
1717                    };
1718                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1719                }
1720                // `ident` possibly followed by block
1721                Token::Ident(ref name) => {
1722                    let func_name = name.clone();
1723                    self.advance();
1724
1725                    // Handle s/// and tr/// encoded tokens
1726                    if func_name.starts_with('\x00') {
1727                        let parts: Vec<&str> = func_name.split('\x00').collect();
1728                        if parts.len() >= 4 && parts[1] == "s" {
1729                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1730                            let stage = Expr {
1731                                kind: ExprKind::Substitution {
1732                                    expr: Box::new(result.clone()),
1733                                    pattern: parts[2].to_string(),
1734                                    replacement: parts[3].to_string(),
1735                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1736                                    delim,
1737                                },
1738                                line: stage_line,
1739                            };
1740                            result = stage;
1741                            continue;
1742                        }
1743                        if parts.len() >= 4 && parts[1] == "tr" {
1744                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1745                            let stage = Expr {
1746                                kind: ExprKind::Transliterate {
1747                                    expr: Box::new(result.clone()),
1748                                    from: parts[2].to_string(),
1749                                    to: parts[3].to_string(),
1750                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1751                                    delim,
1752                                },
1753                                line: stage_line,
1754                            };
1755                            result = stage;
1756                            continue;
1757                        }
1758                        return Err(
1759                            self.syntax_err("Unexpected encoded token in thread", stage_line)
1760                        );
1761                    }
1762
1763                    // `map +{ ... }` — hashref expression form (not a code block).
1764                    // The `+` disambiguates: `+{` is always a hashref constructor.
1765                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
1766                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
1767                    if matches!(self.peek(), Token::Plus)
1768                        && matches!(self.peek_at(1), Token::LBrace)
1769                    {
1770                        self.advance(); // consume `+`
1771                        self.expect(&Token::LBrace)?;
1772                        // try_parse_hash_ref consumes the closing `}`
1773                        let pairs = self.try_parse_hash_ref()?;
1774                        let hashref_expr = Expr {
1775                            kind: ExprKind::HashRef(pairs),
1776                            line: stage_line,
1777                        };
1778                        let flatten_array_refs =
1779                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
1780                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
1781                        // Placeholder list — pipe_forward_apply replaces it with `result`.
1782                        let placeholder = Expr {
1783                            kind: ExprKind::Undef,
1784                            line: stage_line,
1785                        };
1786                        let map_node = Expr {
1787                            kind: ExprKind::MapExprComma {
1788                                expr: Box::new(hashref_expr),
1789                                list: Box::new(placeholder),
1790                                flatten_array_refs,
1791                                stream,
1792                            },
1793                            line: stage_line,
1794                        };
1795                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
1796                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
1797                    } else if matches!(self.peek(), Token::LBrace) {
1798                        // Parse as a block-taking builtin
1799                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
1800                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
1801                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
1802                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1803                    } else if matches!(self.peek(), Token::LParen) {
1804                        // `name($_-bearing-args)` — parse explicit args, require at
1805                        // least one `$_` placeholder, then wrap as a `>{...}` block
1806                        // so the threaded value binds to `$_` at any position.
1807                        // Examples:
1808                        //   t 10 add2($_, 5) p      → add2(10, 5)
1809                        //   t 10 sub2(20, $_) p     → sub2(20, 10)
1810                        //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
1811                        // To pass the threaded value as a sole arg, use bare form:
1812                        //   t 10 add2 p   (not `add2()`)
1813                        self.advance(); // consume `(`
1814                        let mut call_args = Vec::new();
1815                        while !matches!(self.peek(), Token::RParen | Token::Eof) {
1816                            call_args.push(self.parse_assign_expr()?);
1817                            if !self.eat(&Token::Comma) {
1818                                break;
1819                            }
1820                        }
1821                        self.expect(&Token::RParen)?;
1822                        if !call_args.iter().any(Self::expr_contains_topic_var) {
1823                            return Err(self.syntax_err(
1824                                format!(
1825                                    "thread: `{}(...)` call-stage requires `$_` placeholder somewhere in args (e.g. `{}($_, ...)`); use bare `{}` for sole-arg threading or `>{{ ... }}` for arbitrary expressions",
1826                                    func_name, func_name, func_name
1827                                ),
1828                                stage_line,
1829                            ));
1830                        }
1831                        let call_expr = Expr {
1832                            kind: ExprKind::FuncCall {
1833                                name: func_name.clone(),
1834                                args: call_args,
1835                            },
1836                            line: stage_line,
1837                        };
1838                        let code_ref = Expr {
1839                            kind: ExprKind::CodeRef {
1840                                params: vec![],
1841                                body: vec![Statement {
1842                                    label: None,
1843                                    kind: StmtKind::Expression(call_expr),
1844                                    line: stage_line,
1845                                }],
1846                            },
1847                            line: stage_line,
1848                        };
1849                        result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1850                    } else {
1851                        // Bare function name — handle unary builtins specially
1852                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
1853                    }
1854                }
1855                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
1856                Token::Regex(ref pattern, ref flags, delim) => {
1857                    let pattern = pattern.clone();
1858                    let flags = flags.clone();
1859                    self.advance();
1860                    result =
1861                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
1862                }
1863                // Handle `/` that was lexed as Slash (division) because it followed a term.
1864                // In thread stage context, `/pattern/` should be a regex filter.
1865                Token::Slash => {
1866                    self.advance(); // consume opening /
1867
1868                    // Special case: if next token is Ident("m") or similar followed by Regex,
1869                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
1870                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
1871                    if let Token::Ident(ref ident_s) = self.peek().clone() {
1872                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
1873                            && matches!(self.peek_at(1), Token::Regex(..))
1874                        {
1875                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
1876                            self.advance(); // consume the ident
1877                                            // The Token::Regex after it was a misparsed `m/...` - we need to
1878                                            // extract what would have been the closing `/` situation.
1879                                            // Actually, the lexer consumed everything. Let's just use the ident
1880                                            // as the pattern and expect a closing slash.
1881                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
1882                                self.peek().clone()
1883                            {
1884                                // The misparsed regex ate our closing `/`.
1885                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
1886                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
1887                                // interprets as m// regex start, reads until next `/` (none) -> error.
1888                                // So we shouldn't reach here if there was an error.
1889                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
1890                                // This is getting complicated. Let me try a different approach.
1891                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
1892                                // Skip for now and fall through to manual parsing.
1893                                let _ = (misparsed_pattern, misparsed_flags);
1894                            }
1895                        }
1896                    }
1897
1898                    // Manually parse the regex pattern from tokens until we hit another Slash
1899                    let mut pattern = String::new();
1900                    loop {
1901                        match self.peek().clone() {
1902                            Token::Slash => {
1903                                self.advance(); // consume closing /
1904                                break;
1905                            }
1906                            Token::Eof | Token::Semicolon | Token::Newline => {
1907                                return Err(self
1908                                    .syntax_err("Unterminated regex in thread stage", stage_line));
1909                            }
1910                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
1911                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
1912                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
1913                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
1914                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
1915                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
1916                                // treat the previous ident as the whole pattern and this Regex as misparsed.
1917                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
1918                                // This is a lexer bug workaround.
1919                                if pattern.is_empty()
1920                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
1921                                {
1922                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
1923                                    // and lexer misparsed. The Regex token is garbage.
1924                                    // Just use the ident as pattern and ignore this Regex.
1925                                    // But we already advanced past the ident...
1926                                    // This is messy. Let me try a cleaner approach.
1927                                    let _ = (inner_pattern, inner_flags, delim);
1928                                }
1929                                // For now, error out - this case is too complex
1930                                return Err(self.syntax_err(
1931                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
1932                                    stage_line,
1933                                ));
1934                            }
1935                            Token::Ident(ref s) => {
1936                                pattern.push_str(s);
1937                                self.advance();
1938                            }
1939                            Token::Integer(n) => {
1940                                pattern.push_str(&n.to_string());
1941                                self.advance();
1942                            }
1943                            Token::ScalarVar(ref v) => {
1944                                pattern.push('$');
1945                                pattern.push_str(v);
1946                                self.advance();
1947                            }
1948                            Token::Dot => {
1949                                pattern.push('.');
1950                                self.advance();
1951                            }
1952                            Token::Star => {
1953                                pattern.push('*');
1954                                self.advance();
1955                            }
1956                            Token::Plus => {
1957                                pattern.push('+');
1958                                self.advance();
1959                            }
1960                            Token::Question => {
1961                                pattern.push('?');
1962                                self.advance();
1963                            }
1964                            Token::LParen => {
1965                                pattern.push('(');
1966                                self.advance();
1967                            }
1968                            Token::RParen => {
1969                                pattern.push(')');
1970                                self.advance();
1971                            }
1972                            Token::LBracket => {
1973                                pattern.push('[');
1974                                self.advance();
1975                            }
1976                            Token::RBracket => {
1977                                pattern.push(']');
1978                                self.advance();
1979                            }
1980                            Token::Backslash => {
1981                                pattern.push('\\');
1982                                self.advance();
1983                            }
1984                            Token::BitOr => {
1985                                pattern.push('|');
1986                                self.advance();
1987                            }
1988                            Token::Power => {
1989                                pattern.push_str("**");
1990                                self.advance();
1991                            }
1992                            Token::BitXor => {
1993                                pattern.push('^');
1994                                self.advance();
1995                            }
1996                            Token::Minus => {
1997                                pattern.push('-');
1998                                self.advance();
1999                            }
2000                            _ => {
2001                                return Err(self.syntax_err(
2002                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2003                                    stage_line,
2004                                ));
2005                            }
2006                        }
2007                    }
2008                    // Parse optional flags (sequence of letters after closing /)
2009                    // Be careful: single letters like 'e' could be regex flags OR thread
2010                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2011                    let mut flags = String::new();
2012                    if let Token::Ident(ref s) = self.peek().clone() {
2013                        let is_flag_only =
2014                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2015                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2016                        if is_flag_only && !followed_by_brace {
2017                            flags.push_str(s);
2018                            self.advance();
2019                        }
2020                    }
2021                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2022                }
2023                tok => {
2024                    return Err(self.syntax_err(
2025                        format!(
2026                            "thread: expected stage (ident, sub {{}}, s///, tr///, or /re/), got {:?}",
2027                            tok
2028                        ),
2029                        stage_line,
2030                    ));
2031                }
2032            };
2033        }
2034
2035        if pipe_rhs_wrap {
2036            // Wrap as `sub { …stages threaded from $_[0]… }` so the outer
2037            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2038            let body_line = result.line;
2039            return Ok(Expr {
2040                kind: ExprKind::CodeRef {
2041                    params: vec![],
2042                    body: vec![Statement {
2043                        label: None,
2044                        kind: StmtKind::Expression(result),
2045                        line: body_line,
2046                    }],
2047                },
2048                line: _line,
2049            });
2050        }
2051        Ok(result)
2052    }
2053
2054    /// Build a grep filter stage from a regex pattern for the thread macro.
2055    fn thread_regex_grep_stage(
2056        &self,
2057        list: Expr,
2058        pattern: String,
2059        flags: String,
2060        delim: char,
2061        line: usize,
2062    ) -> Expr {
2063        let topic = Expr {
2064            kind: ExprKind::ScalarVar("_".to_string()),
2065            line,
2066        };
2067        let match_expr = Expr {
2068            kind: ExprKind::Match {
2069                expr: Box::new(topic),
2070                pattern,
2071                flags,
2072                scalar_g: false,
2073                delim,
2074            },
2075            line,
2076        };
2077        let block = vec![Statement {
2078            label: None,
2079            kind: StmtKind::Expression(match_expr),
2080            line,
2081        }];
2082        Expr {
2083            kind: ExprKind::GrepExpr {
2084                block,
2085                list: Box::new(list),
2086                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2087            },
2088            line,
2089        }
2090    }
2091
2092    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2093    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2094    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2095    /// must appear in the args, otherwise the threaded value is silently dropped.
2096    ///
2097    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2098    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2099    /// per-variant walker that would need to be updated whenever new `ExprKind`
2100    /// variants are added (and would silently miss any it forgot to handle).
2101    /// Parse-time perf is non-critical and the AST is small at this scope.
2102    fn expr_contains_topic_var(e: &Expr) -> bool {
2103        format!("{:?}", e).contains("ScalarVar(\"_\")")
2104    }
2105
2106    /// Apply a bare function name in thread context, handling unary builtins specially.
2107    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2108        let kind = match name {
2109            // String functions
2110            "uc" => ExprKind::Uc(Box::new(arg)),
2111            "lc" => ExprKind::Lc(Box::new(arg)),
2112            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2113            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2114            "fc" => ExprKind::Fc(Box::new(arg)),
2115            "chomp" => ExprKind::Chomp(Box::new(arg)),
2116            "chop" => ExprKind::Chop(Box::new(arg)),
2117            "length" => ExprKind::Length(Box::new(arg)),
2118            "len" | "cnt" => ExprKind::FuncCall {
2119                name: "count".to_string(),
2120                args: vec![arg],
2121            },
2122            "quotemeta" | "qm" => ExprKind::FuncCall {
2123                name: "quotemeta".to_string(),
2124                args: vec![arg],
2125            },
2126            // Numeric functions
2127            "abs" => ExprKind::Abs(Box::new(arg)),
2128            "int" => ExprKind::Int(Box::new(arg)),
2129            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2130            "sin" => ExprKind::Sin(Box::new(arg)),
2131            "cos" => ExprKind::Cos(Box::new(arg)),
2132            "exp" => ExprKind::Exp(Box::new(arg)),
2133            "log" => ExprKind::Log(Box::new(arg)),
2134            "hex" => ExprKind::Hex(Box::new(arg)),
2135            "oct" => ExprKind::Oct(Box::new(arg)),
2136            "chr" => ExprKind::Chr(Box::new(arg)),
2137            "ord" => ExprKind::Ord(Box::new(arg)),
2138            // Type/ref functions
2139            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2140            "ref" => ExprKind::Ref(Box::new(arg)),
2141            "scalar" => ExprKind::ScalarContext(Box::new(arg)),
2142            // Array/hash functions
2143            "keys" => ExprKind::Keys(Box::new(arg)),
2144            "values" => ExprKind::Values(Box::new(arg)),
2145            "each" => ExprKind::Each(Box::new(arg)),
2146            "pop" => ExprKind::Pop(Box::new(arg)),
2147            "shift" => ExprKind::Shift(Box::new(arg)),
2148            "reverse" | "reversed" | "rv" => ExprKind::ReverseExpr(Box::new(arg)),
2149            "rev" => ExprKind::ScalarReverse(Box::new(arg)),
2150            "sort" | "so" => ExprKind::SortExpr {
2151                cmp: None,
2152                list: Box::new(arg),
2153            },
2154            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2155                name: "uniq".to_string(),
2156                args: vec![arg],
2157            },
2158            "trim" | "tm" => ExprKind::FuncCall {
2159                name: "trim".to_string(),
2160                args: vec![arg],
2161            },
2162            "flatten" | "fl" => ExprKind::FuncCall {
2163                name: "flatten".to_string(),
2164                args: vec![arg],
2165            },
2166            "compact" | "cpt" => ExprKind::FuncCall {
2167                name: "compact".to_string(),
2168                args: vec![arg],
2169            },
2170            "shuffle" | "shuf" => ExprKind::FuncCall {
2171                name: "shuffle".to_string(),
2172                args: vec![arg],
2173            },
2174            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2175                name: "frequencies".to_string(),
2176                args: vec![arg],
2177            },
2178            "dedup" | "dup" => ExprKind::FuncCall {
2179                name: "dedup".to_string(),
2180                args: vec![arg],
2181            },
2182            "enumerate" | "en" => ExprKind::FuncCall {
2183                name: "enumerate".to_string(),
2184                args: vec![arg],
2185            },
2186            "lines" | "ln" => ExprKind::FuncCall {
2187                name: "lines".to_string(),
2188                args: vec![arg],
2189            },
2190            "words" | "wd" => ExprKind::FuncCall {
2191                name: "words".to_string(),
2192                args: vec![arg],
2193            },
2194            "chars" | "ch" => ExprKind::FuncCall {
2195                name: "chars".to_string(),
2196                args: vec![arg],
2197            },
2198            "digits" | "dg" => ExprKind::FuncCall {
2199                name: "digits".to_string(),
2200                args: vec![arg],
2201            },
2202            "sentences" | "sents" => ExprKind::FuncCall {
2203                name: "sentences".to_string(),
2204                args: vec![arg],
2205            },
2206            "paragraphs" | "paras" => ExprKind::FuncCall {
2207                name: "paragraphs".to_string(),
2208                args: vec![arg],
2209            },
2210            "sections" | "sects" => ExprKind::FuncCall {
2211                name: "sections".to_string(),
2212                args: vec![arg],
2213            },
2214            "numbers" | "nums" => ExprKind::FuncCall {
2215                name: "numbers".to_string(),
2216                args: vec![arg],
2217            },
2218            "graphemes" | "grs" => ExprKind::FuncCall {
2219                name: "graphemes".to_string(),
2220                args: vec![arg],
2221            },
2222            "columns" | "cols" => ExprKind::FuncCall {
2223                name: "columns".to_string(),
2224                args: vec![arg],
2225            },
2226            // File functions
2227            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2228            "chdir" => ExprKind::Chdir(Box::new(arg)),
2229            "stat" => ExprKind::Stat(Box::new(arg)),
2230            "lstat" => ExprKind::Lstat(Box::new(arg)),
2231            "readlink" => ExprKind::Readlink(Box::new(arg)),
2232            "readdir" => ExprKind::Readdir(Box::new(arg)),
2233            "close" => ExprKind::Close(Box::new(arg)),
2234            "basename" | "bn" => ExprKind::FuncCall {
2235                name: "basename".to_string(),
2236                args: vec![arg],
2237            },
2238            "dirname" | "dn" => ExprKind::FuncCall {
2239                name: "dirname".to_string(),
2240                args: vec![arg],
2241            },
2242            "realpath" | "rp" => ExprKind::FuncCall {
2243                name: "realpath".to_string(),
2244                args: vec![arg],
2245            },
2246            "which" | "wh" => ExprKind::FuncCall {
2247                name: "which".to_string(),
2248                args: vec![arg],
2249            },
2250            // Other
2251            "eval" => ExprKind::Eval(Box::new(arg)),
2252            "require" => ExprKind::Require(Box::new(arg)),
2253            "study" => ExprKind::Study(Box::new(arg)),
2254            // Case conversion
2255            "snake_case" | "sc" => ExprKind::FuncCall {
2256                name: "snake_case".to_string(),
2257                args: vec![arg],
2258            },
2259            "camel_case" | "cc" => ExprKind::FuncCall {
2260                name: "camel_case".to_string(),
2261                args: vec![arg],
2262            },
2263            "kebab_case" | "kc" => ExprKind::FuncCall {
2264                name: "kebab_case".to_string(),
2265                args: vec![arg],
2266            },
2267            // Serialization
2268            "to_json" | "tj" => ExprKind::FuncCall {
2269                name: "to_json".to_string(),
2270                args: vec![arg],
2271            },
2272            "to_yaml" | "ty" => ExprKind::FuncCall {
2273                name: "to_yaml".to_string(),
2274                args: vec![arg],
2275            },
2276            "to_toml" | "tt" => ExprKind::FuncCall {
2277                name: "to_toml".to_string(),
2278                args: vec![arg],
2279            },
2280            "to_csv" | "tc" => ExprKind::FuncCall {
2281                name: "to_csv".to_string(),
2282                args: vec![arg],
2283            },
2284            "to_xml" | "tx" => ExprKind::FuncCall {
2285                name: "to_xml".to_string(),
2286                args: vec![arg],
2287            },
2288            "to_html" | "th" => ExprKind::FuncCall {
2289                name: "to_html".to_string(),
2290                args: vec![arg],
2291            },
2292            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2293                name: "to_markdown".to_string(),
2294                args: vec![arg],
2295            },
2296            "xopen" | "xo" => ExprKind::FuncCall {
2297                name: "xopen".to_string(),
2298                args: vec![arg],
2299            },
2300            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2301                name: "clip".to_string(),
2302                args: vec![arg],
2303            },
2304            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2305                name: "to_table".to_string(),
2306                args: vec![arg],
2307            },
2308            "sparkline" | "spark" => ExprKind::FuncCall {
2309                name: "sparkline".to_string(),
2310                args: vec![arg],
2311            },
2312            "bar_chart" | "bars" => ExprKind::FuncCall {
2313                name: "bar_chart".to_string(),
2314                args: vec![arg],
2315            },
2316            "flame" | "flamechart" => ExprKind::FuncCall {
2317                name: "flame".to_string(),
2318                args: vec![arg],
2319            },
2320            "ddump" | "dd" => ExprKind::FuncCall {
2321                name: "ddump".to_string(),
2322                args: vec![arg],
2323            },
2324            "stringify" | "str" => ExprKind::FuncCall {
2325                name: "stringify".to_string(),
2326                args: vec![arg],
2327            },
2328            "json_decode" | "jd" => ExprKind::FuncCall {
2329                name: "json_decode".to_string(),
2330                args: vec![arg],
2331            },
2332            "yaml_decode" | "yd" => ExprKind::FuncCall {
2333                name: "yaml_decode".to_string(),
2334                args: vec![arg],
2335            },
2336            "toml_decode" | "td" => ExprKind::FuncCall {
2337                name: "toml_decode".to_string(),
2338                args: vec![arg],
2339            },
2340            "xml_decode" | "xd" => ExprKind::FuncCall {
2341                name: "xml_decode".to_string(),
2342                args: vec![arg],
2343            },
2344            "json_encode" | "je" => ExprKind::FuncCall {
2345                name: "json_encode".to_string(),
2346                args: vec![arg],
2347            },
2348            "yaml_encode" | "ye" => ExprKind::FuncCall {
2349                name: "yaml_encode".to_string(),
2350                args: vec![arg],
2351            },
2352            "toml_encode" | "te" => ExprKind::FuncCall {
2353                name: "toml_encode".to_string(),
2354                args: vec![arg],
2355            },
2356            "xml_encode" | "xe" => ExprKind::FuncCall {
2357                name: "xml_encode".to_string(),
2358                args: vec![arg],
2359            },
2360            // Encoding
2361            "base64_encode" | "b64e" => ExprKind::FuncCall {
2362                name: "base64_encode".to_string(),
2363                args: vec![arg],
2364            },
2365            "base64_decode" | "b64d" => ExprKind::FuncCall {
2366                name: "base64_decode".to_string(),
2367                args: vec![arg],
2368            },
2369            "hex_encode" | "hxe" => ExprKind::FuncCall {
2370                name: "hex_encode".to_string(),
2371                args: vec![arg],
2372            },
2373            "hex_decode" | "hxd" => ExprKind::FuncCall {
2374                name: "hex_decode".to_string(),
2375                args: vec![arg],
2376            },
2377            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
2378                name: "url_encode".to_string(),
2379                args: vec![arg],
2380            },
2381            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
2382                name: "url_decode".to_string(),
2383                args: vec![arg],
2384            },
2385            "gzip" | "gz" => ExprKind::FuncCall {
2386                name: "gzip".to_string(),
2387                args: vec![arg],
2388            },
2389            "gunzip" | "ugz" => ExprKind::FuncCall {
2390                name: "gunzip".to_string(),
2391                args: vec![arg],
2392            },
2393            "zstd" | "zst" => ExprKind::FuncCall {
2394                name: "zstd".to_string(),
2395                args: vec![arg],
2396            },
2397            "zstd_decode" | "uzst" => ExprKind::FuncCall {
2398                name: "zstd_decode".to_string(),
2399                args: vec![arg],
2400            },
2401            // Crypto
2402            "sha256" | "s256" => ExprKind::FuncCall {
2403                name: "sha256".to_string(),
2404                args: vec![arg],
2405            },
2406            "sha1" | "s1" => ExprKind::FuncCall {
2407                name: "sha1".to_string(),
2408                args: vec![arg],
2409            },
2410            "md5" | "m5" => ExprKind::FuncCall {
2411                name: "md5".to_string(),
2412                args: vec![arg],
2413            },
2414            "uuid" | "uid" => ExprKind::FuncCall {
2415                name: "uuid".to_string(),
2416                args: vec![arg],
2417            },
2418            // Datetime
2419            "datetime_utc" | "utc" => ExprKind::FuncCall {
2420                name: "datetime_utc".to_string(),
2421                args: vec![arg],
2422            },
2423            // Output
2424            "p" | "say" => ExprKind::Say {
2425                handle: None,
2426                args: vec![arg],
2427            },
2428            "print" | "pr" => ExprKind::Print {
2429                handle: None,
2430                args: vec![arg],
2431            },
2432            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
2433            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
2434            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
2435                block: vec![Statement {
2436                    label: None,
2437                    kind: StmtKind::Expression(Expr {
2438                        kind: ExprKind::Say {
2439                            handle: None,
2440                            args: vec![Expr {
2441                                kind: ExprKind::ScalarVar("_".into()),
2442                                line,
2443                            }],
2444                        },
2445                        line,
2446                    }),
2447                    line,
2448                }],
2449                list: Box::new(arg),
2450            },
2451            // Default: generic function call
2452            _ => ExprKind::FuncCall {
2453                name: name.to_string(),
2454                args: vec![arg],
2455            },
2456        };
2457        Ok(Expr { kind, line })
2458    }
2459
2460    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
2461    /// In thread context, we only parse the block - the list comes from the piped result.
2462    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
2463        let block = self.parse_block()?;
2464        // Use a placeholder for the list - pipe_forward_apply will replace it
2465        let placeholder = self.pipe_placeholder_list(line);
2466
2467        match name {
2468            "map" | "flat_map" | "maps" | "flat_maps" => {
2469                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
2470                let stream = matches!(name, "maps" | "flat_maps");
2471                Ok(Expr {
2472                    kind: ExprKind::MapExpr {
2473                        block,
2474                        list: Box::new(placeholder),
2475                        flatten_array_refs,
2476                        stream,
2477                    },
2478                    line,
2479                })
2480            }
2481            "grep" | "greps" | "filter" | "f" | "find_all" | "gr" => {
2482                let keyword = match name {
2483                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
2484                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
2485                    "filter" | "f" => crate::ast::GrepBuiltinKeyword::Filter,
2486                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
2487                    _ => unreachable!(),
2488                };
2489                Ok(Expr {
2490                    kind: ExprKind::GrepExpr {
2491                        block,
2492                        list: Box::new(placeholder),
2493                        keyword,
2494                    },
2495                    line,
2496                })
2497            }
2498            "sort" | "so" => Ok(Expr {
2499                kind: ExprKind::SortExpr {
2500                    cmp: Some(SortComparator::Block(block)),
2501                    list: Box::new(placeholder),
2502                },
2503                line,
2504            }),
2505            "reduce" | "rd" => Ok(Expr {
2506                kind: ExprKind::ReduceExpr {
2507                    block,
2508                    list: Box::new(placeholder),
2509                },
2510                line,
2511            }),
2512            "fore" | "e" | "ep" => Ok(Expr {
2513                kind: ExprKind::ForEachExpr {
2514                    block,
2515                    list: Box::new(placeholder),
2516                },
2517                line,
2518            }),
2519            _ => {
2520                // Generic: parse block and treat as FuncCall with code ref arg
2521                let code_ref = Expr {
2522                    kind: ExprKind::CodeRef {
2523                        params: vec![],
2524                        body: block,
2525                    },
2526                    line,
2527                };
2528                Ok(Expr {
2529                    kind: ExprKind::FuncCall {
2530                        name: name.to_string(),
2531                        args: vec![code_ref],
2532                    },
2533                    line,
2534                })
2535            }
2536        }
2537    }
2538
2539    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
2540    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
2541        let line = self.peek_line();
2542        self.advance(); // tie
2543        let target = match self.peek().clone() {
2544            Token::HashVar(h) => {
2545                self.advance();
2546                TieTarget::Hash(h)
2547            }
2548            Token::ArrayVar(a) => {
2549                self.advance();
2550                TieTarget::Array(a)
2551            }
2552            Token::ScalarVar(s) => {
2553                self.advance();
2554                TieTarget::Scalar(s)
2555            }
2556            tok => {
2557                return Err(self.syntax_err(
2558                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
2559                    self.peek_line(),
2560                ));
2561            }
2562        };
2563        self.expect(&Token::Comma)?;
2564        let class = self.parse_assign_expr()?;
2565        let mut args = Vec::new();
2566        while self.eat(&Token::Comma) {
2567            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
2568                break;
2569            }
2570            args.push(self.parse_assign_expr()?);
2571        }
2572        self.eat(&Token::Semicolon);
2573        Ok(Statement {
2574            label: None,
2575            kind: StmtKind::Tie {
2576                target,
2577                class,
2578                args,
2579            },
2580            line,
2581        })
2582    }
2583
2584    /// `given (EXPR) { ... }`
2585    fn parse_given(&mut self) -> PerlResult<Statement> {
2586        let line = self.peek_line();
2587        self.advance();
2588        self.expect(&Token::LParen)?;
2589        let topic = self.parse_expression()?;
2590        self.expect(&Token::RParen)?;
2591        let body = self.parse_block()?;
2592        self.eat(&Token::Semicolon);
2593        Ok(Statement {
2594            label: None,
2595            kind: StmtKind::Given { topic, body },
2596            line,
2597        })
2598    }
2599
2600    /// `when (COND) { ... }` — only meaningful inside `given`
2601    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
2602        let line = self.peek_line();
2603        self.advance();
2604        self.expect(&Token::LParen)?;
2605        let cond = self.parse_expression()?;
2606        self.expect(&Token::RParen)?;
2607        let body = self.parse_block()?;
2608        self.eat(&Token::Semicolon);
2609        Ok(Statement {
2610            label: None,
2611            kind: StmtKind::When { cond, body },
2612            line,
2613        })
2614    }
2615
2616    /// `default { ... }` — only meaningful inside `given`
2617    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
2618        let line = self.peek_line();
2619        self.advance();
2620        let body = self.parse_block()?;
2621        self.eat(&Token::Semicolon);
2622        Ok(Statement {
2623            label: None,
2624            kind: StmtKind::DefaultCase { body },
2625            line,
2626        })
2627    }
2628
2629    /// `match (EXPR) { PATTERN => EXPR, ... }`
2630    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
2631        self.expect(&Token::LParen)?;
2632        let subject = self.parse_expression()?;
2633        self.expect(&Token::RParen)?;
2634        self.expect(&Token::LBrace)?;
2635        let mut arms = Vec::new();
2636        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2637            if self.eat(&Token::Semicolon) {
2638                continue;
2639            }
2640            let pattern = self.parse_match_pattern()?;
2641            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
2642                self.advance();
2643                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
2644                // separator (see [`Self::parse_comma_expr`]).
2645                Some(Box::new(self.parse_assign_expr()?))
2646            } else {
2647                None
2648            };
2649            self.expect(&Token::FatArrow)?;
2650            // Use assign-level parsing so commas separate arms, not `List` elements.
2651            let body = self.parse_assign_expr()?;
2652            arms.push(MatchArm {
2653                pattern,
2654                guard,
2655                body,
2656            });
2657            self.eat(&Token::Comma);
2658        }
2659        self.expect(&Token::RBrace)?;
2660        Ok(Expr {
2661            kind: ExprKind::AlgebraicMatch {
2662                subject: Box::new(subject),
2663                arms,
2664            },
2665            line,
2666        })
2667    }
2668
2669    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
2670        match self.peek().clone() {
2671            Token::Regex(pattern, flags, _delim) => {
2672                self.advance();
2673                Ok(MatchPattern::Regex { pattern, flags })
2674            }
2675            Token::Ident(ref s) if s == "_" => {
2676                self.advance();
2677                Ok(MatchPattern::Any)
2678            }
2679            Token::Ident(ref s) if s == "Some" => {
2680                self.advance();
2681                self.expect(&Token::LParen)?;
2682                let name = self.parse_scalar_var_name()?;
2683                self.expect(&Token::RParen)?;
2684                Ok(MatchPattern::OptionSome(name))
2685            }
2686            Token::LBracket => self.parse_match_array_pattern(),
2687            Token::LBrace => self.parse_match_hash_pattern(),
2688            Token::LParen => {
2689                self.advance();
2690                let e = self.parse_expression()?;
2691                self.expect(&Token::RParen)?;
2692                Ok(MatchPattern::Value(Box::new(e)))
2693            }
2694            _ => {
2695                let e = self.parse_assign_expr()?;
2696                Ok(MatchPattern::Value(Box::new(e)))
2697            }
2698        }
2699    }
2700
2701    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
2702    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
2703        let mut elems = Vec::new();
2704        if self.eat(&Token::RBracket) {
2705            return Ok(vec![]);
2706        }
2707        loop {
2708            if matches!(self.peek(), Token::Star) {
2709                self.advance();
2710                elems.push(MatchArrayElem::Rest);
2711                self.eat(&Token::Comma);
2712                if !matches!(self.peek(), Token::RBracket) {
2713                    return Err(self.syntax_err(
2714                        "`*` must be the last element in an array match pattern",
2715                        self.peek_line(),
2716                    ));
2717                }
2718                self.expect(&Token::RBracket)?;
2719                return Ok(elems);
2720            }
2721            if let Token::ArrayVar(name) = self.peek().clone() {
2722                self.advance();
2723                elems.push(MatchArrayElem::RestBind(name));
2724                self.eat(&Token::Comma);
2725                if !matches!(self.peek(), Token::RBracket) {
2726                    return Err(self.syntax_err(
2727                        "`@name` rest bind must be the last element in an array match pattern",
2728                        self.peek_line(),
2729                    ));
2730                }
2731                self.expect(&Token::RBracket)?;
2732                return Ok(elems);
2733            }
2734            if let Token::ScalarVar(name) = self.peek().clone() {
2735                self.advance();
2736                elems.push(MatchArrayElem::CaptureScalar(name));
2737                if self.eat(&Token::Comma) {
2738                    if matches!(self.peek(), Token::RBracket) {
2739                        break;
2740                    }
2741                    continue;
2742                }
2743                break;
2744            }
2745            let e = self.parse_assign_expr()?;
2746            elems.push(MatchArrayElem::Expr(e));
2747            if self.eat(&Token::Comma) {
2748                if matches!(self.peek(), Token::RBracket) {
2749                    break;
2750                }
2751                continue;
2752            }
2753            break;
2754        }
2755        self.expect(&Token::RBracket)?;
2756        Ok(elems)
2757    }
2758
2759    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
2760        self.expect(&Token::LBracket)?;
2761        let elems = self.parse_match_array_elems_until_rbracket()?;
2762        Ok(MatchPattern::Array(elems))
2763    }
2764
2765    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
2766        self.expect(&Token::LBrace)?;
2767        let mut pairs = Vec::new();
2768        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2769            if self.eat(&Token::Semicolon) {
2770                continue;
2771            }
2772            let key = self.parse_assign_expr()?;
2773            self.expect(&Token::FatArrow)?;
2774            match self.advance().0 {
2775                Token::Ident(ref s) if s == "_" => {
2776                    pairs.push(MatchHashPair::KeyOnly { key });
2777                }
2778                Token::ScalarVar(name) => {
2779                    pairs.push(MatchHashPair::Capture { key, name });
2780                }
2781                tok => {
2782                    return Err(self.syntax_err(
2783                        format!(
2784                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
2785                            tok
2786                        ),
2787                        self.peek_line(),
2788                    ));
2789                }
2790            }
2791            self.eat(&Token::Comma);
2792        }
2793        self.expect(&Token::RBrace)?;
2794        Ok(MatchPattern::Hash(pairs))
2795    }
2796
2797    /// `eval_timeout SECS { ... }`
2798    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
2799        let line = self.peek_line();
2800        self.advance();
2801        let timeout = self.parse_postfix()?;
2802        let body = self.parse_block_or_bareword_block_no_args()?;
2803        self.eat(&Token::Semicolon);
2804        Ok(Statement {
2805            label: None,
2806            kind: StmtKind::EvalTimeout { timeout, body },
2807            line,
2808        })
2809    }
2810
2811    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
2812        match &mut cond.kind {
2813            ExprKind::Match {
2814                flags, scalar_g, ..
2815            } if flags.contains('g') => {
2816                *scalar_g = true;
2817            }
2818            ExprKind::UnaryOp {
2819                op: UnaryOp::LogNot,
2820                expr,
2821            } => {
2822                if let ExprKind::Match {
2823                    flags, scalar_g, ..
2824                } = &mut expr.kind
2825                {
2826                    if flags.contains('g') {
2827                        *scalar_g = true;
2828                    }
2829                }
2830            }
2831            _ => {}
2832        }
2833    }
2834
2835    fn parse_if(&mut self) -> PerlResult<Statement> {
2836        let line = self.peek_line();
2837        self.advance(); // 'if'
2838        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
2839            if crate::compat_mode() {
2840                return Err(self.syntax_err(
2841                    "`if let` is a stryke extension (disabled by --compat)",
2842                    line,
2843                ));
2844            }
2845            return self.parse_if_let(line);
2846        }
2847        self.expect(&Token::LParen)?;
2848        let mut cond = self.parse_expression()?;
2849        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
2850        self.expect(&Token::RParen)?;
2851        let body = self.parse_block()?;
2852
2853        let mut elsifs = Vec::new();
2854        let mut else_block = None;
2855
2856        loop {
2857            if let Token::Ident(ref kw) = self.peek().clone() {
2858                if kw == "elsif" {
2859                    self.advance();
2860                    self.expect(&Token::LParen)?;
2861                    let mut c = self.parse_expression()?;
2862                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
2863                    self.expect(&Token::RParen)?;
2864                    let b = self.parse_block()?;
2865                    elsifs.push((c, b));
2866                    continue;
2867                }
2868                if kw == "else" {
2869                    self.advance();
2870                    else_block = Some(self.parse_block()?);
2871                }
2872            }
2873            break;
2874        }
2875
2876        Ok(Statement {
2877            label: None,
2878            kind: StmtKind::If {
2879                condition: cond,
2880                body,
2881                elsifs,
2882                else_block,
2883            },
2884            line,
2885        })
2886    }
2887
2888    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
2889    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
2890        self.advance(); // `let`
2891        let pattern = self.parse_match_pattern()?;
2892        self.expect(&Token::Assign)?;
2893        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
2894        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
2895        let rhs = self.parse_assign_expr();
2896        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
2897        let rhs = rhs?;
2898        let then_block = self.parse_block()?;
2899        let else_block_opt = match self.peek().clone() {
2900            Token::Ident(ref kw) if kw == "else" => {
2901                self.advance();
2902                Some(self.parse_block()?)
2903            }
2904            Token::Ident(ref kw) if kw == "elsif" => {
2905                return Err(self.syntax_err(
2906                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
2907                    self.peek_line(),
2908                ));
2909            }
2910            _ => None,
2911        };
2912        let then_expr = Self::expr_do_anon_block(then_block, line);
2913        let else_expr = if let Some(eb) = else_block_opt {
2914            Self::expr_do_anon_block(eb, line)
2915        } else {
2916            Expr {
2917                kind: ExprKind::Undef,
2918                line,
2919            }
2920        };
2921        let arms = vec![
2922            MatchArm {
2923                pattern,
2924                guard: None,
2925                body: then_expr,
2926            },
2927            MatchArm {
2928                pattern: MatchPattern::Any,
2929                guard: None,
2930                body: else_expr,
2931            },
2932        ];
2933        Ok(Statement {
2934            label: None,
2935            kind: StmtKind::Expression(Expr {
2936                kind: ExprKind::AlgebraicMatch {
2937                    subject: Box::new(rhs),
2938                    arms,
2939                },
2940                line,
2941            }),
2942            line,
2943        })
2944    }
2945
2946    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
2947        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
2948        Expr {
2949            kind: ExprKind::Do(Box::new(Expr {
2950                kind: ExprKind::CodeRef {
2951                    params: vec![],
2952                    body: block,
2953                },
2954                line: inner_line,
2955            })),
2956            line: outer_line,
2957        }
2958    }
2959
2960    fn parse_unless(&mut self) -> PerlResult<Statement> {
2961        let line = self.peek_line();
2962        self.advance(); // 'unless'
2963        self.expect(&Token::LParen)?;
2964        let mut cond = self.parse_expression()?;
2965        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
2966        self.expect(&Token::RParen)?;
2967        let body = self.parse_block()?;
2968        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
2969            if kw == "else" {
2970                self.advance();
2971                Some(self.parse_block()?)
2972            } else {
2973                None
2974            }
2975        } else {
2976            None
2977        };
2978        Ok(Statement {
2979            label: None,
2980            kind: StmtKind::Unless {
2981                condition: cond,
2982                body,
2983                else_block,
2984            },
2985            line,
2986        })
2987    }
2988
2989    fn parse_while(&mut self) -> PerlResult<Statement> {
2990        let line = self.peek_line();
2991        self.advance(); // 'while'
2992        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
2993            if crate::compat_mode() {
2994                return Err(self.syntax_err(
2995                    "`while let` is a stryke extension (disabled by --compat)",
2996                    line,
2997                ));
2998            }
2999            return self.parse_while_let(line);
3000        }
3001        self.expect(&Token::LParen)?;
3002        let mut cond = self.parse_expression()?;
3003        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3004        self.expect(&Token::RParen)?;
3005        let body = self.parse_block()?;
3006        let continue_block = self.parse_optional_continue_block()?;
3007        Ok(Statement {
3008            label: None,
3009            kind: StmtKind::While {
3010                condition: cond,
3011                body,
3012                label: None,
3013                continue_block,
3014            },
3015            line,
3016        })
3017    }
3018
3019    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3020    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3021    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3022        self.advance(); // `let`
3023        let pattern = self.parse_match_pattern()?;
3024        self.expect(&Token::Assign)?;
3025        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3026        let rhs = self.parse_assign_expr();
3027        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3028        let rhs = rhs?;
3029        let mut user_body = self.parse_block()?;
3030        let continue_block = self.parse_optional_continue_block()?;
3031        user_body.push(Statement::new(
3032            StmtKind::Expression(Expr {
3033                kind: ExprKind::Integer(1),
3034                line,
3035            }),
3036            line,
3037        ));
3038        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3039        let match_expr = Expr {
3040            kind: ExprKind::AlgebraicMatch {
3041                subject: Box::new(rhs),
3042                arms: vec![
3043                    MatchArm {
3044                        pattern,
3045                        guard: None,
3046                        body: Self::expr_do_anon_block(user_body, line),
3047                    },
3048                    MatchArm {
3049                        pattern: MatchPattern::Any,
3050                        guard: None,
3051                        body: Expr {
3052                            kind: ExprKind::Integer(0),
3053                            line,
3054                        },
3055                    },
3056                ],
3057            },
3058            line,
3059        };
3060        let my_stmt = Statement::new(
3061            StmtKind::My(vec![VarDecl {
3062                sigil: Sigil::Scalar,
3063                name: tmp.clone(),
3064                initializer: Some(match_expr),
3065                frozen: false,
3066                type_annotation: None,
3067            }]),
3068            line,
3069        );
3070        let unless_last = Statement::new(
3071            StmtKind::Unless {
3072                condition: Expr {
3073                    kind: ExprKind::ScalarVar(tmp),
3074                    line,
3075                },
3076                body: vec![Statement::new(StmtKind::Last(None), line)],
3077                else_block: None,
3078            },
3079            line,
3080        );
3081        Ok(Statement::new(
3082            StmtKind::While {
3083                condition: Expr {
3084                    kind: ExprKind::Integer(1),
3085                    line,
3086                },
3087                body: vec![my_stmt, unless_last],
3088                label: None,
3089                continue_block,
3090            },
3091            line,
3092        ))
3093    }
3094
3095    fn parse_until(&mut self) -> PerlResult<Statement> {
3096        let line = self.peek_line();
3097        self.advance(); // 'until'
3098        self.expect(&Token::LParen)?;
3099        let mut cond = self.parse_expression()?;
3100        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3101        self.expect(&Token::RParen)?;
3102        let body = self.parse_block()?;
3103        let continue_block = self.parse_optional_continue_block()?;
3104        Ok(Statement {
3105            label: None,
3106            kind: StmtKind::Until {
3107                condition: cond,
3108                body,
3109                label: None,
3110                continue_block,
3111            },
3112            line,
3113        })
3114    }
3115
3116    /// `continue { ... }` after a loop body (optional).
3117    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3118        if let Token::Ident(ref kw) = self.peek().clone() {
3119            if kw == "continue" {
3120                self.advance();
3121                return Ok(Some(self.parse_block()?));
3122            }
3123        }
3124        Ok(None)
3125    }
3126
3127    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3128        let line = self.peek_line();
3129        self.advance(); // 'for'
3130
3131        // Peek to determine if C-style for or foreach
3132        // C-style: for (init; cond; step)
3133        // foreach-style: for $var (list) or for (list)
3134        match self.peek() {
3135            Token::LParen => {
3136                // Check if next after ( is a semicolon or an assignment — C-style
3137                // Or if it's a list — foreach-style
3138                // Heuristic: if the token after ( is 'my' or '$' followed by
3139                // content that contains ';', it's C-style.
3140                let saved = self.pos;
3141                self.advance(); // consume (
3142                                // Look for semicolon at paren depth 0
3143                let mut depth = 1;
3144                let mut has_semi = false;
3145                let mut scan = self.pos;
3146                while scan < self.tokens.len() {
3147                    match &self.tokens[scan].0 {
3148                        Token::LParen => depth += 1,
3149                        Token::RParen => {
3150                            depth -= 1;
3151                            if depth == 0 {
3152                                break;
3153                            }
3154                        }
3155                        Token::Semicolon if depth == 1 => {
3156                            has_semi = true;
3157                            break;
3158                        }
3159                        _ => {}
3160                    }
3161                    scan += 1;
3162                }
3163                self.pos = saved;
3164
3165                if has_semi {
3166                    self.parse_c_style_for(line)
3167                } else {
3168                    // foreach without explicit var — uses $_
3169                    self.expect(&Token::LParen)?;
3170                    let list = self.parse_expression()?;
3171                    self.expect(&Token::RParen)?;
3172                    let body = self.parse_block()?;
3173                    let continue_block = self.parse_optional_continue_block()?;
3174                    Ok(Statement {
3175                        label: None,
3176                        kind: StmtKind::Foreach {
3177                            var: "_".to_string(),
3178                            list,
3179                            body,
3180                            label: None,
3181                            continue_block,
3182                        },
3183                        line,
3184                    })
3185                }
3186            }
3187            Token::Ident(ref kw) if kw == "my" => {
3188                self.advance(); // 'my'
3189                let var = self.parse_scalar_var_name()?;
3190                self.expect(&Token::LParen)?;
3191                let list = self.parse_expression()?;
3192                self.expect(&Token::RParen)?;
3193                let body = self.parse_block()?;
3194                let continue_block = self.parse_optional_continue_block()?;
3195                Ok(Statement {
3196                    label: None,
3197                    kind: StmtKind::Foreach {
3198                        var,
3199                        list,
3200                        body,
3201                        label: None,
3202                        continue_block,
3203                    },
3204                    line,
3205                })
3206            }
3207            Token::ScalarVar(_) => {
3208                let var = self.parse_scalar_var_name()?;
3209                self.expect(&Token::LParen)?;
3210                let list = self.parse_expression()?;
3211                self.expect(&Token::RParen)?;
3212                let body = self.parse_block()?;
3213                let continue_block = self.parse_optional_continue_block()?;
3214                Ok(Statement {
3215                    label: None,
3216                    kind: StmtKind::Foreach {
3217                        var,
3218                        list,
3219                        body,
3220                        label: None,
3221                        continue_block,
3222                    },
3223                    line,
3224                })
3225            }
3226            _ => self.parse_c_style_for(line),
3227        }
3228    }
3229
3230    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
3231        self.expect(&Token::LParen)?;
3232        let init = if self.eat(&Token::Semicolon) {
3233            None
3234        } else {
3235            let s = self.parse_statement()?;
3236            self.eat(&Token::Semicolon);
3237            Some(Box::new(s))
3238        };
3239        let mut condition = if matches!(self.peek(), Token::Semicolon) {
3240            None
3241        } else {
3242            Some(self.parse_expression()?)
3243        };
3244        if let Some(ref mut c) = condition {
3245            Self::mark_match_scalar_g_for_boolean_condition(c);
3246        }
3247        self.expect(&Token::Semicolon)?;
3248        let step = if matches!(self.peek(), Token::RParen) {
3249            None
3250        } else {
3251            Some(self.parse_expression()?)
3252        };
3253        self.expect(&Token::RParen)?;
3254        let body = self.parse_block()?;
3255        let continue_block = self.parse_optional_continue_block()?;
3256        Ok(Statement {
3257            label: None,
3258            kind: StmtKind::For {
3259                init,
3260                condition,
3261                step,
3262                body,
3263                label: None,
3264                continue_block,
3265            },
3266            line,
3267        })
3268    }
3269
3270    fn parse_foreach(&mut self) -> PerlResult<Statement> {
3271        let line = self.peek_line();
3272        self.advance(); // 'foreach'
3273        let var = match self.peek() {
3274            Token::Ident(ref kw) if kw == "my" => {
3275                self.advance();
3276                self.parse_scalar_var_name()?
3277            }
3278            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
3279            _ => "_".to_string(),
3280        };
3281        self.expect(&Token::LParen)?;
3282        let list = self.parse_expression()?;
3283        self.expect(&Token::RParen)?;
3284        let body = self.parse_block()?;
3285        let continue_block = self.parse_optional_continue_block()?;
3286        Ok(Statement {
3287            label: None,
3288            kind: StmtKind::Foreach {
3289                var,
3290                list,
3291                body,
3292                label: None,
3293                continue_block,
3294            },
3295            line,
3296        })
3297    }
3298
3299    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
3300        match self.advance() {
3301            (Token::ScalarVar(name), _) => Ok(name),
3302            (tok, line) => {
3303                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
3304            }
3305        }
3306    }
3307
3308    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
3309    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
3310        let mut s = String::new();
3311        loop {
3312            match self.peek().clone() {
3313                Token::RParen => {
3314                    self.advance();
3315                    break;
3316                }
3317                Token::Eof => {
3318                    return Err(self.syntax_err(
3319                        "Unterminated sub prototype (expected ')' before end of input)",
3320                        self.peek_line(),
3321                    ));
3322                }
3323                Token::ScalarVar(v) if v == ")" => {
3324                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
3325                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
3326                    self.advance();
3327                    s.push('$');
3328                    if matches!(self.peek(), Token::LBrace) {
3329                        break;
3330                    }
3331                }
3332                Token::Ident(i) => {
3333                    let i = i.clone();
3334                    self.advance();
3335                    s.push_str(&i);
3336                }
3337                Token::Semicolon => {
3338                    self.advance();
3339                    s.push(';');
3340                }
3341                Token::LParen => {
3342                    self.advance();
3343                    s.push('(');
3344                }
3345                Token::LBracket => {
3346                    self.advance();
3347                    s.push('[');
3348                }
3349                Token::RBracket => {
3350                    self.advance();
3351                    s.push(']');
3352                }
3353                Token::Backslash => {
3354                    self.advance();
3355                    s.push('\\');
3356                }
3357                Token::Comma => {
3358                    self.advance();
3359                    s.push(',');
3360                }
3361                Token::ScalarVar(v) => {
3362                    let v = v.clone();
3363                    self.advance();
3364                    s.push('$');
3365                    s.push_str(&v);
3366                }
3367                Token::ArrayVar(v) => {
3368                    let v = v.clone();
3369                    self.advance();
3370                    s.push('@');
3371                    s.push_str(&v);
3372                }
3373                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
3374                Token::ArrayAt => {
3375                    self.advance();
3376                    s.push('@');
3377                }
3378                Token::HashVar(v) => {
3379                    let v = v.clone();
3380                    self.advance();
3381                    s.push('%');
3382                    s.push_str(&v);
3383                }
3384                Token::HashPercent => {
3385                    self.advance();
3386                    s.push('%');
3387                }
3388                Token::Plus => {
3389                    self.advance();
3390                    s.push('+');
3391                }
3392                Token::Minus => {
3393                    self.advance();
3394                    s.push('-');
3395                }
3396                Token::BitAnd => {
3397                    self.advance();
3398                    s.push('&');
3399                }
3400                tok => {
3401                    return Err(self.syntax_err(
3402                        format!("Unexpected token in sub prototype: {:?}", tok),
3403                        self.peek_line(),
3404                    ));
3405                }
3406            }
3407        }
3408        Ok(s)
3409    }
3410
3411    fn sub_signature_list_starts_here(&self) -> bool {
3412        match self.peek() {
3413            Token::LBrace | Token::LBracket => true,
3414            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
3415            _ => false,
3416        }
3417    }
3418
3419    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
3420        let (tok, line) = self.advance();
3421        match tok {
3422            Token::Ident(i) => Ok(i),
3423            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
3424            tok => Err(self.syntax_err(
3425                format!(
3426                    "sub signature: expected hash key (identifier or string), got {:?}",
3427                    tok
3428                ),
3429                line,
3430            )),
3431        }
3432    }
3433
3434    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
3435        let mut params = Vec::new();
3436        loop {
3437            if matches!(self.peek(), Token::RParen) {
3438                break;
3439            }
3440            match self.peek().clone() {
3441                Token::ScalarVar(name) => {
3442                    if name == "$$" || name == ")" {
3443                        return Err(self.syntax_err(
3444                            format!(
3445                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
3446                            ),
3447                            self.peek_line(),
3448                        ));
3449                    }
3450                    self.advance();
3451                    let ty = if self.eat(&Token::Colon) {
3452                        match self.peek() {
3453                            Token::Ident(ref tname) => {
3454                                let tname = tname.clone();
3455                                self.advance();
3456                                Some(match tname.as_str() {
3457                                    "Int" => PerlTypeName::Int,
3458                                    "Str" => PerlTypeName::Str,
3459                                    "Float" => PerlTypeName::Float,
3460                                    "Bool" => PerlTypeName::Bool,
3461                                    "Array" => PerlTypeName::Array,
3462                                    "Hash" => PerlTypeName::Hash,
3463                                    "Ref" => PerlTypeName::Ref,
3464                                    "Any" => PerlTypeName::Any,
3465                                    _ => PerlTypeName::Struct(tname),
3466                                })
3467                            }
3468                            _ => {
3469                                return Err(self.syntax_err(
3470                                    "expected type name after `:` in sub signature",
3471                                    self.peek_line(),
3472                                ));
3473                            }
3474                        }
3475                    } else {
3476                        None
3477                    };
3478                    params.push(SubSigParam::Scalar(name, ty));
3479                }
3480                Token::LBracket => {
3481                    self.advance();
3482                    let elems = self.parse_match_array_elems_until_rbracket()?;
3483                    params.push(SubSigParam::ArrayDestruct(elems));
3484                }
3485                Token::LBrace => {
3486                    self.advance();
3487                    let mut pairs = Vec::new();
3488                    loop {
3489                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
3490                            break;
3491                        }
3492                        if self.eat(&Token::Comma) {
3493                            continue;
3494                        }
3495                        let key = self.parse_sub_signature_hash_key()?;
3496                        self.expect(&Token::FatArrow)?;
3497                        let bind = self.parse_scalar_var_name()?;
3498                        pairs.push((key, bind));
3499                        self.eat(&Token::Comma);
3500                    }
3501                    self.expect(&Token::RBrace)?;
3502                    params.push(SubSigParam::HashDestruct(pairs));
3503                }
3504                tok => {
3505                    return Err(self.syntax_err(
3506                        format!(
3507                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
3508                            tok
3509                        ),
3510                        self.peek_line(),
3511                    ));
3512                }
3513            }
3514            match self.peek() {
3515                Token::Comma => {
3516                    self.advance();
3517                    if matches!(self.peek(), Token::RParen) {
3518                        return Err(self.syntax_err(
3519                            "trailing `,` before `)` in sub signature",
3520                            self.peek_line(),
3521                        ));
3522                    }
3523                }
3524                Token::RParen => break,
3525                _ => {
3526                    return Err(self.syntax_err(
3527                        format!(
3528                            "expected `,` or `)` after sub signature parameter, got {:?}",
3529                            self.peek()
3530                        ),
3531                        self.peek_line(),
3532                    ));
3533                }
3534            }
3535        }
3536        Ok(params)
3537    }
3538
3539    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
3540    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
3541        if !matches!(self.peek(), Token::LParen) {
3542            return Ok((vec![], None));
3543        }
3544        self.advance();
3545        if matches!(self.peek(), Token::RParen) {
3546            self.advance();
3547            return Ok((vec![], Some(String::new())));
3548        }
3549        if self.sub_signature_list_starts_here() {
3550            let params = self.parse_sub_signature_param_list()?;
3551            self.expect(&Token::RParen)?;
3552            return Ok((params, None));
3553        }
3554        let proto = self.parse_legacy_sub_prototype_tail()?;
3555        Ok((vec![], Some(proto)))
3556    }
3557
3558    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
3559    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
3560        while self.eat(&Token::Colon) {
3561            match self.advance() {
3562                (Token::Ident(_), _) => {}
3563                (tok, line) => {
3564                    return Err(self.syntax_err(
3565                        format!("Expected attribute name after `:`, got {:?}", tok),
3566                        line,
3567                    ));
3568                }
3569            }
3570            if self.eat(&Token::LParen) {
3571                let mut depth = 1usize;
3572                while depth > 0 {
3573                    match self.advance().0 {
3574                        Token::LParen => depth += 1,
3575                        Token::RParen => {
3576                            depth -= 1;
3577                        }
3578                        Token::Eof => {
3579                            return Err(self.syntax_err(
3580                                "Unterminated sub attribute argument list",
3581                                self.peek_line(),
3582                            ));
3583                        }
3584                        _ => {}
3585                    }
3586                }
3587            }
3588        }
3589        Ok(())
3590    }
3591
3592    fn parse_sub_decl(&mut self) -> PerlResult<Statement> {
3593        let line = self.peek_line();
3594        self.advance(); // 'sub'
3595        match self.peek().clone() {
3596            Token::Ident(_) => {
3597                let name = self.parse_package_qualified_identifier()?;
3598                self.declared_subs.insert(name.clone());
3599                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
3600                self.parse_sub_attributes()?;
3601                let body = self.parse_block()?;
3602                Ok(Statement {
3603                    label: None,
3604                    kind: StmtKind::SubDecl {
3605                        name,
3606                        params,
3607                        body,
3608                        prototype,
3609                    },
3610                    line,
3611                })
3612            }
3613            Token::LParen | Token::LBrace | Token::Colon => {
3614                // Statement-level anonymous sub: `sub { }`, `sub () { }`, `sub :lvalue { }`
3615                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
3616                self.parse_sub_attributes()?;
3617                let body = self.parse_block()?;
3618                Ok(Statement {
3619                    label: None,
3620                    kind: StmtKind::Expression(Expr {
3621                        kind: ExprKind::CodeRef { params, body },
3622                        line,
3623                    }),
3624                    line,
3625                })
3626            }
3627            tok => Err(self.syntax_err(
3628                format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
3629                self.peek_line(),
3630            )),
3631        }
3632    }
3633
3634    /// `struct Name { field => Type, ... ; fn method { } }`
3635    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
3636        let line = self.peek_line();
3637        self.advance(); // struct
3638        let name = match self.advance() {
3639            (Token::Ident(n), _) => n,
3640            (tok, err_line) => {
3641                return Err(
3642                    self.syntax_err(format!("Expected struct name, got {:?}", tok), err_line)
3643                )
3644            }
3645        };
3646        self.expect(&Token::LBrace)?;
3647        let mut fields = Vec::new();
3648        let mut methods = Vec::new();
3649        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3650            // Check for method definition: `fn name { }` or `sub name { }`
3651            let is_method = match self.peek() {
3652                Token::Ident(s) => s == "fn" || s == "sub",
3653                _ => false,
3654            };
3655            if is_method {
3656                self.advance(); // fn/sub
3657                let method_name = match self.advance() {
3658                    (Token::Ident(n), _) => n,
3659                    (tok, err_line) => {
3660                        return Err(self
3661                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
3662                    }
3663                };
3664                // Parse optional signature: `($self, $arg: Type, ...)`
3665                let params = if self.eat(&Token::LParen) {
3666                    let p = self.parse_sub_signature_param_list()?;
3667                    self.expect(&Token::RParen)?;
3668                    p
3669                } else {
3670                    Vec::new()
3671                };
3672                // parse_block handles its own { } delimiters
3673                let body = self.parse_block()?;
3674                methods.push(crate::ast::StructMethod {
3675                    name: method_name,
3676                    params,
3677                    body,
3678                });
3679                // Optional trailing comma/semicolon after method
3680                self.eat(&Token::Comma);
3681                self.eat(&Token::Semicolon);
3682                continue;
3683            }
3684
3685            let field_name = match self.advance() {
3686                (Token::Ident(n), _) => n,
3687                (tok, err_line) => {
3688                    return Err(
3689                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
3690                    )
3691                }
3692            };
3693            // Support both `field => Type` and bare `field` (implies Any type)
3694            let ty = if self.eat(&Token::FatArrow) {
3695                self.parse_type_name()?
3696            } else {
3697                crate::ast::PerlTypeName::Any
3698            };
3699            let default = if self.eat(&Token::Assign) {
3700                // Use parse_ternary to avoid consuming commas (next field separator)
3701                Some(self.parse_ternary()?)
3702            } else {
3703                None
3704            };
3705            fields.push(StructField {
3706                name: field_name,
3707                ty,
3708                default,
3709            });
3710            if !self.eat(&Token::Comma) {
3711                // Also allow semicolons as field separators
3712                self.eat(&Token::Semicolon);
3713            }
3714        }
3715        self.expect(&Token::RBrace)?;
3716        self.eat(&Token::Semicolon);
3717        Ok(Statement {
3718            label: None,
3719            kind: StmtKind::StructDecl {
3720                def: StructDef {
3721                    name,
3722                    fields,
3723                    methods,
3724                },
3725            },
3726            line,
3727        })
3728    }
3729
3730    /// `enum Name { Variant1, Variant2 => Type, ... }`
3731    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
3732        let line = self.peek_line();
3733        self.advance(); // enum
3734        let name = match self.advance() {
3735            (Token::Ident(n), _) => n,
3736            (tok, err_line) => {
3737                return Err(self.syntax_err(format!("Expected enum name, got {:?}", tok), err_line))
3738            }
3739        };
3740        self.expect(&Token::LBrace)?;
3741        let mut variants = Vec::new();
3742        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3743            let variant_name = match self.advance() {
3744                (Token::Ident(n), _) => n,
3745                (tok, err_line) => {
3746                    return Err(
3747                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
3748                    )
3749                }
3750            };
3751            let ty = if self.eat(&Token::FatArrow) {
3752                Some(self.parse_type_name()?)
3753            } else {
3754                None
3755            };
3756            variants.push(EnumVariant {
3757                name: variant_name,
3758                ty,
3759            });
3760            if !self.eat(&Token::Comma) {
3761                self.eat(&Token::Semicolon);
3762            }
3763        }
3764        self.expect(&Token::RBrace)?;
3765        self.eat(&Token::Semicolon);
3766        Ok(Statement {
3767            label: None,
3768            kind: StmtKind::EnumDecl {
3769                def: EnumDef { name, variants },
3770            },
3771            line,
3772        })
3773    }
3774
3775    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
3776    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
3777        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
3778        let line = self.peek_line();
3779        self.advance(); // class
3780        let name = match self.advance() {
3781            (Token::Ident(n), _) => n,
3782            (tok, err_line) => {
3783                return Err(self.syntax_err(format!("Expected class name, got {:?}", tok), err_line))
3784            }
3785        };
3786
3787        // Parse `extends Parent1, Parent2`
3788        let mut extends = Vec::new();
3789        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
3790            self.advance(); // extends
3791            loop {
3792                match self.advance() {
3793                    (Token::Ident(parent), _) => extends.push(parent),
3794                    (tok, err_line) => {
3795                        return Err(self.syntax_err(
3796                            format!("Expected parent class name after `extends`, got {:?}", tok),
3797                            err_line,
3798                        ))
3799                    }
3800                }
3801                if !self.eat(&Token::Comma) {
3802                    break;
3803                }
3804            }
3805        }
3806
3807        // Parse `impl Trait1, Trait2`
3808        let mut implements = Vec::new();
3809        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
3810            self.advance(); // impl
3811            loop {
3812                match self.advance() {
3813                    (Token::Ident(trait_name), _) => implements.push(trait_name),
3814                    (tok, err_line) => {
3815                        return Err(self.syntax_err(
3816                            format!("Expected trait name after `impl`, got {:?}", tok),
3817                            err_line,
3818                        ))
3819                    }
3820                }
3821                if !self.eat(&Token::Comma) {
3822                    break;
3823                }
3824            }
3825        }
3826
3827        self.expect(&Token::LBrace)?;
3828        let mut fields = Vec::new();
3829        let mut methods = Vec::new();
3830        let mut static_fields = Vec::new();
3831
3832        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3833            // Check for visibility modifier
3834            let visibility = match self.peek() {
3835                Token::Ident(ref s) if s == "pub" => {
3836                    self.advance();
3837                    Visibility::Public
3838                }
3839                Token::Ident(ref s) if s == "priv" => {
3840                    self.advance();
3841                    Visibility::Private
3842                }
3843                Token::Ident(ref s) if s == "prot" => {
3844                    self.advance();
3845                    Visibility::Protected
3846                }
3847                _ => Visibility::Public, // default public
3848            };
3849
3850            // Check for static field: `static name: Type = default`
3851            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
3852                self.advance(); // static
3853
3854                // Could be a static method (`static fn`) or static field
3855                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
3856                    // static fn is same as fn Self.name — handled below but not here
3857                    return Err(self.syntax_err(
3858                        "use `fn Self.name` for static methods, not `static fn`",
3859                        self.peek_line(),
3860                    ));
3861                }
3862
3863                let field_name = match self.advance() {
3864                    (Token::Ident(n), _) => n,
3865                    (tok, err_line) => {
3866                        return Err(self.syntax_err(
3867                            format!("Expected static field name, got {:?}", tok),
3868                            err_line,
3869                        ))
3870                    }
3871                };
3872
3873                let ty = if self.eat(&Token::Colon) {
3874                    self.parse_type_name()?
3875                } else {
3876                    crate::ast::PerlTypeName::Any
3877                };
3878
3879                let default = if self.eat(&Token::Assign) {
3880                    Some(self.parse_ternary()?)
3881                } else {
3882                    None
3883                };
3884
3885                static_fields.push(ClassStaticField {
3886                    name: field_name,
3887                    ty,
3888                    visibility,
3889                    default,
3890                });
3891
3892                if !self.eat(&Token::Comma) {
3893                    self.eat(&Token::Semicolon);
3894                }
3895                continue;
3896            }
3897
3898            // Check for `final` modifier before fn
3899            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
3900            if method_is_final {
3901                self.advance(); // final
3902            }
3903
3904            // Check for method: `fn name` or `fn Self.name` (static)
3905            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
3906            if is_method {
3907                self.advance(); // fn/sub
3908
3909                // Check for static method: `fn Self.name`
3910                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
3911                if is_static {
3912                    self.advance(); // Self
3913                    self.expect(&Token::Dot)?;
3914                }
3915
3916                let method_name = match self.advance() {
3917                    (Token::Ident(n), _) => n,
3918                    (tok, err_line) => {
3919                        return Err(self
3920                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
3921                    }
3922                };
3923
3924                // Parse optional signature
3925                let params = if self.eat(&Token::LParen) {
3926                    let p = self.parse_sub_signature_param_list()?;
3927                    self.expect(&Token::RParen)?;
3928                    p
3929                } else {
3930                    Vec::new()
3931                };
3932
3933                // Body is optional (abstract method in trait has no body)
3934                let body = if matches!(self.peek(), Token::LBrace) {
3935                    Some(self.parse_block()?)
3936                } else {
3937                    None
3938                };
3939
3940                methods.push(ClassMethod {
3941                    name: method_name,
3942                    params,
3943                    body,
3944                    visibility,
3945                    is_static,
3946                    is_final: method_is_final,
3947                });
3948                self.eat(&Token::Comma);
3949                self.eat(&Token::Semicolon);
3950                continue;
3951            } else if method_is_final {
3952                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
3953            }
3954
3955            // Parse field: `name: Type = default`
3956            let field_name = match self.advance() {
3957                (Token::Ident(n), _) => n,
3958                (tok, err_line) => {
3959                    return Err(
3960                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
3961                    )
3962                }
3963            };
3964
3965            // Type after colon: `name: Type`
3966            let ty = if self.eat(&Token::Colon) {
3967                self.parse_type_name()?
3968            } else {
3969                crate::ast::PerlTypeName::Any
3970            };
3971
3972            // Default value after `=`
3973            let default = if self.eat(&Token::Assign) {
3974                Some(self.parse_ternary()?)
3975            } else {
3976                None
3977            };
3978
3979            fields.push(ClassField {
3980                name: field_name,
3981                ty,
3982                visibility,
3983                default,
3984            });
3985
3986            if !self.eat(&Token::Comma) {
3987                self.eat(&Token::Semicolon);
3988            }
3989        }
3990
3991        self.expect(&Token::RBrace)?;
3992        self.eat(&Token::Semicolon);
3993
3994        Ok(Statement {
3995            label: None,
3996            kind: StmtKind::ClassDecl {
3997                def: ClassDef {
3998                    name,
3999                    is_abstract,
4000                    is_final,
4001                    extends,
4002                    implements,
4003                    fields,
4004                    methods,
4005                    static_fields,
4006                },
4007            },
4008            line,
4009        })
4010    }
4011
4012    /// `trait Name { fn required; fn with_default { } }`
4013    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
4014        use crate::ast::{ClassMethod, TraitDef, Visibility};
4015        let line = self.peek_line();
4016        self.advance(); // trait
4017        let name = match self.advance() {
4018            (Token::Ident(n), _) => n,
4019            (tok, err_line) => {
4020                return Err(self.syntax_err(format!("Expected trait name, got {:?}", tok), err_line))
4021            }
4022        };
4023
4024        self.expect(&Token::LBrace)?;
4025        let mut methods = Vec::new();
4026
4027        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4028            // Optional visibility
4029            let visibility = match self.peek() {
4030                Token::Ident(ref s) if s == "pub" => {
4031                    self.advance();
4032                    Visibility::Public
4033                }
4034                Token::Ident(ref s) if s == "priv" => {
4035                    self.advance();
4036                    Visibility::Private
4037                }
4038                Token::Ident(ref s) if s == "prot" => {
4039                    self.advance();
4040                    Visibility::Protected
4041                }
4042                _ => Visibility::Public,
4043            };
4044
4045            // Expect `fn` or `sub`
4046            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4047                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
4048            }
4049            self.advance(); // fn/sub
4050
4051            let method_name = match self.advance() {
4052                (Token::Ident(n), _) => n,
4053                (tok, err_line) => {
4054                    return Err(
4055                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
4056                    )
4057                }
4058            };
4059
4060            // Optional signature
4061            let params = if self.eat(&Token::LParen) {
4062                let p = self.parse_sub_signature_param_list()?;
4063                self.expect(&Token::RParen)?;
4064                p
4065            } else {
4066                Vec::new()
4067            };
4068
4069            // Body is optional (no body = abstract/required method)
4070            let body = if matches!(self.peek(), Token::LBrace) {
4071                Some(self.parse_block()?)
4072            } else {
4073                None
4074            };
4075
4076            methods.push(ClassMethod {
4077                name: method_name,
4078                params,
4079                body,
4080                visibility,
4081                is_static: false,
4082                is_final: false,
4083            });
4084
4085            self.eat(&Token::Comma);
4086            self.eat(&Token::Semicolon);
4087        }
4088
4089        self.expect(&Token::RBrace)?;
4090        self.eat(&Token::Semicolon);
4091
4092        Ok(Statement {
4093            label: None,
4094            kind: StmtKind::TraitDecl {
4095                def: TraitDef { name, methods },
4096            },
4097            line,
4098        })
4099    }
4100
4101    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
4102        match &target.kind {
4103            ExprKind::ScalarVar(name) => Some(VarDecl {
4104                sigil: Sigil::Scalar,
4105                name: name.clone(),
4106                initializer: None,
4107                frozen: false,
4108                type_annotation: None,
4109            }),
4110            ExprKind::ArrayVar(name) => Some(VarDecl {
4111                sigil: Sigil::Array,
4112                name: name.clone(),
4113                initializer: None,
4114                frozen: false,
4115                type_annotation: None,
4116            }),
4117            ExprKind::HashVar(name) => Some(VarDecl {
4118                sigil: Sigil::Hash,
4119                name: name.clone(),
4120                initializer: None,
4121                frozen: false,
4122                type_annotation: None,
4123            }),
4124            ExprKind::Typeglob(name) => Some(VarDecl {
4125                sigil: Sigil::Typeglob,
4126                name: name.clone(),
4127                initializer: None,
4128                frozen: false,
4129                type_annotation: None,
4130            }),
4131            _ => None,
4132        }
4133    }
4134
4135    fn parse_decl_array_destructure(
4136        &mut self,
4137        keyword: &str,
4138        line: usize,
4139    ) -> PerlResult<Statement> {
4140        self.expect(&Token::LBracket)?;
4141        let elems = self.parse_match_array_elems_until_rbracket()?;
4142        self.expect(&Token::Assign)?;
4143        self.suppress_scalar_hash_brace += 1;
4144        let rhs = self.parse_expression()?;
4145        self.suppress_scalar_hash_brace -= 1;
4146        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
4147        self.parse_stmt_postfix_modifier(stmt)
4148    }
4149
4150    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
4151        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
4152            unreachable!("parse_match_hash_pattern returns Hash");
4153        };
4154        self.expect(&Token::Assign)?;
4155        self.suppress_scalar_hash_brace += 1;
4156        let rhs = self.parse_expression()?;
4157        self.suppress_scalar_hash_brace -= 1;
4158        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
4159        self.parse_stmt_postfix_modifier(stmt)
4160    }
4161
4162    fn desugar_array_destructure(
4163        &mut self,
4164        keyword: &str,
4165        line: usize,
4166        elems: Vec<MatchArrayElem>,
4167        rhs: Expr,
4168    ) -> PerlResult<Statement> {
4169        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4170        let mut stmts: Vec<Statement> = Vec::new();
4171        stmts.push(destructure_stmt_from_var_decls(
4172            keyword,
4173            vec![VarDecl {
4174                sigil: Sigil::Scalar,
4175                name: tmp.clone(),
4176                initializer: Some(rhs),
4177                frozen: false,
4178                type_annotation: None,
4179            }],
4180            line,
4181        ));
4182
4183        let has_rest = elems
4184            .iter()
4185            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
4186        let fixed_slots = elems
4187            .iter()
4188            .filter(|e| {
4189                matches!(
4190                    e,
4191                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
4192                )
4193            })
4194            .count();
4195        if !has_rest {
4196            let cond = Expr {
4197                kind: ExprKind::BinOp {
4198                    left: Box::new(destructure_expr_array_len(&tmp, line)),
4199                    op: BinOp::NumEq,
4200                    right: Box::new(Expr {
4201                        kind: ExprKind::Integer(fixed_slots as i64),
4202                        line,
4203                    }),
4204                },
4205                line,
4206            };
4207            stmts.push(destructure_stmt_unless_die(
4208                line,
4209                cond,
4210                "array destructure: length mismatch",
4211            ));
4212        }
4213
4214        let mut idx: i64 = 0;
4215        for elem in elems {
4216            match elem {
4217                MatchArrayElem::Rest => break,
4218                MatchArrayElem::RestBind(name) => {
4219                    let list_source = Expr {
4220                        kind: ExprKind::Deref {
4221                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4222                            kind: Sigil::Array,
4223                        },
4224                        line,
4225                    };
4226                    let last_ix = Expr {
4227                        kind: ExprKind::BinOp {
4228                            left: Box::new(destructure_expr_array_len(&tmp, line)),
4229                            op: BinOp::Sub,
4230                            right: Box::new(Expr {
4231                                kind: ExprKind::Integer(1),
4232                                line,
4233                            }),
4234                        },
4235                        line,
4236                    };
4237                    let range = Expr {
4238                        kind: ExprKind::Range {
4239                            from: Box::new(Expr {
4240                                kind: ExprKind::Integer(idx),
4241                                line,
4242                            }),
4243                            to: Box::new(last_ix),
4244                            exclusive: false,
4245                        },
4246                        line,
4247                    };
4248                    let slice = Expr {
4249                        kind: ExprKind::AnonymousListSlice {
4250                            source: Box::new(list_source),
4251                            indices: vec![range],
4252                        },
4253                        line,
4254                    };
4255                    stmts.push(destructure_stmt_from_var_decls(
4256                        keyword,
4257                        vec![VarDecl {
4258                            sigil: Sigil::Array,
4259                            name,
4260                            initializer: Some(slice),
4261                            frozen: false,
4262                            type_annotation: None,
4263                        }],
4264                        line,
4265                    ));
4266                    break;
4267                }
4268                MatchArrayElem::CaptureScalar(name) => {
4269                    let arrow = Expr {
4270                        kind: ExprKind::ArrowDeref {
4271                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4272                            index: Box::new(Expr {
4273                                kind: ExprKind::Integer(idx),
4274                                line,
4275                            }),
4276                            kind: DerefKind::Array,
4277                        },
4278                        line,
4279                    };
4280                    stmts.push(destructure_stmt_from_var_decls(
4281                        keyword,
4282                        vec![VarDecl {
4283                            sigil: Sigil::Scalar,
4284                            name,
4285                            initializer: Some(arrow),
4286                            frozen: false,
4287                            type_annotation: None,
4288                        }],
4289                        line,
4290                    ));
4291                    idx += 1;
4292                }
4293                MatchArrayElem::Expr(e) => {
4294                    let elem_subj = Expr {
4295                        kind: ExprKind::ArrowDeref {
4296                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4297                            index: Box::new(Expr {
4298                                kind: ExprKind::Integer(idx),
4299                                line,
4300                            }),
4301                            kind: DerefKind::Array,
4302                        },
4303                        line,
4304                    };
4305                    let match_expr = Expr {
4306                        kind: ExprKind::AlgebraicMatch {
4307                            subject: Box::new(elem_subj),
4308                            arms: vec![
4309                                MatchArm {
4310                                    pattern: MatchPattern::Value(Box::new(e.clone())),
4311                                    guard: None,
4312                                    body: Expr {
4313                                        kind: ExprKind::Integer(0),
4314                                        line,
4315                                    },
4316                                },
4317                                MatchArm {
4318                                    pattern: MatchPattern::Any,
4319                                    guard: None,
4320                                    body: Expr {
4321                                        kind: ExprKind::Die(vec![Expr {
4322                                            kind: ExprKind::String(
4323                                                "array destructure: element pattern mismatch"
4324                                                    .to_string(),
4325                                            ),
4326                                            line,
4327                                        }]),
4328                                        line,
4329                                    },
4330                                },
4331                            ],
4332                        },
4333                        line,
4334                    };
4335                    stmts.push(Statement {
4336                        label: None,
4337                        kind: StmtKind::Expression(match_expr),
4338                        line,
4339                    });
4340                    idx += 1;
4341                }
4342            }
4343        }
4344
4345        Ok(Statement {
4346            label: None,
4347            kind: StmtKind::StmtGroup(stmts),
4348            line,
4349        })
4350    }
4351
4352    fn desugar_hash_destructure(
4353        &mut self,
4354        keyword: &str,
4355        line: usize,
4356        pairs: Vec<MatchHashPair>,
4357        rhs: Expr,
4358    ) -> PerlResult<Statement> {
4359        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4360        let mut stmts: Vec<Statement> = Vec::new();
4361        stmts.push(destructure_stmt_from_var_decls(
4362            keyword,
4363            vec![VarDecl {
4364                sigil: Sigil::Scalar,
4365                name: tmp.clone(),
4366                initializer: Some(rhs),
4367                frozen: false,
4368                type_annotation: None,
4369            }],
4370            line,
4371        ));
4372
4373        for pair in pairs {
4374            match pair {
4375                MatchHashPair::KeyOnly { key } => {
4376                    let exists_op = Expr {
4377                        kind: ExprKind::Exists(Box::new(Expr {
4378                            kind: ExprKind::ArrowDeref {
4379                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4380                                index: Box::new(key),
4381                                kind: DerefKind::Hash,
4382                            },
4383                            line,
4384                        })),
4385                        line,
4386                    };
4387                    stmts.push(destructure_stmt_unless_die(
4388                        line,
4389                        exists_op,
4390                        "hash destructure: missing required key",
4391                    ));
4392                }
4393                MatchHashPair::Capture { key, name } => {
4394                    let init = Expr {
4395                        kind: ExprKind::ArrowDeref {
4396                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4397                            index: Box::new(key),
4398                            kind: DerefKind::Hash,
4399                        },
4400                        line,
4401                    };
4402                    stmts.push(destructure_stmt_from_var_decls(
4403                        keyword,
4404                        vec![VarDecl {
4405                            sigil: Sigil::Scalar,
4406                            name,
4407                            initializer: Some(init),
4408                            frozen: false,
4409                            type_annotation: None,
4410                        }],
4411                        line,
4412                    ));
4413                }
4414            }
4415        }
4416
4417        Ok(Statement {
4418            label: None,
4419            kind: StmtKind::StmtGroup(stmts),
4420            line,
4421        })
4422    }
4423
4424    fn parse_my_our_local(
4425        &mut self,
4426        keyword: &str,
4427        allow_type_annotation: bool,
4428    ) -> PerlResult<Statement> {
4429        let line = self.peek_line();
4430        self.advance(); // 'my'/'our'/'local'
4431
4432        if keyword == "local"
4433            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
4434        {
4435            let target = self.parse_postfix()?;
4436            let mut initializer: Option<Expr> = None;
4437            if self.eat(&Token::Assign) {
4438                initializer = Some(self.parse_expression()?);
4439            } else if matches!(
4440                self.peek(),
4441                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
4442            ) {
4443                if matches!(&target.kind, ExprKind::Typeglob(_)) {
4444                    return Err(self.syntax_err(
4445                        "compound assignment on typeglob declaration is not supported",
4446                        self.peek_line(),
4447                    ));
4448                }
4449                let op = match self.peek().clone() {
4450                    Token::OrAssign => BinOp::LogOr,
4451                    Token::DefinedOrAssign => BinOp::DefinedOr,
4452                    Token::AndAssign => BinOp::LogAnd,
4453                    _ => unreachable!(),
4454                };
4455                self.advance();
4456                let rhs = self.parse_assign_expr()?;
4457                let tgt_line = target.line;
4458                initializer = Some(Expr {
4459                    kind: ExprKind::CompoundAssign {
4460                        target: Box::new(target.clone()),
4461                        op,
4462                        value: Box::new(rhs),
4463                    },
4464                    line: tgt_line,
4465                });
4466            }
4467
4468            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
4469                decl.initializer = initializer;
4470                StmtKind::Local(vec![decl])
4471            } else {
4472                StmtKind::LocalExpr {
4473                    target,
4474                    initializer,
4475                }
4476            };
4477            let stmt = Statement {
4478                label: None,
4479                kind,
4480                line,
4481            };
4482            return self.parse_stmt_postfix_modifier(stmt);
4483        }
4484
4485        if matches!(self.peek(), Token::LBracket) {
4486            return self.parse_decl_array_destructure(keyword, line);
4487        }
4488        if matches!(self.peek(), Token::LBrace) {
4489            return self.parse_decl_hash_destructure(keyword, line);
4490        }
4491
4492        let mut decls = Vec::new();
4493
4494        if self.eat(&Token::LParen) {
4495            // my ($a, @b, %c)
4496            while !matches!(self.peek(), Token::RParen | Token::Eof) {
4497                let decl = self.parse_var_decl(allow_type_annotation)?;
4498                decls.push(decl);
4499                if !self.eat(&Token::Comma) {
4500                    break;
4501                }
4502            }
4503            self.expect(&Token::RParen)?;
4504        } else {
4505            decls.push(self.parse_var_decl(allow_type_annotation)?);
4506        }
4507
4508        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
4509        if self.eat(&Token::Assign) {
4510            if keyword == "our" && decls.len() == 1 {
4511                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
4512                    self.advance();
4513                    decls.push(self.parse_var_decl(allow_type_annotation)?);
4514                    if !self.eat(&Token::Assign) {
4515                        return Err(self.syntax_err(
4516                            "expected `=` after `our` in chained our-declaration",
4517                            self.peek_line(),
4518                        ));
4519                    }
4520                }
4521            }
4522            let val = self.parse_expression()?;
4523            if decls.len() == 1 {
4524                decls[0].initializer = Some(val);
4525            } else {
4526                for decl in &mut decls {
4527                    decl.initializer = Some(val.clone());
4528                }
4529            }
4530        } else if decls.len() == 1 {
4531            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
4532            let op = match self.peek().clone() {
4533                Token::OrAssign => Some(BinOp::LogOr),
4534                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
4535                Token::AndAssign => Some(BinOp::LogAnd),
4536                _ => None,
4537            };
4538            if let Some(op) = op {
4539                let d = &decls[0];
4540                if matches!(d.sigil, Sigil::Typeglob) {
4541                    return Err(self.syntax_err(
4542                        "compound assignment on typeglob declaration is not supported",
4543                        self.peek_line(),
4544                    ));
4545                }
4546                self.advance();
4547                let rhs = self.parse_assign_expr()?;
4548                let target = Expr {
4549                    kind: match d.sigil {
4550                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
4551                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
4552                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
4553                        Sigil::Typeglob => unreachable!(),
4554                    },
4555                    line,
4556                };
4557                decls[0].initializer = Some(Expr {
4558                    kind: ExprKind::CompoundAssign {
4559                        target: Box::new(target),
4560                        op,
4561                        value: Box::new(rhs),
4562                    },
4563                    line,
4564                });
4565            }
4566        }
4567
4568        let kind = match keyword {
4569            "my" => StmtKind::My(decls),
4570            "mysync" => StmtKind::MySync(decls),
4571            "our" => StmtKind::Our(decls),
4572            "local" => StmtKind::Local(decls),
4573            "state" => StmtKind::State(decls),
4574            _ => unreachable!(),
4575        };
4576        let stmt = Statement {
4577            label: None,
4578            kind,
4579            line,
4580        };
4581        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
4582        self.parse_stmt_postfix_modifier(stmt)
4583    }
4584
4585    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
4586        let mut decl = match self.advance() {
4587            (Token::ScalarVar(name), _) => VarDecl {
4588                sigil: Sigil::Scalar,
4589                name,
4590                initializer: None,
4591                frozen: false,
4592                type_annotation: None,
4593            },
4594            (Token::ArrayVar(name), _) => VarDecl {
4595                sigil: Sigil::Array,
4596                name,
4597                initializer: None,
4598                frozen: false,
4599                type_annotation: None,
4600            },
4601            (Token::HashVar(name), _) => VarDecl {
4602                sigil: Sigil::Hash,
4603                name,
4604                initializer: None,
4605                frozen: false,
4606                type_annotation: None,
4607            },
4608            (Token::Star, _line) => {
4609                let name = match self.advance() {
4610                    (Token::Ident(n), _) => n,
4611                    (tok, l) => {
4612                        return Err(self
4613                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
4614                    }
4615                };
4616                VarDecl {
4617                    sigil: Sigil::Typeglob,
4618                    name,
4619                    initializer: None,
4620                    frozen: false,
4621                    type_annotation: None,
4622                }
4623            }
4624            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
4625            // slot in a list assignment. The interpreter treats `undef`-named
4626            // scalar decls as throwaway: declared into a unique sink so the
4627            // distribute-to-decls loop advances past the slot.
4628            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
4629                sigil: Sigil::Scalar,
4630                // Synthesize a name that user code cannot reference. Each
4631                // sink slot in a list-assign gets its own unique name so the
4632                // declarations don't collide.
4633                name: format!("__undef_sink_{}", self.pos),
4634                initializer: None,
4635                frozen: false,
4636                type_annotation: None,
4637            },
4638            (tok, line) => {
4639                return Err(self.syntax_err(
4640                    format!("Expected variable in declaration, got {:?}", tok),
4641                    line,
4642                ));
4643            }
4644        };
4645        if allow_type_annotation && self.eat(&Token::Colon) {
4646            let ty = self.parse_type_name()?;
4647            if decl.sigil != Sigil::Scalar {
4648                return Err(self.syntax_err(
4649                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
4650                    self.peek_line(),
4651                ));
4652            }
4653            decl.type_annotation = Some(ty);
4654        }
4655        Ok(decl)
4656    }
4657
4658    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
4659        match self.advance() {
4660            (Token::Ident(name), _) => match name.as_str() {
4661                "Int" => Ok(PerlTypeName::Int),
4662                "Str" => Ok(PerlTypeName::Str),
4663                "Float" => Ok(PerlTypeName::Float),
4664                "Bool" => Ok(PerlTypeName::Bool),
4665                "Array" => Ok(PerlTypeName::Array),
4666                "Hash" => Ok(PerlTypeName::Hash),
4667                "Ref" => Ok(PerlTypeName::Ref),
4668                "Any" => Ok(PerlTypeName::Any),
4669                _ => Ok(PerlTypeName::Struct(name)),
4670            },
4671            (tok, err_line) => Err(self.syntax_err(
4672                format!("Expected type name after `:`, got {:?}", tok),
4673                err_line,
4674            )),
4675        }
4676    }
4677
4678    fn parse_package(&mut self) -> PerlResult<Statement> {
4679        let line = self.peek_line();
4680        self.advance(); // 'package'
4681        let name = match self.advance() {
4682            (Token::Ident(n), _) => n,
4683            (tok, line) => {
4684                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
4685            }
4686        };
4687        // Handle Foo::Bar
4688        let mut full_name = name;
4689        while self.eat(&Token::PackageSep) {
4690            if let (Token::Ident(part), _) = self.advance() {
4691                full_name = format!("{}::{}", full_name, part);
4692            }
4693        }
4694        self.eat(&Token::Semicolon);
4695        Ok(Statement {
4696            label: None,
4697            kind: StmtKind::Package { name: full_name },
4698            line,
4699        })
4700    }
4701
4702    fn parse_use(&mut self) -> PerlResult<Statement> {
4703        let line = self.peek_line();
4704        self.advance(); // 'use'
4705        let (tok, tok_line) = self.advance();
4706        match tok {
4707            Token::Float(v) => {
4708                self.eat(&Token::Semicolon);
4709                Ok(Statement {
4710                    label: None,
4711                    kind: StmtKind::UsePerlVersion { version: v },
4712                    line,
4713                })
4714            }
4715            Token::Integer(n) => {
4716                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
4717                    self.eat(&Token::Semicolon);
4718                    Ok(Statement {
4719                        label: None,
4720                        kind: StmtKind::UsePerlVersion { version: n as f64 },
4721                        line,
4722                    })
4723                } else {
4724                    Err(self.syntax_err(
4725                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
4726                        line,
4727                    ))
4728                }
4729            }
4730            Token::Ident(n) => {
4731                let mut full_name = n;
4732                while self.eat(&Token::PackageSep) {
4733                    if let (Token::Ident(part), _) = self.advance() {
4734                        full_name = format!("{}::{}", full_name, part);
4735                    }
4736                }
4737                if full_name == "overload" {
4738                    let mut pairs = Vec::new();
4739                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
4740                        loop {
4741                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
4742                            {
4743                                break;
4744                            }
4745                            let key_e = this.parse_assign_expr()?;
4746                            this.expect(&Token::FatArrow)?;
4747                            let val_e = this.parse_assign_expr()?;
4748                            let key = this.expr_to_overload_key(&key_e)?;
4749                            let val = this.expr_to_overload_sub(&val_e)?;
4750                            pairs.push((key, val));
4751                            if !this.eat(&Token::Comma) {
4752                                break;
4753                            }
4754                        }
4755                        Ok(())
4756                    };
4757                    if self.eat(&Token::LParen) {
4758                        // `use overload ();` — common in JSON::PP and other modules.
4759                        parse_overload_pairs(self)?;
4760                        self.expect(&Token::RParen)?;
4761                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
4762                        parse_overload_pairs(self)?;
4763                    }
4764                    self.eat(&Token::Semicolon);
4765                    return Ok(Statement {
4766                        label: None,
4767                        kind: StmtKind::UseOverload { pairs },
4768                        line,
4769                    });
4770                }
4771                let mut imports = Vec::new();
4772                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
4773                    && !self.next_is_new_stmt_keyword(tok_line)
4774                {
4775                    loop {
4776                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
4777                            break;
4778                        }
4779                        imports.push(self.parse_expression()?);
4780                        if !self.eat(&Token::Comma) {
4781                            break;
4782                        }
4783                    }
4784                }
4785                self.eat(&Token::Semicolon);
4786                Ok(Statement {
4787                    label: None,
4788                    kind: StmtKind::Use {
4789                        module: full_name,
4790                        imports,
4791                    },
4792                    line,
4793                })
4794            }
4795            other => Err(self.syntax_err(
4796                format!("Expected module name or version after use, got {:?}", other),
4797                tok_line,
4798            )),
4799        }
4800    }
4801
4802    fn parse_no(&mut self) -> PerlResult<Statement> {
4803        let line = self.peek_line();
4804        self.advance(); // 'no'
4805        let module = match self.advance() {
4806            (Token::Ident(n), tok_line) => (n, tok_line),
4807            (tok, line) => {
4808                return Err(self.syntax_err(
4809                    format!("Expected module name after no, got {:?}", tok),
4810                    line,
4811                ))
4812            }
4813        };
4814        let (module_name, tok_line) = module;
4815        let mut full_name = module_name;
4816        while self.eat(&Token::PackageSep) {
4817            if let (Token::Ident(part), _) = self.advance() {
4818                full_name = format!("{}::{}", full_name, part);
4819            }
4820        }
4821        let mut imports = Vec::new();
4822        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
4823            && !self.next_is_new_stmt_keyword(tok_line)
4824        {
4825            loop {
4826                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
4827                    break;
4828                }
4829                imports.push(self.parse_expression()?);
4830                if !self.eat(&Token::Comma) {
4831                    break;
4832                }
4833            }
4834        }
4835        self.eat(&Token::Semicolon);
4836        Ok(Statement {
4837            label: None,
4838            kind: StmtKind::No {
4839                module: full_name,
4840                imports,
4841            },
4842            line,
4843        })
4844    }
4845
4846    fn parse_return(&mut self) -> PerlResult<Statement> {
4847        let line = self.peek_line();
4848        self.advance(); // 'return'
4849        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
4850            None
4851        } else {
4852            // Only parse up to the assign level to avoid consuming postfix if/unless
4853            Some(self.parse_assign_expr()?)
4854        };
4855        // Check for postfix modifiers on return
4856        let stmt = Statement {
4857            label: None,
4858            kind: StmtKind::Return(val),
4859            line,
4860        };
4861        if let Token::Ident(ref kw) = self.peek().clone() {
4862            match kw.as_str() {
4863                "if" => {
4864                    self.advance();
4865                    let cond = self.parse_expression()?;
4866                    self.eat(&Token::Semicolon);
4867                    return Ok(Statement {
4868                        label: None,
4869                        kind: StmtKind::If {
4870                            condition: cond,
4871                            body: vec![stmt],
4872                            elsifs: vec![],
4873                            else_block: None,
4874                        },
4875                        line,
4876                    });
4877                }
4878                "unless" => {
4879                    self.advance();
4880                    let cond = self.parse_expression()?;
4881                    self.eat(&Token::Semicolon);
4882                    return Ok(Statement {
4883                        label: None,
4884                        kind: StmtKind::Unless {
4885                            condition: cond,
4886                            body: vec![stmt],
4887                            else_block: None,
4888                        },
4889                        line,
4890                    });
4891                }
4892                _ => {}
4893            }
4894        }
4895        self.eat(&Token::Semicolon);
4896        Ok(stmt)
4897    }
4898
4899    // ── Expressions (Pratt / precedence climbing) ──
4900
4901    fn parse_expression(&mut self) -> PerlResult<Expr> {
4902        self.parse_comma_expr()
4903    }
4904
4905    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
4906        let expr = self.parse_assign_expr()?;
4907        let mut exprs = vec![expr];
4908        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
4909            if matches!(
4910                self.peek(),
4911                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
4912            ) {
4913                break; // trailing comma
4914            }
4915            exprs.push(self.parse_assign_expr()?);
4916        }
4917        if exprs.len() == 1 {
4918            return Ok(exprs.pop().unwrap());
4919        }
4920        let line = exprs[0].line;
4921        Ok(Expr {
4922            kind: ExprKind::List(exprs),
4923            line,
4924        })
4925    }
4926
4927    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
4928        let expr = self.parse_ternary()?;
4929        let line = expr.line;
4930
4931        match self.peek().clone() {
4932            Token::Assign => {
4933                self.advance();
4934                let right = self.parse_assign_expr()?;
4935                Ok(Expr {
4936                    kind: ExprKind::Assign {
4937                        target: Box::new(expr),
4938                        value: Box::new(right),
4939                    },
4940                    line,
4941                })
4942            }
4943            Token::PlusAssign => {
4944                self.advance();
4945                let r = self.parse_assign_expr()?;
4946                Ok(Expr {
4947                    kind: ExprKind::CompoundAssign {
4948                        target: Box::new(expr),
4949                        op: BinOp::Add,
4950                        value: Box::new(r),
4951                    },
4952                    line,
4953                })
4954            }
4955            Token::MinusAssign => {
4956                self.advance();
4957                let r = self.parse_assign_expr()?;
4958                Ok(Expr {
4959                    kind: ExprKind::CompoundAssign {
4960                        target: Box::new(expr),
4961                        op: BinOp::Sub,
4962                        value: Box::new(r),
4963                    },
4964                    line,
4965                })
4966            }
4967            Token::MulAssign => {
4968                self.advance();
4969                let r = self.parse_assign_expr()?;
4970                Ok(Expr {
4971                    kind: ExprKind::CompoundAssign {
4972                        target: Box::new(expr),
4973                        op: BinOp::Mul,
4974                        value: Box::new(r),
4975                    },
4976                    line,
4977                })
4978            }
4979            Token::DivAssign => {
4980                self.advance();
4981                let r = self.parse_assign_expr()?;
4982                Ok(Expr {
4983                    kind: ExprKind::CompoundAssign {
4984                        target: Box::new(expr),
4985                        op: BinOp::Div,
4986                        value: Box::new(r),
4987                    },
4988                    line,
4989                })
4990            }
4991            Token::ModAssign => {
4992                self.advance();
4993                let r = self.parse_assign_expr()?;
4994                Ok(Expr {
4995                    kind: ExprKind::CompoundAssign {
4996                        target: Box::new(expr),
4997                        op: BinOp::Mod,
4998                        value: Box::new(r),
4999                    },
5000                    line,
5001                })
5002            }
5003            Token::PowAssign => {
5004                self.advance();
5005                let r = self.parse_assign_expr()?;
5006                Ok(Expr {
5007                    kind: ExprKind::CompoundAssign {
5008                        target: Box::new(expr),
5009                        op: BinOp::Pow,
5010                        value: Box::new(r),
5011                    },
5012                    line,
5013                })
5014            }
5015            Token::DotAssign => {
5016                self.advance();
5017                let r = self.parse_assign_expr()?;
5018                Ok(Expr {
5019                    kind: ExprKind::CompoundAssign {
5020                        target: Box::new(expr),
5021                        op: BinOp::Concat,
5022                        value: Box::new(r),
5023                    },
5024                    line,
5025                })
5026            }
5027            Token::BitAndAssign => {
5028                self.advance();
5029                let r = self.parse_assign_expr()?;
5030                Ok(Expr {
5031                    kind: ExprKind::CompoundAssign {
5032                        target: Box::new(expr),
5033                        op: BinOp::BitAnd,
5034                        value: Box::new(r),
5035                    },
5036                    line,
5037                })
5038            }
5039            Token::BitOrAssign => {
5040                self.advance();
5041                let r = self.parse_assign_expr()?;
5042                Ok(Expr {
5043                    kind: ExprKind::CompoundAssign {
5044                        target: Box::new(expr),
5045                        op: BinOp::BitOr,
5046                        value: Box::new(r),
5047                    },
5048                    line,
5049                })
5050            }
5051            Token::XorAssign => {
5052                self.advance();
5053                let r = self.parse_assign_expr()?;
5054                Ok(Expr {
5055                    kind: ExprKind::CompoundAssign {
5056                        target: Box::new(expr),
5057                        op: BinOp::BitXor,
5058                        value: Box::new(r),
5059                    },
5060                    line,
5061                })
5062            }
5063            Token::ShiftLeftAssign => {
5064                self.advance();
5065                let r = self.parse_assign_expr()?;
5066                Ok(Expr {
5067                    kind: ExprKind::CompoundAssign {
5068                        target: Box::new(expr),
5069                        op: BinOp::ShiftLeft,
5070                        value: Box::new(r),
5071                    },
5072                    line,
5073                })
5074            }
5075            Token::ShiftRightAssign => {
5076                self.advance();
5077                let r = self.parse_assign_expr()?;
5078                Ok(Expr {
5079                    kind: ExprKind::CompoundAssign {
5080                        target: Box::new(expr),
5081                        op: BinOp::ShiftRight,
5082                        value: Box::new(r),
5083                    },
5084                    line,
5085                })
5086            }
5087            Token::OrAssign => {
5088                self.advance();
5089                let r = self.parse_assign_expr()?;
5090                Ok(Expr {
5091                    kind: ExprKind::CompoundAssign {
5092                        target: Box::new(expr),
5093                        op: BinOp::LogOr,
5094                        value: Box::new(r),
5095                    },
5096                    line,
5097                })
5098            }
5099            Token::DefinedOrAssign => {
5100                self.advance();
5101                let r = self.parse_assign_expr()?;
5102                Ok(Expr {
5103                    kind: ExprKind::CompoundAssign {
5104                        target: Box::new(expr),
5105                        op: BinOp::DefinedOr,
5106                        value: Box::new(r),
5107                    },
5108                    line,
5109                })
5110            }
5111            Token::AndAssign => {
5112                self.advance();
5113                let r = self.parse_assign_expr()?;
5114                Ok(Expr {
5115                    kind: ExprKind::CompoundAssign {
5116                        target: Box::new(expr),
5117                        op: BinOp::LogAnd,
5118                        value: Box::new(r),
5119                    },
5120                    line,
5121                })
5122            }
5123            _ => Ok(expr),
5124        }
5125    }
5126
5127    fn parse_ternary(&mut self) -> PerlResult<Expr> {
5128        let expr = self.parse_pipe_forward()?;
5129        if self.eat(&Token::Question) {
5130            let line = expr.line;
5131            let then_expr = self.parse_assign_expr()?;
5132            self.expect(&Token::Colon)?;
5133            let else_expr = self.parse_assign_expr()?;
5134            return Ok(Expr {
5135                kind: ExprKind::Ternary {
5136                    condition: Box::new(expr),
5137                    then_expr: Box::new(then_expr),
5138                    else_expr: Box::new(else_expr),
5139                },
5140                line,
5141            });
5142        }
5143        Ok(expr)
5144    }
5145
5146    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
5147    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
5148    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
5149    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
5150    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
5151    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
5152        let mut left = self.parse_or_word()?;
5153        // Inside a paren-less arg list, `|>` is a hard terminator for the
5154        // enclosing call — leave it for the outer `parse_pipe_forward` loop
5155        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
5156        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
5157        // outer `|>` via its first-arg `parse_assign_expr`.
5158        if self.no_pipe_forward_depth > 0 {
5159            return Ok(left);
5160        }
5161        while matches!(self.peek(), Token::PipeForward) {
5162            if crate::compat_mode() {
5163                return Err(self.syntax_err(
5164                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
5165                    left.line,
5166                ));
5167            }
5168            let line = left.line;
5169            self.advance();
5170            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
5171            // `join`, …) accept a placeholder in place of their list operand.
5172            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
5173            let right_result = self.parse_or_word();
5174            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
5175            let right = right_result?;
5176            left = self.pipe_forward_apply(left, right, line)?;
5177        }
5178        Ok(left)
5179    }
5180
5181    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
5182    /// its **first** argument (Elixir / R / proposed-JS convention).
5183    ///
5184    /// The strategy depends on the shape of `rhs`:
5185    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
5186    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
5187    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
5188    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
5189    ///   matching the `(data, filter)` signature the builtin expects.
5190    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
5191    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
5192    ///   `lhs` (these parse a single default `$_` when called without an arg, so
5193    ///   piping overrides that default; first-arg and last-arg are identical).
5194    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
5195    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
5196    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
5197    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
5198    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
5199    ///   as the sole argument.
5200    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
5201    ///   since silently calling a non-callable at runtime would be worse.
5202    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
5203        let Expr { kind, line: rline } = rhs;
5204        let new_kind = match kind {
5205            // ── Generic / user-defined calls ───────────────────────────────────
5206            ExprKind::FuncCall { name, mut args } => {
5207                match name.as_str() {
5208                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
5209                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
5210                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
5211                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "numbers"
5212                    | "graphemes" | "columns" | "sentences" | "paragraphs" | "sections"
5213                    | "trim" | "avg" | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
5214                    | "to_html" | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline"
5215                    | "bar_chart" | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed"
5216                    | "cb" | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
5217                        if args.is_empty() {
5218                            args.push(lhs);
5219                        } else {
5220                            args[0] = lhs;
5221                        }
5222                    }
5223                    "chunked" | "windowed" => {
5224                        if args.is_empty() {
5225                            return Err(self.syntax_err(
5226                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
5227                                line,
5228                            ));
5229                        }
5230                        args.insert(0, lhs);
5231                    }
5232                    "List::Util::reduce" | "List::Util::fold" => {
5233                        args.push(lhs);
5234                    }
5235                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
5236                        // data |> grep_v "pattern" → grep_v("pattern", data...)
5237                        // data |> pluck "key" → pluck("key", data...)
5238                        // data |> tee "file" → tee("file", data...)
5239                        // data |> nth N → nth(N, data...)
5240                        // data |> chunk N → chunk(N, data...)
5241                        args.push(lhs);
5242                    }
5243                    "enumerate" | "dedup" => {
5244                        // data |> enumerate → enumerate(data)
5245                        // data |> dedup → dedup(data)
5246                        args.insert(0, lhs);
5247                    }
5248                    "clamp" => {
5249                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
5250                        args.push(lhs);
5251                    }
5252                    "pfirst" | "pany" | "any" | "all" | "none" | "first" | "take_while"
5253                    | "drop_while" | "skip_while" | "reject" | "tap" | "peek" | "group_by"
5254                    | "chunk_by" | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
5255                        if args.len() < 2 {
5256                            return Err(self.syntax_err(
5257                                format!(
5258                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
5259                                ),
5260                                line,
5261                            ));
5262                        }
5263                        args[1] = lhs;
5264                    }
5265                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail" => {
5266                        if args.is_empty() {
5267                            return Err(self.syntax_err(
5268                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
5269                                line,
5270                            ));
5271                        }
5272                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
5273                        args.insert(0, lhs);
5274                    }
5275                    _ => {
5276                        args.insert(0, lhs);
5277                    }
5278                }
5279                ExprKind::FuncCall { name, args }
5280            }
5281            ExprKind::MethodCall {
5282                object,
5283                method,
5284                mut args,
5285                super_call,
5286            } => {
5287                args.insert(0, lhs);
5288                ExprKind::MethodCall {
5289                    object,
5290                    method,
5291                    args,
5292                    super_call,
5293                }
5294            }
5295            ExprKind::IndirectCall {
5296                target,
5297                mut args,
5298                ampersand,
5299                pass_caller_arglist: _,
5300            } => {
5301                args.insert(0, lhs);
5302                ExprKind::IndirectCall {
5303                    target,
5304                    args,
5305                    ampersand,
5306                    // Prepending an explicit first arg means this is no longer
5307                    // "pass the caller's @_" — that form is only bare `&$cr`.
5308                    pass_caller_arglist: false,
5309                }
5310            }
5311
5312            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
5313            ExprKind::Print { handle, mut args } => {
5314                args.insert(0, lhs);
5315                ExprKind::Print { handle, args }
5316            }
5317            ExprKind::Say { handle, mut args } => {
5318                args.insert(0, lhs);
5319                ExprKind::Say { handle, args }
5320            }
5321            ExprKind::Printf { handle, mut args } => {
5322                args.insert(0, lhs);
5323                ExprKind::Printf { handle, args }
5324            }
5325            ExprKind::Die(mut args) => {
5326                args.insert(0, lhs);
5327                ExprKind::Die(args)
5328            }
5329            ExprKind::Warn(mut args) => {
5330                args.insert(0, lhs);
5331                ExprKind::Warn(args)
5332            }
5333
5334            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
5335            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
5336            //   but piping the format string is the rarer case. Prepending
5337            //   to the values list gives `sprintf(format, lhs, ...args)` for
5338            //   the common `$n |> sprintf "count=%d"` case.
5339            ExprKind::Sprintf { format, mut args } => {
5340                args.insert(0, lhs);
5341                ExprKind::Sprintf { format, args }
5342            }
5343
5344            // ── System / exec / globbing / filesystem variadics ────────────────
5345            ExprKind::System(mut args) => {
5346                args.insert(0, lhs);
5347                ExprKind::System(args)
5348            }
5349            ExprKind::Exec(mut args) => {
5350                args.insert(0, lhs);
5351                ExprKind::Exec(args)
5352            }
5353            ExprKind::Unlink(mut args) => {
5354                args.insert(0, lhs);
5355                ExprKind::Unlink(args)
5356            }
5357            ExprKind::Chmod(mut args) => {
5358                args.insert(0, lhs);
5359                ExprKind::Chmod(args)
5360            }
5361            ExprKind::Chown(mut args) => {
5362                args.insert(0, lhs);
5363                ExprKind::Chown(args)
5364            }
5365            ExprKind::Glob(mut args) => {
5366                args.insert(0, lhs);
5367                ExprKind::Glob(args)
5368            }
5369            ExprKind::Files(mut args) => {
5370                args.insert(0, lhs);
5371                ExprKind::Files(args)
5372            }
5373            ExprKind::Filesf(mut args) => {
5374                args.insert(0, lhs);
5375                ExprKind::Filesf(args)
5376            }
5377            ExprKind::FilesfRecursive(mut args) => {
5378                args.insert(0, lhs);
5379                ExprKind::FilesfRecursive(args)
5380            }
5381            ExprKind::Dirs(mut args) => {
5382                args.insert(0, lhs);
5383                ExprKind::Dirs(args)
5384            }
5385            ExprKind::DirsRecursive(mut args) => {
5386                args.insert(0, lhs);
5387                ExprKind::DirsRecursive(args)
5388            }
5389            ExprKind::SymLinks(mut args) => {
5390                args.insert(0, lhs);
5391                ExprKind::SymLinks(args)
5392            }
5393            ExprKind::Sockets(mut args) => {
5394                args.insert(0, lhs);
5395                ExprKind::Sockets(args)
5396            }
5397            ExprKind::Pipes(mut args) => {
5398                args.insert(0, lhs);
5399                ExprKind::Pipes(args)
5400            }
5401            ExprKind::BlockDevices(mut args) => {
5402                args.insert(0, lhs);
5403                ExprKind::BlockDevices(args)
5404            }
5405            ExprKind::CharDevices(mut args) => {
5406                args.insert(0, lhs);
5407                ExprKind::CharDevices(args)
5408            }
5409            ExprKind::GlobPar { mut args, progress } => {
5410                args.insert(0, lhs);
5411                ExprKind::GlobPar { args, progress }
5412            }
5413            ExprKind::ParSed { mut args, progress } => {
5414                args.insert(0, lhs);
5415                ExprKind::ParSed { args, progress }
5416            }
5417
5418            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
5419            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
5420            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
5421            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
5422            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
5423            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
5424            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
5425            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
5426            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
5427            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
5428            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
5429            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
5430            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
5431            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
5432            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
5433            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
5434            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
5435            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
5436            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
5437            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
5438            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
5439            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
5440            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
5441            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
5442            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
5443            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
5444            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
5445            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
5446            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
5447            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
5448            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
5449            ExprKind::ScalarReverse(_) => ExprKind::ScalarReverse(Box::new(lhs)),
5450            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
5451            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
5452            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
5453            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
5454            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
5455            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
5456            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
5457            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
5458            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
5459            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
5460            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
5461            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
5462            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
5463            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
5464            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
5465            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
5466            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
5467            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
5468            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
5469            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
5470
5471            // ── Higher-order / list-taking forms: replace the `list` slot ──────
5472            ExprKind::MapExpr {
5473                block,
5474                list: _,
5475                flatten_array_refs,
5476                stream,
5477            } => ExprKind::MapExpr {
5478                block,
5479                list: Box::new(lhs),
5480                flatten_array_refs,
5481                stream,
5482            },
5483            ExprKind::MapExprComma {
5484                expr,
5485                list: _,
5486                flatten_array_refs,
5487                stream,
5488            } => ExprKind::MapExprComma {
5489                expr,
5490                list: Box::new(lhs),
5491                flatten_array_refs,
5492                stream,
5493            },
5494            ExprKind::GrepExpr {
5495                block,
5496                list: _,
5497                keyword,
5498            } => ExprKind::GrepExpr {
5499                block,
5500                list: Box::new(lhs),
5501                keyword,
5502            },
5503            ExprKind::GrepExprComma {
5504                expr,
5505                list: _,
5506                keyword,
5507            } => ExprKind::GrepExprComma {
5508                expr,
5509                list: Box::new(lhs),
5510                keyword,
5511            },
5512            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
5513                block,
5514                list: Box::new(lhs),
5515            },
5516            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
5517                cmp,
5518                list: Box::new(lhs),
5519            },
5520            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
5521                separator,
5522                list: Box::new(lhs),
5523            },
5524            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
5525                block,
5526                list: Box::new(lhs),
5527            },
5528            ExprKind::PMapExpr {
5529                block,
5530                list: _,
5531                progress,
5532                flat_outputs,
5533                on_cluster,
5534            } => ExprKind::PMapExpr {
5535                block,
5536                list: Box::new(lhs),
5537                progress,
5538                flat_outputs,
5539                on_cluster,
5540            },
5541            ExprKind::PMapChunkedExpr {
5542                chunk_size,
5543                block,
5544                list: _,
5545                progress,
5546            } => ExprKind::PMapChunkedExpr {
5547                chunk_size,
5548                block,
5549                list: Box::new(lhs),
5550                progress,
5551            },
5552            ExprKind::PGrepExpr {
5553                block,
5554                list: _,
5555                progress,
5556            } => ExprKind::PGrepExpr {
5557                block,
5558                list: Box::new(lhs),
5559                progress,
5560            },
5561            ExprKind::PForExpr {
5562                block,
5563                list: _,
5564                progress,
5565            } => ExprKind::PForExpr {
5566                block,
5567                list: Box::new(lhs),
5568                progress,
5569            },
5570            ExprKind::PSortExpr {
5571                cmp,
5572                list: _,
5573                progress,
5574            } => ExprKind::PSortExpr {
5575                cmp,
5576                list: Box::new(lhs),
5577                progress,
5578            },
5579            ExprKind::PReduceExpr {
5580                block,
5581                list: _,
5582                progress,
5583            } => ExprKind::PReduceExpr {
5584                block,
5585                list: Box::new(lhs),
5586                progress,
5587            },
5588            ExprKind::PcacheExpr {
5589                block,
5590                list: _,
5591                progress,
5592            } => ExprKind::PcacheExpr {
5593                block,
5594                list: Box::new(lhs),
5595                progress,
5596            },
5597            ExprKind::PReduceInitExpr {
5598                init,
5599                block,
5600                list: _,
5601                progress,
5602            } => ExprKind::PReduceInitExpr {
5603                init,
5604                block,
5605                list: Box::new(lhs),
5606                progress,
5607            },
5608            ExprKind::PMapReduceExpr {
5609                map_block,
5610                reduce_block,
5611                list: _,
5612                progress,
5613            } => ExprKind::PMapReduceExpr {
5614                map_block,
5615                reduce_block,
5616                list: Box::new(lhs),
5617                progress,
5618            },
5619
5620            // ── Push / unshift: first arg is the array, so pipe the LHS
5621            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
5622            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
5623            //     directly for that.
5624            ExprKind::Push { array, mut values } => {
5625                values.insert(0, lhs);
5626                ExprKind::Push { array, values }
5627            }
5628            ExprKind::Unshift { array, mut values } => {
5629                values.insert(0, lhs);
5630                ExprKind::Unshift { array, values }
5631            }
5632
5633            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
5634            ExprKind::SplitExpr {
5635                pattern,
5636                string: _,
5637                limit,
5638            } => ExprKind::SplitExpr {
5639                pattern,
5640                string: Box::new(lhs),
5641                limit,
5642            },
5643
5644            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
5645            //    Auto-inject `r` flag so the substitution returns the modified
5646            //    string instead of the match count (non-destructive / Perl /r).
5647            ExprKind::Substitution {
5648                pattern,
5649                replacement,
5650                mut flags,
5651                expr: _,
5652                delim,
5653            } => {
5654                if !flags.contains('r') {
5655                    flags.push('r');
5656                }
5657                ExprKind::Substitution {
5658                    expr: Box::new(lhs),
5659                    pattern,
5660                    replacement,
5661                    flags,
5662                    delim,
5663                }
5664            }
5665            ExprKind::Transliterate {
5666                from,
5667                to,
5668                mut flags,
5669                expr: _,
5670                delim,
5671            } => {
5672                if !flags.contains('r') {
5673                    flags.push('r');
5674                }
5675                ExprKind::Transliterate {
5676                    expr: Box::new(lhs),
5677                    from,
5678                    to,
5679                    flags,
5680                    delim,
5681                }
5682            }
5683            ExprKind::Match {
5684                pattern,
5685                flags,
5686                scalar_g,
5687                expr: _,
5688                delim,
5689            } => ExprKind::Match {
5690                expr: Box::new(lhs),
5691                pattern,
5692                flags,
5693                scalar_g,
5694                delim,
5695            },
5696            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
5697            ExprKind::Regex(pattern, flags) => ExprKind::Match {
5698                expr: Box::new(lhs),
5699                pattern,
5700                flags,
5701                scalar_g: false,
5702                delim: '/',
5703            },
5704
5705            // ── Bareword function name → plain unary call ──────────────────────
5706            ExprKind::Bareword(name) => match name.as_str() {
5707                "rv" | "reverse" | "reversed" => ExprKind::ReverseExpr(Box::new(lhs)),
5708                "rev" => ExprKind::ScalarReverse(Box::new(lhs)),
5709                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
5710                    name: "uniq".to_string(),
5711                    args: vec![lhs],
5712                },
5713                "fl" | "flatten" => ExprKind::FuncCall {
5714                    name: "flatten".to_string(),
5715                    args: vec![lhs],
5716                },
5717                _ => ExprKind::FuncCall {
5718                    name,
5719                    args: vec![lhs],
5720                },
5721            },
5722
5723            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
5724            kind @ (ExprKind::ScalarVar(_)
5725            | ExprKind::ArrayElement { .. }
5726            | ExprKind::HashElement { .. }
5727            | ExprKind::Deref { .. }
5728            | ExprKind::ArrowDeref { .. }
5729            | ExprKind::CodeRef { .. }
5730            | ExprKind::SubroutineRef(_)
5731            | ExprKind::SubroutineCodeRef(_)
5732            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
5733                target: Box::new(Expr { kind, line: rline }),
5734                args: vec![lhs],
5735                ampersand: false,
5736                pass_caller_arglist: false,
5737            },
5738
5739            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
5740            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
5741            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> sub { ... }`.
5742            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
5743                ExprKind::IndirectCall {
5744                    target: inner,
5745                    args: vec![lhs],
5746                    ampersand: false,
5747                    pass_caller_arglist: false,
5748                }
5749            }
5750
5751            other => {
5752                return Err(self.syntax_err(
5753                    format!(
5754                        "right-hand side of `|>` must be a call, builtin, or coderef \
5755                         expression (got {})",
5756                        Self::expr_kind_name(&other)
5757                    ),
5758                    line,
5759                ));
5760            }
5761        };
5762        Ok(Expr {
5763            kind: new_kind,
5764            line,
5765        })
5766    }
5767
5768    /// Short label for an `ExprKind` (used in `|>` error messages).
5769    fn expr_kind_name(kind: &ExprKind) -> &'static str {
5770        match kind {
5771            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
5772            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
5773            ExprKind::BinOp { .. } => "binary expression",
5774            ExprKind::UnaryOp { .. } => "unary expression",
5775            ExprKind::Ternary { .. } => "ternary expression",
5776            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
5777            ExprKind::List(_) => "list expression",
5778            ExprKind::Range { .. } => "range expression",
5779            _ => "expression",
5780        }
5781    }
5782
5783    // or / not (lowest precedence word operators)
5784    fn parse_or_word(&mut self) -> PerlResult<Expr> {
5785        let mut left = self.parse_and_word()?;
5786        while matches!(self.peek(), Token::LogOrWord) {
5787            let line = left.line;
5788            self.advance();
5789            let right = self.parse_and_word()?;
5790            left = Expr {
5791                kind: ExprKind::BinOp {
5792                    left: Box::new(left),
5793                    op: BinOp::LogOrWord,
5794                    right: Box::new(right),
5795                },
5796                line,
5797            };
5798        }
5799        Ok(left)
5800    }
5801
5802    fn parse_and_word(&mut self) -> PerlResult<Expr> {
5803        let mut left = self.parse_not_word()?;
5804        while matches!(self.peek(), Token::LogAndWord) {
5805            let line = left.line;
5806            self.advance();
5807            let right = self.parse_not_word()?;
5808            left = Expr {
5809                kind: ExprKind::BinOp {
5810                    left: Box::new(left),
5811                    op: BinOp::LogAndWord,
5812                    right: Box::new(right),
5813                },
5814                line,
5815            };
5816        }
5817        Ok(left)
5818    }
5819
5820    fn parse_not_word(&mut self) -> PerlResult<Expr> {
5821        if matches!(self.peek(), Token::LogNotWord) {
5822            let line = self.peek_line();
5823            self.advance();
5824            let expr = self.parse_not_word()?;
5825            return Ok(Expr {
5826                kind: ExprKind::UnaryOp {
5827                    op: UnaryOp::LogNotWord,
5828                    expr: Box::new(expr),
5829                },
5830                line,
5831            });
5832        }
5833        self.parse_range()
5834    }
5835
5836    fn parse_log_or(&mut self) -> PerlResult<Expr> {
5837        let mut left = self.parse_log_and()?;
5838        loop {
5839            let op = match self.peek() {
5840                Token::LogOr => BinOp::LogOr,
5841                Token::DefinedOr => BinOp::DefinedOr,
5842                _ => break,
5843            };
5844            let line = left.line;
5845            self.advance();
5846            let right = self.parse_log_and()?;
5847            left = Expr {
5848                kind: ExprKind::BinOp {
5849                    left: Box::new(left),
5850                    op,
5851                    right: Box::new(right),
5852                },
5853                line,
5854            };
5855        }
5856        Ok(left)
5857    }
5858
5859    fn parse_log_and(&mut self) -> PerlResult<Expr> {
5860        let mut left = self.parse_bit_or()?;
5861        while matches!(self.peek(), Token::LogAnd) {
5862            let line = left.line;
5863            self.advance();
5864            let right = self.parse_bit_or()?;
5865            left = Expr {
5866                kind: ExprKind::BinOp {
5867                    left: Box::new(left),
5868                    op: BinOp::LogAnd,
5869                    right: Box::new(right),
5870                },
5871                line,
5872            };
5873        }
5874        Ok(left)
5875    }
5876
5877    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
5878        let mut left = self.parse_bit_xor()?;
5879        while matches!(self.peek(), Token::BitOr) {
5880            let line = left.line;
5881            self.advance();
5882            let right = self.parse_bit_xor()?;
5883            left = Expr {
5884                kind: ExprKind::BinOp {
5885                    left: Box::new(left),
5886                    op: BinOp::BitOr,
5887                    right: Box::new(right),
5888                },
5889                line,
5890            };
5891        }
5892        Ok(left)
5893    }
5894
5895    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
5896        let mut left = self.parse_bit_and()?;
5897        while matches!(self.peek(), Token::BitXor) {
5898            let line = left.line;
5899            self.advance();
5900            let right = self.parse_bit_and()?;
5901            left = Expr {
5902                kind: ExprKind::BinOp {
5903                    left: Box::new(left),
5904                    op: BinOp::BitXor,
5905                    right: Box::new(right),
5906                },
5907                line,
5908            };
5909        }
5910        Ok(left)
5911    }
5912
5913    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
5914        let mut left = self.parse_equality()?;
5915        while matches!(self.peek(), Token::BitAnd) {
5916            let line = left.line;
5917            self.advance();
5918            let right = self.parse_equality()?;
5919            left = Expr {
5920                kind: ExprKind::BinOp {
5921                    left: Box::new(left),
5922                    op: BinOp::BitAnd,
5923                    right: Box::new(right),
5924                },
5925                line,
5926            };
5927        }
5928        Ok(left)
5929    }
5930
5931    fn parse_equality(&mut self) -> PerlResult<Expr> {
5932        let mut left = self.parse_comparison()?;
5933        loop {
5934            let op = match self.peek() {
5935                Token::NumEq => BinOp::NumEq,
5936                Token::NumNe => BinOp::NumNe,
5937                Token::StrEq => BinOp::StrEq,
5938                Token::StrNe => BinOp::StrNe,
5939                Token::Spaceship => BinOp::Spaceship,
5940                Token::StrCmp => BinOp::StrCmp,
5941                _ => break,
5942            };
5943            let line = left.line;
5944            self.advance();
5945            let right = self.parse_comparison()?;
5946            left = Expr {
5947                kind: ExprKind::BinOp {
5948                    left: Box::new(left),
5949                    op,
5950                    right: Box::new(right),
5951                },
5952                line,
5953            };
5954        }
5955        Ok(left)
5956    }
5957
5958    fn parse_comparison(&mut self) -> PerlResult<Expr> {
5959        let mut left = self.parse_shift()?;
5960        loop {
5961            let op = match self.peek() {
5962                Token::NumLt => BinOp::NumLt,
5963                Token::NumGt => BinOp::NumGt,
5964                Token::NumLe => BinOp::NumLe,
5965                Token::NumGe => BinOp::NumGe,
5966                Token::StrLt => BinOp::StrLt,
5967                Token::StrGt => BinOp::StrGt,
5968                Token::StrLe => BinOp::StrLe,
5969                Token::StrGe => BinOp::StrGe,
5970                _ => break,
5971            };
5972            let line = left.line;
5973            self.advance();
5974            let right = self.parse_shift()?;
5975            left = Expr {
5976                kind: ExprKind::BinOp {
5977                    left: Box::new(left),
5978                    op,
5979                    right: Box::new(right),
5980                },
5981                line,
5982            };
5983        }
5984        Ok(left)
5985    }
5986
5987    fn parse_shift(&mut self) -> PerlResult<Expr> {
5988        let mut left = self.parse_addition()?;
5989        loop {
5990            let op = match self.peek() {
5991                Token::ShiftLeft => BinOp::ShiftLeft,
5992                Token::ShiftRight => BinOp::ShiftRight,
5993                _ => break,
5994            };
5995            let line = left.line;
5996            self.advance();
5997            let right = self.parse_addition()?;
5998            left = Expr {
5999                kind: ExprKind::BinOp {
6000                    left: Box::new(left),
6001                    op,
6002                    right: Box::new(right),
6003                },
6004                line,
6005            };
6006        }
6007        Ok(left)
6008    }
6009
6010    fn parse_addition(&mut self) -> PerlResult<Expr> {
6011        let mut left = self.parse_multiplication()?;
6012        loop {
6013            let op = match self.peek() {
6014                Token::Plus => BinOp::Add,
6015                Token::Minus => BinOp::Sub,
6016                Token::Dot => BinOp::Concat,
6017                _ => break,
6018            };
6019            let line = left.line;
6020            self.advance();
6021            let right = self.parse_multiplication()?;
6022            left = Expr {
6023                kind: ExprKind::BinOp {
6024                    left: Box::new(left),
6025                    op,
6026                    right: Box::new(right),
6027                },
6028                line,
6029            };
6030        }
6031        Ok(left)
6032    }
6033
6034    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
6035        let mut left = self.parse_regex_bind()?;
6036        loop {
6037            let op = match self.peek() {
6038                Token::Star => BinOp::Mul,
6039                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
6040                Token::Percent => BinOp::Mod,
6041                Token::X => {
6042                    let line = left.line;
6043                    self.advance();
6044                    let right = self.parse_regex_bind()?;
6045                    left = Expr {
6046                        kind: ExprKind::Repeat {
6047                            expr: Box::new(left),
6048                            count: Box::new(right),
6049                        },
6050                        line,
6051                    };
6052                    continue;
6053                }
6054                _ => break,
6055            };
6056            let line = left.line;
6057            self.advance();
6058            let right = self.parse_regex_bind()?;
6059            left = Expr {
6060                kind: ExprKind::BinOp {
6061                    left: Box::new(left),
6062                    op,
6063                    right: Box::new(right),
6064                },
6065                line,
6066            };
6067        }
6068        Ok(left)
6069    }
6070
6071    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
6072        let left = self.parse_unary()?;
6073        match self.peek() {
6074            Token::BindMatch => {
6075                let line = left.line;
6076                self.advance();
6077                match self.peek().clone() {
6078                    Token::Regex(pattern, flags, delim) => {
6079                        self.advance();
6080                        Ok(Expr {
6081                            kind: ExprKind::Match {
6082                                expr: Box::new(left),
6083                                pattern,
6084                                flags,
6085                                scalar_g: false,
6086                                delim,
6087                            },
6088                            line,
6089                        })
6090                    }
6091                    Token::Ident(ref s) if s.starts_with('\x00') => {
6092                        let (Token::Ident(encoded), _) = self.advance() else {
6093                            unreachable!()
6094                        };
6095                        let parts: Vec<&str> = encoded.split('\x00').collect();
6096                        if parts.len() >= 4 && parts[1] == "s" {
6097                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6098                            Ok(Expr {
6099                                kind: ExprKind::Substitution {
6100                                    expr: Box::new(left),
6101                                    pattern: parts[2].to_string(),
6102                                    replacement: parts[3].to_string(),
6103                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6104                                    delim,
6105                                },
6106                                line,
6107                            })
6108                        } else if parts.len() >= 4 && parts[1] == "tr" {
6109                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6110                            Ok(Expr {
6111                                kind: ExprKind::Transliterate {
6112                                    expr: Box::new(left),
6113                                    from: parts[2].to_string(),
6114                                    to: parts[3].to_string(),
6115                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6116                                    delim,
6117                                },
6118                                line,
6119                            })
6120                        } else {
6121                            Err(self.syntax_err("Invalid regex binding", line))
6122                        }
6123                    }
6124                    _ => {
6125                        let rhs = self.parse_unary()?;
6126                        Ok(Expr {
6127                            kind: ExprKind::BinOp {
6128                                left: Box::new(left),
6129                                op: BinOp::BindMatch,
6130                                right: Box::new(rhs),
6131                            },
6132                            line,
6133                        })
6134                    }
6135                }
6136            }
6137            Token::BindNotMatch => {
6138                let line = left.line;
6139                self.advance();
6140                match self.peek().clone() {
6141                    Token::Regex(pattern, flags, delim) => {
6142                        self.advance();
6143                        Ok(Expr {
6144                            kind: ExprKind::UnaryOp {
6145                                op: UnaryOp::LogNot,
6146                                expr: Box::new(Expr {
6147                                    kind: ExprKind::Match {
6148                                        expr: Box::new(left),
6149                                        pattern,
6150                                        flags,
6151                                        scalar_g: false,
6152                                        delim,
6153                                    },
6154                                    line,
6155                                }),
6156                            },
6157                            line,
6158                        })
6159                    }
6160                    Token::Ident(ref s) if s.starts_with('\x00') => {
6161                        let (Token::Ident(encoded), _) = self.advance() else {
6162                            unreachable!()
6163                        };
6164                        let parts: Vec<&str> = encoded.split('\x00').collect();
6165                        if parts.len() >= 4 && parts[1] == "s" {
6166                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6167                            Ok(Expr {
6168                                kind: ExprKind::UnaryOp {
6169                                    op: UnaryOp::LogNot,
6170                                    expr: Box::new(Expr {
6171                                        kind: ExprKind::Substitution {
6172                                            expr: Box::new(left),
6173                                            pattern: parts[2].to_string(),
6174                                            replacement: parts[3].to_string(),
6175                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6176                                            delim,
6177                                        },
6178                                        line,
6179                                    }),
6180                                },
6181                                line,
6182                            })
6183                        } else if parts.len() >= 4 && parts[1] == "tr" {
6184                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6185                            Ok(Expr {
6186                                kind: ExprKind::UnaryOp {
6187                                    op: UnaryOp::LogNot,
6188                                    expr: Box::new(Expr {
6189                                        kind: ExprKind::Transliterate {
6190                                            expr: Box::new(left),
6191                                            from: parts[2].to_string(),
6192                                            to: parts[3].to_string(),
6193                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6194                                            delim,
6195                                        },
6196                                        line,
6197                                    }),
6198                                },
6199                                line,
6200                            })
6201                        } else {
6202                            Err(self.syntax_err("Invalid regex binding after !~", line))
6203                        }
6204                    }
6205                    _ => {
6206                        let rhs = self.parse_unary()?;
6207                        Ok(Expr {
6208                            kind: ExprKind::BinOp {
6209                                left: Box::new(left),
6210                                op: BinOp::BindNotMatch,
6211                                right: Box::new(rhs),
6212                            },
6213                            line,
6214                        })
6215                    }
6216                }
6217            }
6218            _ => Ok(left),
6219        }
6220    }
6221
6222    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
6223    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
6224    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
6225        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
6226        let result = self.parse_range();
6227        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
6228        result
6229    }
6230
6231    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
6232    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
6233    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
6234    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
6235    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
6236    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
6237    fn parse_range(&mut self) -> PerlResult<Expr> {
6238        let left = self.parse_log_or()?;
6239        let line = left.line;
6240        let exclusive = if self.eat(&Token::RangeExclusive) {
6241            true
6242        } else if self.eat(&Token::Range) {
6243            false
6244        } else {
6245            return Ok(left);
6246        };
6247        let right = self.parse_log_or()?;
6248        Ok(Expr {
6249            kind: ExprKind::Range {
6250                from: Box::new(left),
6251                to: Box::new(right),
6252                exclusive,
6253            },
6254            line,
6255        })
6256    }
6257
6258    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
6259    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
6260        let mut name = match self.advance() {
6261            (Token::Ident(n), _) => n,
6262            (tok, l) => {
6263                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
6264            }
6265        };
6266        while self.eat(&Token::PackageSep) {
6267            match self.advance() {
6268                (Token::Ident(part), _) => {
6269                    name.push_str("::");
6270                    name.push_str(&part);
6271                }
6272                (tok, l) => {
6273                    return Err(self
6274                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
6275                }
6276            }
6277        }
6278        Ok(name)
6279    }
6280
6281    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
6282    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
6283        self.parse_package_qualified_identifier()
6284    }
6285
6286    fn parse_unary(&mut self) -> PerlResult<Expr> {
6287        let line = self.peek_line();
6288        match self.peek().clone() {
6289            Token::Minus => {
6290                self.advance();
6291                let expr = self.parse_power()?;
6292                Ok(Expr {
6293                    kind: ExprKind::UnaryOp {
6294                        op: UnaryOp::Negate,
6295                        expr: Box::new(expr),
6296                    },
6297                    line,
6298                })
6299            }
6300            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
6301            // and for scalar context; treat as a no-op on the parsed operand.
6302            Token::Plus => {
6303                self.advance();
6304                self.parse_unary()
6305            }
6306            Token::LogNot => {
6307                self.advance();
6308                let expr = self.parse_unary()?;
6309                Ok(Expr {
6310                    kind: ExprKind::UnaryOp {
6311                        op: UnaryOp::LogNot,
6312                        expr: Box::new(expr),
6313                    },
6314                    line,
6315                })
6316            }
6317            Token::BitNot => {
6318                self.advance();
6319                let expr = self.parse_unary()?;
6320                Ok(Expr {
6321                    kind: ExprKind::UnaryOp {
6322                        op: UnaryOp::BitNot,
6323                        expr: Box::new(expr),
6324                    },
6325                    line,
6326                })
6327            }
6328            Token::Increment => {
6329                self.advance();
6330                let expr = self.parse_postfix()?;
6331                Ok(Expr {
6332                    kind: ExprKind::UnaryOp {
6333                        op: UnaryOp::PreIncrement,
6334                        expr: Box::new(expr),
6335                    },
6336                    line,
6337                })
6338            }
6339            Token::Decrement => {
6340                self.advance();
6341                let expr = self.parse_postfix()?;
6342                Ok(Expr {
6343                    kind: ExprKind::UnaryOp {
6344                        op: UnaryOp::PreDecrement,
6345                        expr: Box::new(expr),
6346                    },
6347                    line,
6348                })
6349            }
6350            Token::BitAnd => {
6351                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
6352                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
6353                self.advance();
6354                if matches!(self.peek(), Token::LBrace) {
6355                    self.advance();
6356                    let inner = self.parse_expression()?;
6357                    self.expect(&Token::RBrace)?;
6358                    return Ok(Expr {
6359                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
6360                        line,
6361                    });
6362                }
6363                if matches!(self.peek(), Token::Ident(_)) {
6364                    let name = self.parse_qualified_subroutine_name()?;
6365                    return Ok(Expr {
6366                        kind: ExprKind::SubroutineRef(name),
6367                        line,
6368                    });
6369                }
6370                let target = self.parse_primary()?;
6371                if matches!(self.peek(), Token::LParen) {
6372                    self.advance();
6373                    let args = self.parse_arg_list()?;
6374                    self.expect(&Token::RParen)?;
6375                    return Ok(Expr {
6376                        kind: ExprKind::IndirectCall {
6377                            target: Box::new(target),
6378                            args,
6379                            ampersand: true,
6380                            pass_caller_arglist: false,
6381                        },
6382                        line,
6383                    });
6384                }
6385                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
6386                Ok(Expr {
6387                    kind: ExprKind::IndirectCall {
6388                        target: Box::new(target),
6389                        args: vec![],
6390                        ampersand: true,
6391                        pass_caller_arglist: true,
6392                    },
6393                    line,
6394                })
6395            }
6396            Token::Backslash => {
6397                self.advance();
6398                let expr = self.parse_unary()?;
6399                if let ExprKind::SubroutineRef(name) = expr.kind {
6400                    return Ok(Expr {
6401                        kind: ExprKind::SubroutineCodeRef(name),
6402                        line,
6403                    });
6404                }
6405                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
6406                    return Ok(expr);
6407                }
6408                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
6409                Ok(Expr {
6410                    kind: ExprKind::ScalarRef(Box::new(expr)),
6411                    line,
6412                })
6413            }
6414            Token::FileTest(op) => {
6415                self.advance();
6416                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
6417                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
6418                    Expr {
6419                        kind: ExprKind::ScalarVar("_".into()),
6420                        line: self.peek_line(),
6421                    }
6422                } else {
6423                    self.parse_unary()?
6424                };
6425                Ok(Expr {
6426                    kind: ExprKind::FileTest {
6427                        op,
6428                        expr: Box::new(expr),
6429                    },
6430                    line,
6431                })
6432            }
6433            _ => self.parse_power(),
6434        }
6435    }
6436
6437    fn parse_power(&mut self) -> PerlResult<Expr> {
6438        let left = self.parse_postfix()?;
6439        if matches!(self.peek(), Token::Power) {
6440            let line = left.line;
6441            self.advance();
6442            let right = self.parse_unary()?; // right-associative
6443            return Ok(Expr {
6444                kind: ExprKind::BinOp {
6445                    left: Box::new(left),
6446                    op: BinOp::Pow,
6447                    right: Box::new(right),
6448                },
6449                line,
6450            });
6451        }
6452        Ok(left)
6453    }
6454
6455    fn parse_postfix(&mut self) -> PerlResult<Expr> {
6456        let mut expr = self.parse_primary()?;
6457        loop {
6458            match self.peek().clone() {
6459                Token::Increment => {
6460                    let line = expr.line;
6461                    self.advance();
6462                    expr = Expr {
6463                        kind: ExprKind::PostfixOp {
6464                            expr: Box::new(expr),
6465                            op: PostfixOp::Increment,
6466                        },
6467                        line,
6468                    };
6469                }
6470                Token::Decrement => {
6471                    let line = expr.line;
6472                    self.advance();
6473                    expr = Expr {
6474                        kind: ExprKind::PostfixOp {
6475                            expr: Box::new(expr),
6476                            op: PostfixOp::Decrement,
6477                        },
6478                        line,
6479                    };
6480                }
6481                Token::LParen => {
6482                    if self.suppress_indirect_paren_call > 0 {
6483                        break;
6484                    }
6485                    // Implicit semicolon: `(` on a new line after an expression
6486                    // is a new statement, not a postfix code-ref call.
6487                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
6488                    if self.peek_line() > self.prev_line() {
6489                        break;
6490                    }
6491                    let line = expr.line;
6492                    self.advance();
6493                    let args = self.parse_arg_list()?;
6494                    self.expect(&Token::RParen)?;
6495                    expr = Expr {
6496                        kind: ExprKind::IndirectCall {
6497                            target: Box::new(expr),
6498                            args,
6499                            ampersand: false,
6500                            pass_caller_arglist: false,
6501                        },
6502                        line,
6503                    };
6504                }
6505                Token::Arrow => {
6506                    let line = expr.line;
6507                    self.advance();
6508                    match self.peek().clone() {
6509                        Token::LBracket => {
6510                            self.advance();
6511                            let index = self.parse_expression()?;
6512                            self.expect(&Token::RBracket)?;
6513                            expr = Expr {
6514                                kind: ExprKind::ArrowDeref {
6515                                    expr: Box::new(expr),
6516                                    index: Box::new(index),
6517                                    kind: DerefKind::Array,
6518                                },
6519                                line,
6520                            };
6521                        }
6522                        Token::LBrace => {
6523                            self.advance();
6524                            let key = self.parse_hash_subscript_key()?;
6525                            self.expect(&Token::RBrace)?;
6526                            expr = Expr {
6527                                kind: ExprKind::ArrowDeref {
6528                                    expr: Box::new(expr),
6529                                    index: Box::new(key),
6530                                    kind: DerefKind::Hash,
6531                                },
6532                                line,
6533                            };
6534                        }
6535                        Token::LParen => {
6536                            self.advance();
6537                            let args = self.parse_arg_list()?;
6538                            self.expect(&Token::RParen)?;
6539                            expr = Expr {
6540                                kind: ExprKind::ArrowDeref {
6541                                    expr: Box::new(expr),
6542                                    index: Box::new(Expr {
6543                                        kind: ExprKind::List(args),
6544                                        line,
6545                                    }),
6546                                    kind: DerefKind::Call,
6547                                },
6548                                line,
6549                            };
6550                        }
6551                        Token::Ident(method) => {
6552                            self.advance();
6553                            if method == "SUPER" {
6554                                self.expect(&Token::PackageSep)?;
6555                                let real_method = match self.advance() {
6556                                    (Token::Ident(n), _) => n,
6557                                    (tok, l) => {
6558                                        return Err(self.syntax_err(
6559                                            format!(
6560                                                "Expected method name after SUPER::, got {:?}",
6561                                                tok
6562                                            ),
6563                                            l,
6564                                        ));
6565                                    }
6566                                };
6567                                let args = if self.eat(&Token::LParen) {
6568                                    let a = self.parse_arg_list()?;
6569                                    self.expect(&Token::RParen)?;
6570                                    a
6571                                } else {
6572                                    self.parse_method_arg_list_no_paren()?
6573                                };
6574                                expr = Expr {
6575                                    kind: ExprKind::MethodCall {
6576                                        object: Box::new(expr),
6577                                        method: real_method,
6578                                        args,
6579                                        super_call: true,
6580                                    },
6581                                    line,
6582                                };
6583                            } else {
6584                                let mut method_name = method;
6585                                while self.eat(&Token::PackageSep) {
6586                                    match self.advance() {
6587                                        (Token::Ident(part), _) => {
6588                                            method_name.push_str("::");
6589                                            method_name.push_str(&part);
6590                                        }
6591                                        (tok, l) => {
6592                                            return Err(self.syntax_err(
6593                                                format!(
6594                                                    "Expected identifier after :: in method name, got {:?}",
6595                                                    tok
6596                                                ),
6597                                                l,
6598                                            ));
6599                                        }
6600                                    }
6601                                }
6602                                let args = if self.eat(&Token::LParen) {
6603                                    let a = self.parse_arg_list()?;
6604                                    self.expect(&Token::RParen)?;
6605                                    a
6606                                } else {
6607                                    self.parse_method_arg_list_no_paren()?
6608                                };
6609                                expr = Expr {
6610                                    kind: ExprKind::MethodCall {
6611                                        object: Box::new(expr),
6612                                        method: method_name,
6613                                        args,
6614                                        super_call: false,
6615                                    },
6616                                    line,
6617                                };
6618                            }
6619                        }
6620                        // Postfix dereference (Perl 5.20+, default 5.24+):
6621                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
6622                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
6623                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
6624                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
6625                        Token::ArrayAt => {
6626                            self.advance(); // consume `@`
6627                            match self.peek().clone() {
6628                                Token::Star => {
6629                                    self.advance();
6630                                    expr = Expr {
6631                                        kind: ExprKind::Deref {
6632                                            expr: Box::new(expr),
6633                                            kind: Sigil::Array,
6634                                        },
6635                                        line,
6636                                    };
6637                                }
6638                                Token::LBracket => {
6639                                    self.advance();
6640                                    let mut indices = Vec::new();
6641                                    while !matches!(self.peek(), Token::RBracket | Token::Eof) {
6642                                        indices.push(self.parse_assign_expr()?);
6643                                        if !self.eat(&Token::Comma) {
6644                                            break;
6645                                        }
6646                                    }
6647                                    self.expect(&Token::RBracket)?;
6648                                    let source = Expr {
6649                                        kind: ExprKind::Deref {
6650                                            expr: Box::new(expr),
6651                                            kind: Sigil::Array,
6652                                        },
6653                                        line,
6654                                    };
6655                                    expr = Expr {
6656                                        kind: ExprKind::AnonymousListSlice {
6657                                            source: Box::new(source),
6658                                            indices,
6659                                        },
6660                                        line,
6661                                    };
6662                                }
6663                                Token::LBrace => {
6664                                    self.advance();
6665                                    let mut keys = Vec::new();
6666                                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
6667                                        keys.push(self.parse_assign_expr()?);
6668                                        if !self.eat(&Token::Comma) {
6669                                            break;
6670                                        }
6671                                    }
6672                                    self.expect(&Token::RBrace)?;
6673                                    expr = Expr {
6674                                        kind: ExprKind::HashSliceDeref {
6675                                            container: Box::new(expr),
6676                                            keys,
6677                                        },
6678                                        line,
6679                                    };
6680                                }
6681                                tok => {
6682                                    return Err(self.syntax_err(
6683                                        format!(
6684                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
6685                                            tok
6686                                        ),
6687                                        line,
6688                                    ));
6689                                }
6690                            }
6691                        }
6692                        Token::HashPercent => {
6693                            self.advance(); // consume `%`
6694                            match self.peek().clone() {
6695                                Token::Star => {
6696                                    self.advance();
6697                                    expr = Expr {
6698                                        kind: ExprKind::Deref {
6699                                            expr: Box::new(expr),
6700                                            kind: Sigil::Hash,
6701                                        },
6702                                        line,
6703                                    };
6704                                }
6705                                tok => {
6706                                    return Err(self.syntax_err(
6707                                        format!("Expected `*` after `->%`, got {:?}", tok),
6708                                        line,
6709                                    ));
6710                                }
6711                            }
6712                        }
6713                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
6714                        Token::X => {
6715                            self.advance();
6716                            let args = if self.eat(&Token::LParen) {
6717                                let a = self.parse_arg_list()?;
6718                                self.expect(&Token::RParen)?;
6719                                a
6720                            } else {
6721                                self.parse_method_arg_list_no_paren()?
6722                            };
6723                            expr = Expr {
6724                                kind: ExprKind::MethodCall {
6725                                    object: Box::new(expr),
6726                                    method: "x".to_string(),
6727                                    args,
6728                                    super_call: false,
6729                                },
6730                                line,
6731                            };
6732                        }
6733                        _ => break,
6734                    }
6735                }
6736                Token::LBracket => {
6737                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
6738                    let line = expr.line;
6739                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
6740                        if let ExprKind::ScalarVar(ref name) = expr.kind {
6741                            let name = name.clone();
6742                            self.advance();
6743                            let index = self.parse_expression()?;
6744                            self.expect(&Token::RBracket)?;
6745                            expr = Expr {
6746                                kind: ExprKind::ArrayElement {
6747                                    array: name,
6748                                    index: Box::new(index),
6749                                },
6750                                line,
6751                            };
6752                        }
6753                    } else if postfix_lbracket_is_arrow_container(&expr) {
6754                        self.advance();
6755                        let indices = self.parse_arg_list()?;
6756                        self.expect(&Token::RBracket)?;
6757                        expr = Expr {
6758                            kind: ExprKind::ArrowDeref {
6759                                expr: Box::new(expr),
6760                                index: Box::new(Expr {
6761                                    kind: ExprKind::List(indices),
6762                                    line,
6763                                }),
6764                                kind: DerefKind::Array,
6765                            },
6766                            line,
6767                        };
6768                    } else {
6769                        self.advance();
6770                        let indices = self.parse_arg_list()?;
6771                        self.expect(&Token::RBracket)?;
6772                        expr = Expr {
6773                            kind: ExprKind::AnonymousListSlice {
6774                                source: Box::new(expr),
6775                                indices,
6776                            },
6777                            line,
6778                        };
6779                    }
6780                }
6781                Token::LBrace => {
6782                    if self.suppress_scalar_hash_brace > 0 {
6783                        break;
6784                    }
6785                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
6786                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
6787                    let line = expr.line;
6788                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
6789                    let is_chainable_hash_subscript = is_scalar_named_hash
6790                        || matches!(
6791                            expr.kind,
6792                            ExprKind::HashElement { .. }
6793                                | ExprKind::ArrayElement { .. }
6794                                | ExprKind::ArrowDeref { .. }
6795                                | ExprKind::Deref {
6796                                    kind: Sigil::Scalar,
6797                                    ..
6798                                }
6799                        );
6800                    if !is_chainable_hash_subscript {
6801                        break;
6802                    }
6803                    self.advance();
6804                    let key = self.parse_hash_subscript_key()?;
6805                    self.expect(&Token::RBrace)?;
6806                    expr = if is_scalar_named_hash {
6807                        if let ExprKind::ScalarVar(ref name) = expr.kind {
6808                            let name = name.clone();
6809                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
6810                            if name == "_" {
6811                                Expr {
6812                                    kind: ExprKind::ArrowDeref {
6813                                        expr: Box::new(Expr {
6814                                            kind: ExprKind::ScalarVar("_".into()),
6815                                            line,
6816                                        }),
6817                                        index: Box::new(key),
6818                                        kind: DerefKind::Hash,
6819                                    },
6820                                    line,
6821                                }
6822                            } else {
6823                                Expr {
6824                                    kind: ExprKind::HashElement {
6825                                        hash: name,
6826                                        key: Box::new(key),
6827                                    },
6828                                    line,
6829                                }
6830                            }
6831                        } else {
6832                            unreachable!("is_scalar_named_hash implies ScalarVar");
6833                        }
6834                    } else {
6835                        Expr {
6836                            kind: ExprKind::ArrowDeref {
6837                                expr: Box::new(expr),
6838                                index: Box::new(key),
6839                                kind: DerefKind::Hash,
6840                            },
6841                            line,
6842                        }
6843                    };
6844                }
6845                _ => break,
6846            }
6847        }
6848        Ok(expr)
6849    }
6850
6851    fn parse_primary(&mut self) -> PerlResult<Expr> {
6852        let line = self.peek_line();
6853        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
6854        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
6855        // assigned value(s); has the side effect of declaring the variable in
6856        // the current scope.  See `ExprKind::MyExpr`.
6857        if let Token::Ident(ref kw) = self.peek().clone() {
6858            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
6859                let kw_owned = kw.clone();
6860                // Parse exactly like the statement form via `parse_my_our_local`,
6861                // then unwrap the resulting `StmtKind::*` back into a list of
6862                // `VarDecl`s for the expression node.  This re-uses the full
6863                // syntax (typed sigs, list destructuring, type annotations).
6864                let saved_pos = self.pos;
6865                let stmt = self.parse_my_our_local(&kw_owned, false)?;
6866                let decls = match stmt.kind {
6867                    StmtKind::My(d)
6868                    | StmtKind::Our(d)
6869                    | StmtKind::State(d)
6870                    | StmtKind::Local(d) => d,
6871                    _ => {
6872                        // `local *FOO = …` / non-decl forms — fall back to the
6873                        // statement parser (already advanced); restore position
6874                        // and let the surrounding code handle it as a statement
6875                        // by erroring loudly here.
6876                        self.pos = saved_pos;
6877                        return Err(self.syntax_err(
6878                            "`my`/`our`/`local` in expression must declare variables",
6879                            line,
6880                        ));
6881                    }
6882                };
6883                return Ok(Expr {
6884                    kind: ExprKind::MyExpr {
6885                        keyword: kw_owned,
6886                        decls,
6887                    },
6888                    line,
6889                });
6890            }
6891        }
6892        match self.peek().clone() {
6893            Token::Integer(n) => {
6894                self.advance();
6895                Ok(Expr {
6896                    kind: ExprKind::Integer(n),
6897                    line,
6898                })
6899            }
6900            Token::Float(f) => {
6901                self.advance();
6902                Ok(Expr {
6903                    kind: ExprKind::Float(f),
6904                    line,
6905                })
6906            }
6907            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
6908            // Valid in any expression position; evaluates the block and yields its last value.
6909            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
6910            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
6911            // instead pipe-applied as a coderef — that path is never reached from here.
6912            Token::ArrowBrace => {
6913                self.advance();
6914                let mut stmts = Vec::new();
6915                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
6916                    if self.eat(&Token::Semicolon) {
6917                        continue;
6918                    }
6919                    stmts.push(self.parse_statement()?);
6920                }
6921                self.expect(&Token::RBrace)?;
6922                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
6923                let inner = Expr {
6924                    kind: ExprKind::CodeRef {
6925                        params: vec![],
6926                        body: stmts,
6927                    },
6928                    line: inner_line,
6929                };
6930                Ok(Expr {
6931                    kind: ExprKind::Do(Box::new(inner)),
6932                    line,
6933                })
6934            }
6935            Token::Star => {
6936                self.advance();
6937                if matches!(self.peek(), Token::LBrace) {
6938                    self.advance();
6939                    let inner = self.parse_expression()?;
6940                    self.expect(&Token::RBrace)?;
6941                    return Ok(Expr {
6942                        kind: ExprKind::Deref {
6943                            expr: Box::new(inner),
6944                            kind: Sigil::Typeglob,
6945                        },
6946                        line,
6947                    });
6948                }
6949                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
6950                if matches!(
6951                    self.peek(),
6952                    Token::ScalarVar(_)
6953                        | Token::ArrayVar(_)
6954                        | Token::HashVar(_)
6955                        | Token::DerefScalarVar(_)
6956                        | Token::HashPercent
6957                ) {
6958                    let inner = self.parse_postfix()?;
6959                    return Ok(Expr {
6960                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
6961                        line,
6962                    });
6963                }
6964                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
6965                let mut full_name = match self.advance() {
6966                    (Token::Ident(n), _) => n,
6967                    (Token::X, _) => "x".to_string(),
6968                    (tok, l) => {
6969                        return Err(self
6970                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
6971                    }
6972                };
6973                while self.eat(&Token::PackageSep) {
6974                    match self.advance() {
6975                        (Token::Ident(part), _) => {
6976                            full_name = format!("{}::{}", full_name, part);
6977                        }
6978                        (Token::X, _) => {
6979                            full_name = format!("{}::x", full_name);
6980                        }
6981                        (tok, l) => {
6982                            return Err(self.syntax_err(
6983                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
6984                                l,
6985                            ));
6986                        }
6987                    }
6988                }
6989                Ok(Expr {
6990                    kind: ExprKind::Typeglob(full_name),
6991                    line,
6992                })
6993            }
6994            Token::SingleString(s) => {
6995                self.advance();
6996                Ok(Expr {
6997                    kind: ExprKind::String(s),
6998                    line,
6999                })
7000            }
7001            Token::DoubleString(s) => {
7002                self.advance();
7003                self.parse_interpolated_string(&s, line)
7004            }
7005            Token::BacktickString(s) => {
7006                self.advance();
7007                let inner = self.parse_interpolated_string(&s, line)?;
7008                Ok(Expr {
7009                    kind: ExprKind::Qx(Box::new(inner)),
7010                    line,
7011                })
7012            }
7013            Token::HereDoc(_, body, interpolate) => {
7014                self.advance();
7015                if interpolate {
7016                    self.parse_interpolated_string(&body, line)
7017                } else {
7018                    Ok(Expr {
7019                        kind: ExprKind::String(body),
7020                        line,
7021                    })
7022                }
7023            }
7024            Token::Regex(pattern, flags, _delim) => {
7025                self.advance();
7026                Ok(Expr {
7027                    kind: ExprKind::Regex(pattern, flags),
7028                    line,
7029                })
7030            }
7031            Token::QW(words) => {
7032                self.advance();
7033                Ok(Expr {
7034                    kind: ExprKind::QW(words),
7035                    line,
7036                })
7037            }
7038            Token::DerefScalarVar(name) => {
7039                self.advance();
7040                Ok(Expr {
7041                    kind: ExprKind::Deref {
7042                        expr: Box::new(Expr {
7043                            kind: ExprKind::ScalarVar(name),
7044                            line,
7045                        }),
7046                        kind: Sigil::Scalar,
7047                    },
7048                    line,
7049                })
7050            }
7051            Token::ScalarVar(name) => {
7052                self.advance();
7053                Ok(Expr {
7054                    kind: ExprKind::ScalarVar(name),
7055                    line,
7056                })
7057            }
7058            Token::ArrayVar(name) => {
7059                self.advance();
7060                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
7061                match self.peek() {
7062                    Token::LBracket => {
7063                        self.advance();
7064                        let indices = self.parse_arg_list()?;
7065                        self.expect(&Token::RBracket)?;
7066                        Ok(Expr {
7067                            kind: ExprKind::ArraySlice {
7068                                array: name,
7069                                indices,
7070                            },
7071                            line,
7072                        })
7073                    }
7074                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
7075                        self.advance();
7076                        let keys = self.parse_arg_list()?;
7077                        self.expect(&Token::RBrace)?;
7078                        Ok(Expr {
7079                            kind: ExprKind::HashSlice { hash: name, keys },
7080                            line,
7081                        })
7082                    }
7083                    _ => Ok(Expr {
7084                        kind: ExprKind::ArrayVar(name),
7085                        line,
7086                    }),
7087                }
7088            }
7089            Token::HashVar(name) => {
7090                self.advance();
7091                Ok(Expr {
7092                    kind: ExprKind::HashVar(name),
7093                    line,
7094                })
7095            }
7096            Token::HashPercent => {
7097                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
7098                self.advance();
7099                if matches!(self.peek(), Token::ScalarVar(_)) {
7100                    let n = match self.advance() {
7101                        (Token::ScalarVar(n), _) => n,
7102                        (tok, l) => {
7103                            return Err(self.syntax_err(
7104                                format!("Expected scalar variable after %%, got {:?}", tok),
7105                                l,
7106                            ));
7107                        }
7108                    };
7109                    return Ok(Expr {
7110                        kind: ExprKind::Deref {
7111                            expr: Box::new(Expr {
7112                                kind: ExprKind::ScalarVar(n),
7113                                line,
7114                            }),
7115                            kind: Sigil::Hash,
7116                        },
7117                        line,
7118                    });
7119                }
7120                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
7121                // anonymous hashref inline, using `[...]` as the delimiter to avoid
7122                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
7123                // Real Perl errors on `%[...]` syntactically, so no compat risk.
7124                if matches!(self.peek(), Token::LBracket) {
7125                    self.advance();
7126                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7127                    self.expect(&Token::RBracket)?;
7128                    let href = Expr {
7129                        kind: ExprKind::HashRef(pairs),
7130                        line,
7131                    };
7132                    return Ok(Expr {
7133                        kind: ExprKind::Deref {
7134                            expr: Box::new(href),
7135                            kind: Sigil::Hash,
7136                        },
7137                        line,
7138                    });
7139                }
7140                self.expect(&Token::LBrace)?;
7141                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
7142                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
7143                // heuristic is famously unreliable — when the first non-whitespace
7144                // token is an ident/string followed by `=>`, treat the whole thing
7145                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
7146                let looks_like_pair = matches!(
7147                    self.peek(),
7148                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7149                ) && matches!(self.peek_at(1), Token::FatArrow);
7150                let inner = if looks_like_pair {
7151                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7152                    Expr {
7153                        kind: ExprKind::HashRef(pairs),
7154                        line,
7155                    }
7156                } else {
7157                    self.parse_expression()?
7158                };
7159                self.expect(&Token::RBrace)?;
7160                Ok(Expr {
7161                    kind: ExprKind::Deref {
7162                        expr: Box::new(inner),
7163                        kind: Sigil::Hash,
7164                    },
7165                    line,
7166                })
7167            }
7168            Token::ArrayAt => {
7169                self.advance();
7170                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
7171                if matches!(self.peek(), Token::LBrace) {
7172                    self.advance();
7173                    let inner = self.parse_expression()?;
7174                    self.expect(&Token::RBrace)?;
7175                    return Ok(Expr {
7176                        kind: ExprKind::Deref {
7177                            expr: Box::new(inner),
7178                            kind: Sigil::Array,
7179                        },
7180                        line,
7181                    });
7182                }
7183                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
7184                // anonymous arrayref inline. Real Perl rejects `@[...]` at
7185                // the parser level, so this extension has no compat risk.
7186                if matches!(self.peek(), Token::LBracket) {
7187                    self.advance();
7188                    let mut elems = Vec::new();
7189                    if !matches!(self.peek(), Token::RBracket) {
7190                        elems.push(self.parse_assign_expr()?);
7191                        while self.eat(&Token::Comma) {
7192                            if matches!(self.peek(), Token::RBracket) {
7193                                break;
7194                            }
7195                            elems.push(self.parse_assign_expr()?);
7196                        }
7197                    }
7198                    self.expect(&Token::RBracket)?;
7199                    let aref = Expr {
7200                        kind: ExprKind::ArrayRef(elems),
7201                        line,
7202                    };
7203                    return Ok(Expr {
7204                        kind: ExprKind::Deref {
7205                            expr: Box::new(aref),
7206                            kind: Sigil::Array,
7207                        },
7208                        line,
7209                    });
7210                }
7211                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
7212                let container = match self.peek().clone() {
7213                    Token::ScalarVar(n) => {
7214                        self.advance();
7215                        Expr {
7216                            kind: ExprKind::ScalarVar(n),
7217                            line,
7218                        }
7219                    }
7220                    _ => {
7221                        return Err(self.syntax_err(
7222                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
7223                            line,
7224                        ));
7225                    }
7226                };
7227                if matches!(self.peek(), Token::LBrace) {
7228                    self.advance();
7229                    let keys = self.parse_arg_list()?;
7230                    self.expect(&Token::RBrace)?;
7231                    return Ok(Expr {
7232                        kind: ExprKind::HashSliceDeref {
7233                            container: Box::new(container),
7234                            keys,
7235                        },
7236                        line,
7237                    });
7238                }
7239                Ok(Expr {
7240                    kind: ExprKind::Deref {
7241                        expr: Box::new(container),
7242                        kind: Sigil::Array,
7243                    },
7244                    line,
7245                })
7246            }
7247            Token::LParen => {
7248                self.advance();
7249                if matches!(self.peek(), Token::RParen) {
7250                    self.advance();
7251                    return Ok(Expr {
7252                        kind: ExprKind::List(vec![]),
7253                        line,
7254                    });
7255                }
7256                let expr = self.parse_expression()?;
7257                self.expect(&Token::RParen)?;
7258                Ok(expr)
7259            }
7260            Token::LBracket => {
7261                self.advance();
7262                let elems = self.parse_arg_list()?;
7263                self.expect(&Token::RBracket)?;
7264                Ok(Expr {
7265                    kind: ExprKind::ArrayRef(elems),
7266                    line,
7267                })
7268            }
7269            Token::LBrace => {
7270                // Could be hash ref or block — disambiguate
7271                self.advance();
7272                // Try to parse as hash ref: { key => val, ... }
7273                let saved = self.pos;
7274                match self.try_parse_hash_ref() {
7275                    Ok(pairs) => Ok(Expr {
7276                        kind: ExprKind::HashRef(pairs),
7277                        line,
7278                    }),
7279                    Err(_) => {
7280                        self.pos = saved;
7281                        // Parse as block, wrap in code ref
7282                        let mut stmts = Vec::new();
7283                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7284                            if self.eat(&Token::Semicolon) {
7285                                continue;
7286                            }
7287                            stmts.push(self.parse_statement()?);
7288                        }
7289                        self.expect(&Token::RBrace)?;
7290                        Ok(Expr {
7291                            kind: ExprKind::CodeRef {
7292                                params: vec![],
7293                                body: stmts,
7294                            },
7295                            line,
7296                        })
7297                    }
7298                }
7299            }
7300            Token::Diamond => {
7301                self.advance();
7302                Ok(Expr {
7303                    kind: ExprKind::ReadLine(None),
7304                    line,
7305                })
7306            }
7307            Token::ReadLine(handle) => {
7308                self.advance();
7309                Ok(Expr {
7310                    kind: ExprKind::ReadLine(Some(handle)),
7311                    line,
7312                })
7313            }
7314
7315            // Named functions / builtins
7316            Token::ThreadArrow => {
7317                self.advance();
7318                self.parse_thread_macro(line)
7319            }
7320            Token::Ident(ref name) => {
7321                let name = name.clone();
7322                // Handle s///
7323                if name.starts_with('\x00') {
7324                    self.advance();
7325                    let parts: Vec<&str> = name.split('\x00').collect();
7326                    if parts.len() >= 4 && parts[1] == "s" {
7327                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7328                        return Ok(Expr {
7329                            kind: ExprKind::Substitution {
7330                                expr: Box::new(Expr {
7331                                    kind: ExprKind::ScalarVar("_".into()),
7332                                    line,
7333                                }),
7334                                pattern: parts[2].to_string(),
7335                                replacement: parts[3].to_string(),
7336                                flags: parts.get(4).unwrap_or(&"").to_string(),
7337                                delim,
7338                            },
7339                            line,
7340                        });
7341                    }
7342                    if parts.len() >= 4 && parts[1] == "tr" {
7343                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7344                        return Ok(Expr {
7345                            kind: ExprKind::Transliterate {
7346                                expr: Box::new(Expr {
7347                                    kind: ExprKind::ScalarVar("_".into()),
7348                                    line,
7349                                }),
7350                                from: parts[2].to_string(),
7351                                to: parts[3].to_string(),
7352                                flags: parts.get(4).unwrap_or(&"").to_string(),
7353                                delim,
7354                            },
7355                            line,
7356                        });
7357                    }
7358                    return Err(self.syntax_err("Unexpected encoded token", line));
7359                }
7360                self.parse_named_expr(name)
7361            }
7362
7363            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
7364            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
7365            Token::Percent => {
7366                self.advance();
7367                match self.peek().clone() {
7368                    Token::Ident(name) => {
7369                        self.advance();
7370                        Ok(Expr {
7371                            kind: ExprKind::HashVar(name),
7372                            line,
7373                        })
7374                    }
7375                    Token::ScalarVar(n) => {
7376                        self.advance();
7377                        Ok(Expr {
7378                            kind: ExprKind::Deref {
7379                                expr: Box::new(Expr {
7380                                    kind: ExprKind::ScalarVar(n),
7381                                    line,
7382                                }),
7383                                kind: Sigil::Hash,
7384                            },
7385                            line,
7386                        })
7387                    }
7388                    Token::LBrace => {
7389                        self.advance();
7390                        let looks_like_pair = matches!(
7391                            self.peek(),
7392                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7393                        ) && matches!(self.peek_at(1), Token::FatArrow);
7394                        let inner = if looks_like_pair {
7395                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7396                            Expr {
7397                                kind: ExprKind::HashRef(pairs),
7398                                line,
7399                            }
7400                        } else {
7401                            self.parse_expression()?
7402                        };
7403                        self.expect(&Token::RBrace)?;
7404                        Ok(Expr {
7405                            kind: ExprKind::Deref {
7406                                expr: Box::new(inner),
7407                                kind: Sigil::Hash,
7408                            },
7409                            line,
7410                        })
7411                    }
7412                    Token::LBracket => {
7413                        self.advance();
7414                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7415                        self.expect(&Token::RBracket)?;
7416                        let href = Expr {
7417                            kind: ExprKind::HashRef(pairs),
7418                            line,
7419                        };
7420                        Ok(Expr {
7421                            kind: ExprKind::Deref {
7422                                expr: Box::new(href),
7423                                kind: Sigil::Hash,
7424                            },
7425                            line,
7426                        })
7427                    }
7428                    tok => Err(self.syntax_err(
7429                        format!(
7430                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
7431                            tok
7432                        ),
7433                        line,
7434                    )),
7435                }
7436            }
7437
7438            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
7439        }
7440    }
7441
7442    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
7443        let line = self.peek_line();
7444        self.advance(); // consume the ident
7445        while self.eat(&Token::PackageSep) {
7446            match self.advance() {
7447                (Token::Ident(part), _) => {
7448                    name = format!("{}::{}", name, part);
7449                }
7450                (tok, err_line) => {
7451                    return Err(self.syntax_err(
7452                        format!("Expected identifier after `::`, got {:?}", tok),
7453                        err_line,
7454                    ));
7455                }
7456            }
7457        }
7458
7459        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
7460        // before `=>` is treated as a string key, matching Perl 5 semantics.
7461        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
7462        if matches!(self.peek(), Token::FatArrow) {
7463            return Ok(Expr {
7464                kind: ExprKind::String(name),
7465                line,
7466            });
7467        }
7468
7469        if crate::compat_mode() {
7470            if let Some(ext) = Self::stryke_extension_name(&name) {
7471                if !self.declared_subs.contains(&name) {
7472                    return Err(self.syntax_err(
7473                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
7474                        line,
7475                    ));
7476                }
7477            }
7478        }
7479
7480        match name.as_str() {
7481            "__FILE__" => Ok(Expr {
7482                kind: ExprKind::MagicConst(MagicConstKind::File),
7483                line,
7484            }),
7485            "__LINE__" => Ok(Expr {
7486                kind: ExprKind::MagicConst(MagicConstKind::Line),
7487                line,
7488            }),
7489            "__SUB__" => Ok(Expr {
7490                kind: ExprKind::MagicConst(MagicConstKind::Sub),
7491                line,
7492            }),
7493            "stdin" => Ok(Expr {
7494                kind: ExprKind::FuncCall {
7495                    name: "stdin".into(),
7496                    args: vec![],
7497                },
7498                line,
7499            }),
7500            "range" => {
7501                let args = self.parse_builtin_args()?;
7502                Ok(Expr {
7503                    kind: ExprKind::FuncCall {
7504                        name: "range".into(),
7505                        args,
7506                    },
7507                    line,
7508                })
7509            }
7510            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
7511            "say" | "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
7512            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
7513            "die" => {
7514                let args = self.parse_list_until_terminator()?;
7515                Ok(Expr {
7516                    kind: ExprKind::Die(args),
7517                    line,
7518                })
7519            }
7520            "warn" => {
7521                let args = self.parse_list_until_terminator()?;
7522                Ok(Expr {
7523                    kind: ExprKind::Warn(args),
7524                    line,
7525                })
7526            }
7527            // `croak` / `confess` — `Carp` builtins available without `use Carp`
7528            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
7529            // `die` — TODO: croak should report caller's file/line, confess
7530            // should append a full stack trace.
7531            "croak" | "confess" => {
7532                let args = self.parse_list_until_terminator()?;
7533                Ok(Expr {
7534                    kind: ExprKind::Die(args),
7535                    line,
7536                })
7537            }
7538            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
7539            "carp" | "cluck" => {
7540                let args = self.parse_list_until_terminator()?;
7541                Ok(Expr {
7542                    kind: ExprKind::Warn(args),
7543                    line,
7544                })
7545            }
7546            "chomp" => {
7547                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7548                    return Ok(e);
7549                }
7550                let a = self.parse_one_arg_or_default()?;
7551                Ok(Expr {
7552                    kind: ExprKind::Chomp(Box::new(a)),
7553                    line,
7554                })
7555            }
7556            "chop" => {
7557                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7558                    return Ok(e);
7559                }
7560                let a = self.parse_one_arg_or_default()?;
7561                Ok(Expr {
7562                    kind: ExprKind::Chop(Box::new(a)),
7563                    line,
7564                })
7565            }
7566            "length" => {
7567                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7568                    return Ok(e);
7569                }
7570                let a = self.parse_one_arg_or_default()?;
7571                Ok(Expr {
7572                    kind: ExprKind::Length(Box::new(a)),
7573                    line,
7574                })
7575            }
7576            "defined" => {
7577                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7578                    return Ok(e);
7579                }
7580                let a = self.parse_one_arg_or_default()?;
7581                Ok(Expr {
7582                    kind: ExprKind::Defined(Box::new(a)),
7583                    line,
7584                })
7585            }
7586            "ref" => {
7587                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7588                    return Ok(e);
7589                }
7590                let a = self.parse_one_arg_or_default()?;
7591                Ok(Expr {
7592                    kind: ExprKind::Ref(Box::new(a)),
7593                    line,
7594                })
7595            }
7596            "undef" => {
7597                if matches!(
7598                    self.peek(),
7599                    Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
7600                ) {
7601                    let _ = self.advance();
7602                }
7603                Ok(Expr {
7604                    kind: ExprKind::Undef,
7605                    line,
7606                })
7607            }
7608            "scalar" => {
7609                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7610                    return Ok(e);
7611                }
7612                let a = self.parse_one_arg_or_default()?;
7613                Ok(Expr {
7614                    kind: ExprKind::ScalarContext(Box::new(a)),
7615                    line,
7616                })
7617            }
7618            "abs" => {
7619                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7620                    return Ok(e);
7621                }
7622                let a = self.parse_one_arg_or_default()?;
7623                Ok(Expr {
7624                    kind: ExprKind::Abs(Box::new(a)),
7625                    line,
7626                })
7627            }
7628            // stryke unary numeric extensions — treat like `abs` so a bare
7629            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
7630            // call with implicit `$_` rather than falling through to the
7631            // generic `Bareword` arm (which stringifies to `"inc"`).
7632            "inc" | "dec" => {
7633                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7634                    return Ok(e);
7635                }
7636                let a = self.parse_one_arg_or_default()?;
7637                Ok(Expr {
7638                    kind: ExprKind::FuncCall {
7639                        name,
7640                        args: vec![a],
7641                    },
7642                    line,
7643                })
7644            }
7645            "int" => {
7646                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7647                    return Ok(e);
7648                }
7649                let a = self.parse_one_arg_or_default()?;
7650                Ok(Expr {
7651                    kind: ExprKind::Int(Box::new(a)),
7652                    line,
7653                })
7654            }
7655            "sqrt" => {
7656                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7657                    return Ok(e);
7658                }
7659                let a = self.parse_one_arg_or_default()?;
7660                Ok(Expr {
7661                    kind: ExprKind::Sqrt(Box::new(a)),
7662                    line,
7663                })
7664            }
7665            "sin" => {
7666                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7667                    return Ok(e);
7668                }
7669                let a = self.parse_one_arg_or_default()?;
7670                Ok(Expr {
7671                    kind: ExprKind::Sin(Box::new(a)),
7672                    line,
7673                })
7674            }
7675            "cos" => {
7676                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7677                    return Ok(e);
7678                }
7679                let a = self.parse_one_arg_or_default()?;
7680                Ok(Expr {
7681                    kind: ExprKind::Cos(Box::new(a)),
7682                    line,
7683                })
7684            }
7685            "atan2" => {
7686                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7687                    return Ok(e);
7688                }
7689                let args = self.parse_builtin_args()?;
7690                if args.len() != 2 {
7691                    return Err(self.syntax_err("atan2 requires two arguments", line));
7692                }
7693                Ok(Expr {
7694                    kind: ExprKind::Atan2 {
7695                        y: Box::new(args[0].clone()),
7696                        x: Box::new(args[1].clone()),
7697                    },
7698                    line,
7699                })
7700            }
7701            "exp" => {
7702                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7703                    return Ok(e);
7704                }
7705                let a = self.parse_one_arg_or_default()?;
7706                Ok(Expr {
7707                    kind: ExprKind::Exp(Box::new(a)),
7708                    line,
7709                })
7710            }
7711            "log" => {
7712                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7713                    return Ok(e);
7714                }
7715                let a = self.parse_one_arg_or_default()?;
7716                Ok(Expr {
7717                    kind: ExprKind::Log(Box::new(a)),
7718                    line,
7719                })
7720            }
7721            "input" => {
7722                let args = if matches!(
7723                    self.peek(),
7724                    Token::Semicolon
7725                        | Token::RBrace
7726                        | Token::RParen
7727                        | Token::Eof
7728                        | Token::Comma
7729                        | Token::PipeForward
7730                ) {
7731                    vec![]
7732                } else if matches!(self.peek(), Token::LParen) {
7733                    self.advance();
7734                    if matches!(self.peek(), Token::RParen) {
7735                        self.advance();
7736                        vec![]
7737                    } else {
7738                        let a = self.parse_expression()?;
7739                        self.expect(&Token::RParen)?;
7740                        vec![a]
7741                    }
7742                } else {
7743                    let a = self.parse_one_arg()?;
7744                    vec![a]
7745                };
7746                Ok(Expr {
7747                    kind: ExprKind::FuncCall {
7748                        name: "input".to_string(),
7749                        args,
7750                    },
7751                    line,
7752                })
7753            }
7754            "rand" => {
7755                if matches!(
7756                    self.peek(),
7757                    Token::Semicolon
7758                        | Token::RBrace
7759                        | Token::RParen
7760                        | Token::Eof
7761                        | Token::Comma
7762                        | Token::PipeForward
7763                ) {
7764                    Ok(Expr {
7765                        kind: ExprKind::Rand(None),
7766                        line,
7767                    })
7768                } else if matches!(self.peek(), Token::LParen) {
7769                    self.advance();
7770                    if matches!(self.peek(), Token::RParen) {
7771                        self.advance();
7772                        Ok(Expr {
7773                            kind: ExprKind::Rand(None),
7774                            line,
7775                        })
7776                    } else {
7777                        let a = self.parse_expression()?;
7778                        self.expect(&Token::RParen)?;
7779                        Ok(Expr {
7780                            kind: ExprKind::Rand(Some(Box::new(a))),
7781                            line,
7782                        })
7783                    }
7784                } else {
7785                    let a = self.parse_one_arg()?;
7786                    Ok(Expr {
7787                        kind: ExprKind::Rand(Some(Box::new(a))),
7788                        line,
7789                    })
7790                }
7791            }
7792            "srand" => {
7793                if matches!(
7794                    self.peek(),
7795                    Token::Semicolon
7796                        | Token::RBrace
7797                        | Token::RParen
7798                        | Token::Eof
7799                        | Token::Comma
7800                        | Token::PipeForward
7801                ) {
7802                    Ok(Expr {
7803                        kind: ExprKind::Srand(None),
7804                        line,
7805                    })
7806                } else if matches!(self.peek(), Token::LParen) {
7807                    self.advance();
7808                    if matches!(self.peek(), Token::RParen) {
7809                        self.advance();
7810                        Ok(Expr {
7811                            kind: ExprKind::Srand(None),
7812                            line,
7813                        })
7814                    } else {
7815                        let a = self.parse_expression()?;
7816                        self.expect(&Token::RParen)?;
7817                        Ok(Expr {
7818                            kind: ExprKind::Srand(Some(Box::new(a))),
7819                            line,
7820                        })
7821                    }
7822                } else {
7823                    let a = self.parse_one_arg()?;
7824                    Ok(Expr {
7825                        kind: ExprKind::Srand(Some(Box::new(a))),
7826                        line,
7827                    })
7828                }
7829            }
7830            "hex" => {
7831                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7832                    return Ok(e);
7833                }
7834                let a = self.parse_one_arg_or_default()?;
7835                Ok(Expr {
7836                    kind: ExprKind::Hex(Box::new(a)),
7837                    line,
7838                })
7839            }
7840            "oct" => {
7841                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7842                    return Ok(e);
7843                }
7844                let a = self.parse_one_arg_or_default()?;
7845                Ok(Expr {
7846                    kind: ExprKind::Oct(Box::new(a)),
7847                    line,
7848                })
7849            }
7850            "chr" => {
7851                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7852                    return Ok(e);
7853                }
7854                let a = self.parse_one_arg_or_default()?;
7855                Ok(Expr {
7856                    kind: ExprKind::Chr(Box::new(a)),
7857                    line,
7858                })
7859            }
7860            "ord" => {
7861                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7862                    return Ok(e);
7863                }
7864                let a = self.parse_one_arg_or_default()?;
7865                Ok(Expr {
7866                    kind: ExprKind::Ord(Box::new(a)),
7867                    line,
7868                })
7869            }
7870            "lc" => {
7871                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7872                    return Ok(e);
7873                }
7874                let a = self.parse_one_arg_or_default()?;
7875                Ok(Expr {
7876                    kind: ExprKind::Lc(Box::new(a)),
7877                    line,
7878                })
7879            }
7880            "uc" => {
7881                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7882                    return Ok(e);
7883                }
7884                let a = self.parse_one_arg_or_default()?;
7885                Ok(Expr {
7886                    kind: ExprKind::Uc(Box::new(a)),
7887                    line,
7888                })
7889            }
7890            "lcfirst" => {
7891                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7892                    return Ok(e);
7893                }
7894                let a = self.parse_one_arg_or_default()?;
7895                Ok(Expr {
7896                    kind: ExprKind::Lcfirst(Box::new(a)),
7897                    line,
7898                })
7899            }
7900            "ucfirst" => {
7901                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7902                    return Ok(e);
7903                }
7904                let a = self.parse_one_arg_or_default()?;
7905                Ok(Expr {
7906                    kind: ExprKind::Ucfirst(Box::new(a)),
7907                    line,
7908                })
7909            }
7910            "fc" => {
7911                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7912                    return Ok(e);
7913                }
7914                let a = self.parse_one_arg_or_default()?;
7915                Ok(Expr {
7916                    kind: ExprKind::Fc(Box::new(a)),
7917                    line,
7918                })
7919            }
7920            "crypt" => {
7921                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7922                    return Ok(e);
7923                }
7924                let args = self.parse_builtin_args()?;
7925                if args.len() != 2 {
7926                    return Err(self.syntax_err("crypt requires two arguments", line));
7927                }
7928                Ok(Expr {
7929                    kind: ExprKind::Crypt {
7930                        plaintext: Box::new(args[0].clone()),
7931                        salt: Box::new(args[1].clone()),
7932                    },
7933                    line,
7934                })
7935            }
7936            "pos" => {
7937                if matches!(
7938                    self.peek(),
7939                    Token::Semicolon
7940                        | Token::RBrace
7941                        | Token::RParen
7942                        | Token::Eof
7943                        | Token::Comma
7944                        | Token::PipeForward
7945                ) {
7946                    Ok(Expr {
7947                        kind: ExprKind::Pos(None),
7948                        line,
7949                    })
7950                } else if matches!(self.peek(), Token::Assign) {
7951                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
7952                    self.advance();
7953                    let rhs = self.parse_assign_expr()?;
7954                    Ok(Expr {
7955                        kind: ExprKind::Assign {
7956                            target: Box::new(Expr {
7957                                kind: ExprKind::Pos(Some(Box::new(Expr {
7958                                    kind: ExprKind::ScalarVar("_".into()),
7959                                    line,
7960                                }))),
7961                                line,
7962                            }),
7963                            value: Box::new(rhs),
7964                        },
7965                        line,
7966                    })
7967                } else if matches!(self.peek(), Token::LParen) {
7968                    self.advance();
7969                    if matches!(self.peek(), Token::RParen) {
7970                        self.advance();
7971                        Ok(Expr {
7972                            kind: ExprKind::Pos(None),
7973                            line,
7974                        })
7975                    } else {
7976                        let a = self.parse_expression()?;
7977                        self.expect(&Token::RParen)?;
7978                        Ok(Expr {
7979                            kind: ExprKind::Pos(Some(Box::new(a))),
7980                            line,
7981                        })
7982                    }
7983                } else {
7984                    let saved = self.pos;
7985                    let subj = self.parse_unary()?;
7986                    if matches!(self.peek(), Token::Assign) {
7987                        self.advance();
7988                        let rhs = self.parse_assign_expr()?;
7989                        Ok(Expr {
7990                            kind: ExprKind::Assign {
7991                                target: Box::new(Expr {
7992                                    kind: ExprKind::Pos(Some(Box::new(subj))),
7993                                    line,
7994                                }),
7995                                value: Box::new(rhs),
7996                            },
7997                            line,
7998                        })
7999                    } else {
8000                        self.pos = saved;
8001                        let a = self.parse_one_arg()?;
8002                        Ok(Expr {
8003                            kind: ExprKind::Pos(Some(Box::new(a))),
8004                            line,
8005                        })
8006                    }
8007                }
8008            }
8009            "study" => {
8010                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8011                    return Ok(e);
8012                }
8013                let a = self.parse_one_arg_or_default()?;
8014                Ok(Expr {
8015                    kind: ExprKind::Study(Box::new(a)),
8016                    line,
8017                })
8018            }
8019            "push" => {
8020                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8021                    return Ok(e);
8022                }
8023                let args = self.parse_builtin_args()?;
8024                let (first, rest) = args
8025                    .split_first()
8026                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
8027                Ok(Expr {
8028                    kind: ExprKind::Push {
8029                        array: Box::new(first.clone()),
8030                        values: rest.to_vec(),
8031                    },
8032                    line,
8033                })
8034            }
8035            "pop" => {
8036                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8037                    return Ok(e);
8038                }
8039                let a = self.parse_one_arg_or_argv()?;
8040                Ok(Expr {
8041                    kind: ExprKind::Pop(Box::new(a)),
8042                    line,
8043                })
8044            }
8045            "shift" => {
8046                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8047                    return Ok(e);
8048                }
8049                let a = self.parse_one_arg_or_argv()?;
8050                Ok(Expr {
8051                    kind: ExprKind::Shift(Box::new(a)),
8052                    line,
8053                })
8054            }
8055            "unshift" => {
8056                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8057                    return Ok(e);
8058                }
8059                let args = self.parse_builtin_args()?;
8060                let (first, rest) = args
8061                    .split_first()
8062                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
8063                Ok(Expr {
8064                    kind: ExprKind::Unshift {
8065                        array: Box::new(first.clone()),
8066                        values: rest.to_vec(),
8067                    },
8068                    line,
8069                })
8070            }
8071            "splice" => {
8072                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8073                    return Ok(e);
8074                }
8075                let args = self.parse_builtin_args()?;
8076                let mut iter = args.into_iter();
8077                let array = Box::new(
8078                    iter.next()
8079                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
8080                );
8081                let offset = iter.next().map(Box::new);
8082                let length = iter.next().map(Box::new);
8083                let replacement: Vec<Expr> = iter.collect();
8084                Ok(Expr {
8085                    kind: ExprKind::Splice {
8086                        array,
8087                        offset,
8088                        length,
8089                        replacement,
8090                    },
8091                    line,
8092                })
8093            }
8094            "delete" => {
8095                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8096                    return Ok(e);
8097                }
8098                let a = self.parse_postfix()?;
8099                Ok(Expr {
8100                    kind: ExprKind::Delete(Box::new(a)),
8101                    line,
8102                })
8103            }
8104            "exists" => {
8105                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8106                    return Ok(e);
8107                }
8108                let a = self.parse_postfix()?;
8109                Ok(Expr {
8110                    kind: ExprKind::Exists(Box::new(a)),
8111                    line,
8112                })
8113            }
8114            "keys" => {
8115                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8116                    return Ok(e);
8117                }
8118                let a = self.parse_one_arg_or_default()?;
8119                Ok(Expr {
8120                    kind: ExprKind::Keys(Box::new(a)),
8121                    line,
8122                })
8123            }
8124            "values" => {
8125                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8126                    return Ok(e);
8127                }
8128                let a = self.parse_one_arg_or_default()?;
8129                Ok(Expr {
8130                    kind: ExprKind::Values(Box::new(a)),
8131                    line,
8132                })
8133            }
8134            "each" => {
8135                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8136                    return Ok(e);
8137                }
8138                let a = self.parse_one_arg_or_default()?;
8139                Ok(Expr {
8140                    kind: ExprKind::Each(Box::new(a)),
8141                    line,
8142                })
8143            }
8144            "fore" | "e" | "ep" => {
8145                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
8146                if matches!(self.peek(), Token::LBrace) {
8147                    let (block, list) = self.parse_block_list()?;
8148                    Ok(Expr {
8149                        kind: ExprKind::ForEachExpr {
8150                            block,
8151                            list: Box::new(list),
8152                        },
8153                        line,
8154                    })
8155                } else if self.in_pipe_rhs() {
8156                    // `|> ep` — bare ep at end of pipe: default to `say $_`
8157                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
8158                    let is_terminal = matches!(
8159                        self.peek(),
8160                        Token::Semicolon
8161                            | Token::RParen
8162                            | Token::Eof
8163                            | Token::PipeForward
8164                            | Token::RBrace
8165                    );
8166                    let block = if name == "ep" && is_terminal {
8167                        vec![Statement {
8168                            label: None,
8169                            kind: StmtKind::Expression(Expr {
8170                                kind: ExprKind::Say {
8171                                    handle: None,
8172                                    args: vec![Expr {
8173                                        kind: ExprKind::ScalarVar("_".into()),
8174                                        line,
8175                                    }],
8176                                },
8177                                line,
8178                            }),
8179                            line,
8180                        }]
8181                    } else {
8182                        let expr = self.parse_assign_expr_stop_at_pipe()?;
8183                        let expr = Self::lift_bareword_to_topic_call(expr);
8184                        vec![Statement {
8185                            label: None,
8186                            kind: StmtKind::Expression(expr),
8187                            line,
8188                        }]
8189                    };
8190                    let list = self.pipe_placeholder_list(line);
8191                    Ok(Expr {
8192                        kind: ExprKind::ForEachExpr {
8193                            block,
8194                            list: Box::new(list),
8195                        },
8196                        line,
8197                    })
8198                } else {
8199                    // `fore EXPR, LIST` — comma form
8200                    let expr = self.parse_assign_expr()?;
8201                    let expr = Self::lift_bareword_to_topic_call(expr);
8202                    self.expect(&Token::Comma)?;
8203                    let list_parts = self.parse_list_until_terminator()?;
8204                    let list_expr = if list_parts.len() == 1 {
8205                        list_parts.into_iter().next().unwrap()
8206                    } else {
8207                        Expr {
8208                            kind: ExprKind::List(list_parts),
8209                            line,
8210                        }
8211                    };
8212                    let block = vec![Statement {
8213                        label: None,
8214                        kind: StmtKind::Expression(expr),
8215                        line,
8216                    }];
8217                    Ok(Expr {
8218                        kind: ExprKind::ForEachExpr {
8219                            block,
8220                            list: Box::new(list_expr),
8221                        },
8222                        line,
8223                    })
8224                }
8225            }
8226            "rev" => {
8227                // `rev` — context-aware reverse: string in scalar, list in list context.
8228                // Defaults to $_ when no argument given.
8229                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
8230                // RBrace means we're inside a block like `map { rev }` - use $_ default.
8231                let a = if self.in_pipe_rhs()
8232                    && matches!(
8233                        self.peek(),
8234                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
8235                    ) {
8236                    self.pipe_placeholder_list(line)
8237                } else {
8238                    self.parse_one_arg_or_default()?
8239                };
8240                Ok(Expr {
8241                    kind: ExprKind::ScalarReverse(Box::new(a)),
8242                    line,
8243                })
8244            }
8245            "reverse" | "reversed" => {
8246                // On the RHS of `|>`, the operand is supplied by the piped LHS.
8247                let a = if self.in_pipe_rhs()
8248                    && matches!(
8249                        self.peek(),
8250                        Token::Semicolon
8251                            | Token::RBrace
8252                            | Token::RParen
8253                            | Token::Eof
8254                            | Token::PipeForward
8255                    ) {
8256                    self.pipe_placeholder_list(line)
8257                } else {
8258                    self.parse_one_arg()?
8259                };
8260                Ok(Expr {
8261                    kind: ExprKind::ReverseExpr(Box::new(a)),
8262                    line,
8263                })
8264            }
8265            "join" => {
8266                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8267                    return Ok(e);
8268                }
8269                let args = self.parse_builtin_args()?;
8270                if args.is_empty() {
8271                    return Err(self.syntax_err("join requires separator and list", line));
8272                }
8273                // `@list |> join(",")` — list slot is filled by the piped LHS.
8274                if args.len() < 2 && !self.in_pipe_rhs() {
8275                    return Err(self.syntax_err("join requires separator and list", line));
8276                }
8277                Ok(Expr {
8278                    kind: ExprKind::JoinExpr {
8279                        separator: Box::new(args[0].clone()),
8280                        list: Box::new(Expr {
8281                            kind: ExprKind::List(args[1..].to_vec()),
8282                            line,
8283                        }),
8284                    },
8285                    line,
8286                })
8287            }
8288            "split" => {
8289                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8290                    return Ok(e);
8291                }
8292                let args = self.parse_builtin_args()?;
8293                let pattern = args.first().cloned().unwrap_or(Expr {
8294                    kind: ExprKind::String(" ".into()),
8295                    line,
8296                });
8297                let string = args.get(1).cloned().unwrap_or(Expr {
8298                    kind: ExprKind::ScalarVar("_".into()),
8299                    line,
8300                });
8301                let limit = args.get(2).cloned().map(Box::new);
8302                Ok(Expr {
8303                    kind: ExprKind::SplitExpr {
8304                        pattern: Box::new(pattern),
8305                        string: Box::new(string),
8306                        limit,
8307                    },
8308                    line,
8309                })
8310            }
8311            "substr" => {
8312                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8313                    return Ok(e);
8314                }
8315                let args = self.parse_builtin_args()?;
8316                Ok(Expr {
8317                    kind: ExprKind::Substr {
8318                        string: Box::new(args[0].clone()),
8319                        offset: Box::new(args[1].clone()),
8320                        length: args.get(2).cloned().map(Box::new),
8321                        replacement: args.get(3).cloned().map(Box::new),
8322                    },
8323                    line,
8324                })
8325            }
8326            "index" => {
8327                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8328                    return Ok(e);
8329                }
8330                let args = self.parse_builtin_args()?;
8331                Ok(Expr {
8332                    kind: ExprKind::Index {
8333                        string: Box::new(args[0].clone()),
8334                        substr: Box::new(args[1].clone()),
8335                        position: args.get(2).cloned().map(Box::new),
8336                    },
8337                    line,
8338                })
8339            }
8340            "rindex" => {
8341                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8342                    return Ok(e);
8343                }
8344                let args = self.parse_builtin_args()?;
8345                Ok(Expr {
8346                    kind: ExprKind::Rindex {
8347                        string: Box::new(args[0].clone()),
8348                        substr: Box::new(args[1].clone()),
8349                        position: args.get(2).cloned().map(Box::new),
8350                    },
8351                    line,
8352                })
8353            }
8354            "sprintf" => {
8355                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8356                    return Ok(e);
8357                }
8358                let args = self.parse_builtin_args()?;
8359                let (first, rest) = args
8360                    .split_first()
8361                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
8362                Ok(Expr {
8363                    kind: ExprKind::Sprintf {
8364                        format: Box::new(first.clone()),
8365                        args: rest.to_vec(),
8366                    },
8367                    line,
8368                })
8369            }
8370            "map" | "flat_map" | "maps" | "flat_maps" => {
8371                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
8372                let stream = matches!(name.as_str(), "maps" | "flat_maps");
8373                if matches!(self.peek(), Token::LBrace) {
8374                    let (block, list) = self.parse_block_list()?;
8375                    Ok(Expr {
8376                        kind: ExprKind::MapExpr {
8377                            block,
8378                            list: Box::new(list),
8379                            flatten_array_refs,
8380                            stream,
8381                        },
8382                        line,
8383                    })
8384                } else {
8385                    let expr = self.parse_assign_expr_stop_at_pipe()?;
8386                    // Lift bareword to FuncCall($_) so `map sha512, @list`
8387                    // calls sha512($_) for each element instead of stringifying.
8388                    let expr = Self::lift_bareword_to_topic_call(expr);
8389                    let list_expr = if self.in_pipe_rhs()
8390                        && matches!(
8391                            self.peek(),
8392                            Token::Semicolon
8393                                | Token::RBrace
8394                                | Token::RParen
8395                                | Token::Eof
8396                                | Token::PipeForward
8397                        ) {
8398                        self.pipe_placeholder_list(line)
8399                    } else {
8400                        self.expect(&Token::Comma)?;
8401                        let list_parts = self.parse_list_until_terminator()?;
8402                        if list_parts.len() == 1 {
8403                            list_parts.into_iter().next().unwrap()
8404                        } else {
8405                            Expr {
8406                                kind: ExprKind::List(list_parts),
8407                                line,
8408                            }
8409                        }
8410                    };
8411                    Ok(Expr {
8412                        kind: ExprKind::MapExprComma {
8413                            expr: Box::new(expr),
8414                            list: Box::new(list_expr),
8415                            flatten_array_refs,
8416                            stream,
8417                        },
8418                        line,
8419                    })
8420                }
8421            }
8422            "match" => {
8423                if crate::compat_mode() {
8424                    return Err(self.syntax_err(
8425                        "algebraic `match` is a stryke extension (disabled by --compat)",
8426                        line,
8427                    ));
8428                }
8429                self.parse_algebraic_match_expr(line)
8430            }
8431            "grep" | "greps" | "filter" | "f" | "find_all" => {
8432                let keyword = match name.as_str() {
8433                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
8434                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
8435                    "filter" | "f" => crate::ast::GrepBuiltinKeyword::Filter,
8436                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
8437                    _ => unreachable!(),
8438                };
8439                if matches!(self.peek(), Token::LBrace) {
8440                    let (block, list) = self.parse_block_list()?;
8441                    Ok(Expr {
8442                        kind: ExprKind::GrepExpr {
8443                            block,
8444                            list: Box::new(list),
8445                            keyword,
8446                        },
8447                        line,
8448                    })
8449                } else {
8450                    let expr = self.parse_assign_expr_stop_at_pipe()?;
8451                    if self.in_pipe_rhs()
8452                        && matches!(
8453                            self.peek(),
8454                            Token::Semicolon
8455                                | Token::RBrace
8456                                | Token::RParen
8457                                | Token::Eof
8458                                | Token::PipeForward
8459                        )
8460                    {
8461                        // Pipe-RHS blockless form: `|> grep EXPR`
8462                        // For literals, desugar to `$_ eq/== EXPR` so
8463                        // `|> filter 't'` keeps only elements equal to 't'.
8464                        // For regexes, desugar to `$_ =~ EXPR`.
8465                        let list = self.pipe_placeholder_list(line);
8466                        let topic = Expr {
8467                            kind: ExprKind::ScalarVar("_".into()),
8468                            line,
8469                        };
8470                        let test = match &expr.kind {
8471                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
8472                                kind: ExprKind::BinOp {
8473                                    op: BinOp::NumEq,
8474                                    left: Box::new(topic),
8475                                    right: Box::new(expr),
8476                                },
8477                                line,
8478                            },
8479                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
8480                                kind: ExprKind::BinOp {
8481                                    op: BinOp::StrEq,
8482                                    left: Box::new(topic),
8483                                    right: Box::new(expr),
8484                                },
8485                                line,
8486                            },
8487                            ExprKind::Regex { .. } => Expr {
8488                                kind: ExprKind::BinOp {
8489                                    op: BinOp::BindMatch,
8490                                    left: Box::new(topic),
8491                                    right: Box::new(expr),
8492                                },
8493                                line,
8494                            },
8495                            _ => {
8496                                // Non-literal (e.g. `defined`): lift bareword to call
8497                                Self::lift_bareword_to_topic_call(expr)
8498                            }
8499                        };
8500                        let block = vec![Statement {
8501                            label: None,
8502                            kind: StmtKind::Expression(test),
8503                            line,
8504                        }];
8505                        Ok(Expr {
8506                            kind: ExprKind::GrepExpr {
8507                                block,
8508                                list: Box::new(list),
8509                                keyword,
8510                            },
8511                            line,
8512                        })
8513                    } else {
8514                        let expr = Self::lift_bareword_to_topic_call(expr);
8515                        self.expect(&Token::Comma)?;
8516                        let list_parts = self.parse_list_until_terminator()?;
8517                        let list_expr = if list_parts.len() == 1 {
8518                            list_parts.into_iter().next().unwrap()
8519                        } else {
8520                            Expr {
8521                                kind: ExprKind::List(list_parts),
8522                                line,
8523                            }
8524                        };
8525                        Ok(Expr {
8526                            kind: ExprKind::GrepExprComma {
8527                                expr: Box::new(expr),
8528                                list: Box::new(list_expr),
8529                                keyword,
8530                            },
8531                            line,
8532                        })
8533                    }
8534                }
8535            }
8536            "sort" => {
8537                use crate::ast::SortComparator;
8538                if matches!(self.peek(), Token::LBrace) {
8539                    let block = self.parse_block()?;
8540                    let _ = self.eat(&Token::Comma);
8541                    let list = if self.in_pipe_rhs()
8542                        && matches!(
8543                            self.peek(),
8544                            Token::Semicolon
8545                                | Token::RBrace
8546                                | Token::RParen
8547                                | Token::Eof
8548                                | Token::PipeForward
8549                        ) {
8550                        self.pipe_placeholder_list(line)
8551                    } else {
8552                        self.parse_expression()?
8553                    };
8554                    Ok(Expr {
8555                        kind: ExprKind::SortExpr {
8556                            cmp: Some(SortComparator::Block(block)),
8557                            list: Box::new(list),
8558                        },
8559                        line,
8560                    })
8561                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
8562                    // Blockless comparator: `sort $a <=> $b, @list`
8563                    let block = self.parse_block_or_bareword_cmp_block()?;
8564                    let _ = self.eat(&Token::Comma);
8565                    let list = if self.in_pipe_rhs()
8566                        && matches!(
8567                            self.peek(),
8568                            Token::Semicolon
8569                                | Token::RBrace
8570                                | Token::RParen
8571                                | Token::Eof
8572                                | Token::PipeForward
8573                        ) {
8574                        self.pipe_placeholder_list(line)
8575                    } else {
8576                        self.parse_expression()?
8577                    };
8578                    Ok(Expr {
8579                        kind: ExprKind::SortExpr {
8580                            cmp: Some(SortComparator::Block(block)),
8581                            list: Box::new(list),
8582                        },
8583                        line,
8584                    })
8585                } else if matches!(self.peek(), Token::ScalarVar(_)) {
8586                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized
8587                    self.suppress_indirect_paren_call =
8588                        self.suppress_indirect_paren_call.saturating_add(1);
8589                    let code = self.parse_assign_expr()?;
8590                    self.suppress_indirect_paren_call =
8591                        self.suppress_indirect_paren_call.saturating_sub(1);
8592                    let list = if matches!(self.peek(), Token::LParen) {
8593                        self.advance();
8594                        let e = self.parse_expression()?;
8595                        self.expect(&Token::RParen)?;
8596                        e
8597                    } else {
8598                        self.parse_expression()?
8599                    };
8600                    Ok(Expr {
8601                        kind: ExprKind::SortExpr {
8602                            cmp: Some(SortComparator::Code(Box::new(code))),
8603                            list: Box::new(list),
8604                        },
8605                        line,
8606                    })
8607                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
8608                {
8609                    // Blockless comparator via bare sub name: `sort my_cmp @list`
8610                    let block = self.parse_block_or_bareword_cmp_block()?;
8611                    let _ = self.eat(&Token::Comma);
8612                    let list = if self.in_pipe_rhs()
8613                        && matches!(
8614                            self.peek(),
8615                            Token::Semicolon
8616                                | Token::RBrace
8617                                | Token::RParen
8618                                | Token::Eof
8619                                | Token::PipeForward
8620                        ) {
8621                        self.pipe_placeholder_list(line)
8622                    } else {
8623                        self.parse_expression()?
8624                    };
8625                    Ok(Expr {
8626                        kind: ExprKind::SortExpr {
8627                            cmp: Some(SortComparator::Block(block)),
8628                            list: Box::new(list),
8629                        },
8630                        line,
8631                    })
8632                } else {
8633                    // Bare `sort` with no comparator and no list: only allowed
8634                    // as the RHS of `|>`, where the list comes from the LHS.
8635                    let list = if self.in_pipe_rhs()
8636                        && matches!(
8637                            self.peek(),
8638                            Token::Semicolon
8639                                | Token::RBrace
8640                                | Token::RParen
8641                                | Token::Eof
8642                                | Token::PipeForward
8643                        ) {
8644                        self.pipe_placeholder_list(line)
8645                    } else {
8646                        self.parse_expression()?
8647                    };
8648                    Ok(Expr {
8649                        kind: ExprKind::SortExpr {
8650                            cmp: None,
8651                            list: Box::new(list),
8652                        },
8653                        line,
8654                    })
8655                }
8656            }
8657            "reduce" | "fold" | "inject" => {
8658                let (block, list) = self.parse_block_list()?;
8659                Ok(Expr {
8660                    kind: ExprKind::ReduceExpr {
8661                        block,
8662                        list: Box::new(list),
8663                    },
8664                    line,
8665                })
8666            }
8667            // Parallel extensions
8668            "pmap" => {
8669                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8670                Ok(Expr {
8671                    kind: ExprKind::PMapExpr {
8672                        block,
8673                        list: Box::new(list),
8674                        progress: progress.map(Box::new),
8675                        flat_outputs: false,
8676                        on_cluster: None,
8677                    },
8678                    line,
8679                })
8680            }
8681            "pmap_on" => {
8682                let (cluster, block, list, progress) =
8683                    self.parse_cluster_block_then_list_optional_progress()?;
8684                Ok(Expr {
8685                    kind: ExprKind::PMapExpr {
8686                        block,
8687                        list: Box::new(list),
8688                        progress: progress.map(Box::new),
8689                        flat_outputs: false,
8690                        on_cluster: Some(Box::new(cluster)),
8691                    },
8692                    line,
8693                })
8694            }
8695            "pflat_map" => {
8696                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8697                Ok(Expr {
8698                    kind: ExprKind::PMapExpr {
8699                        block,
8700                        list: Box::new(list),
8701                        progress: progress.map(Box::new),
8702                        flat_outputs: true,
8703                        on_cluster: None,
8704                    },
8705                    line,
8706                })
8707            }
8708            "pflat_map_on" => {
8709                let (cluster, block, list, progress) =
8710                    self.parse_cluster_block_then_list_optional_progress()?;
8711                Ok(Expr {
8712                    kind: ExprKind::PMapExpr {
8713                        block,
8714                        list: Box::new(list),
8715                        progress: progress.map(Box::new),
8716                        flat_outputs: true,
8717                        on_cluster: Some(Box::new(cluster)),
8718                    },
8719                    line,
8720                })
8721            }
8722            "pmap_chunked" => {
8723                let chunk_size = self.parse_assign_expr()?;
8724                let block = self.parse_block_or_bareword_block()?;
8725                self.eat(&Token::Comma);
8726                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
8727                Ok(Expr {
8728                    kind: ExprKind::PMapChunkedExpr {
8729                        chunk_size: Box::new(chunk_size),
8730                        block,
8731                        list: Box::new(list),
8732                        progress: progress.map(Box::new),
8733                    },
8734                    line,
8735                })
8736            }
8737            "pgrep" => {
8738                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8739                Ok(Expr {
8740                    kind: ExprKind::PGrepExpr {
8741                        block,
8742                        list: Box::new(list),
8743                        progress: progress.map(Box::new),
8744                    },
8745                    line,
8746                })
8747            }
8748            "pfor" => {
8749                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8750                Ok(Expr {
8751                    kind: ExprKind::PForExpr {
8752                        block,
8753                        list: Box::new(list),
8754                        progress: progress.map(Box::new),
8755                    },
8756                    line,
8757                })
8758            }
8759            "par_lines" | "par_walk" => {
8760                let args = self.parse_builtin_args()?;
8761                if args.len() < 2 {
8762                    return Err(
8763                        self.syntax_err(format!("{} requires at least two arguments", name), line)
8764                    );
8765                }
8766
8767                if name == "par_lines" {
8768                    Ok(Expr {
8769                        kind: ExprKind::ParLinesExpr {
8770                            path: Box::new(args[0].clone()),
8771                            callback: Box::new(args[1].clone()),
8772                            progress: None,
8773                        },
8774                        line,
8775                    })
8776                } else {
8777                    Ok(Expr {
8778                        kind: ExprKind::ParWalkExpr {
8779                            path: Box::new(args[0].clone()),
8780                            callback: Box::new(args[1].clone()),
8781                            progress: None,
8782                        },
8783                        line,
8784                    })
8785                }
8786            }
8787            "pwatch" | "watch" => {
8788                let args = self.parse_builtin_args()?;
8789                if args.len() < 2 {
8790                    return Err(
8791                        self.syntax_err(format!("{} requires at least two arguments", name), line)
8792                    );
8793                }
8794                Ok(Expr {
8795                    kind: ExprKind::PwatchExpr {
8796                        path: Box::new(args[0].clone()),
8797                        callback: Box::new(args[1].clone()),
8798                    },
8799                    line,
8800                })
8801            }
8802            "fan" => {
8803                // fan { BLOCK }            — no count, block body
8804                // fan COUNT { BLOCK }      — count + block body
8805                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
8806                // fan COUNT EXPR;          — count + blockless body
8807                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
8808                let (count, block) = self.parse_fan_count_and_block(line)?;
8809                let progress = self.parse_fan_optional_progress("fan")?;
8810                Ok(Expr {
8811                    kind: ExprKind::FanExpr {
8812                        count,
8813                        block,
8814                        progress,
8815                        capture: false,
8816                    },
8817                    line,
8818                })
8819            }
8820            "fan_cap" => {
8821                let (count, block) = self.parse_fan_count_and_block(line)?;
8822                let progress = self.parse_fan_optional_progress("fan_cap")?;
8823                Ok(Expr {
8824                    kind: ExprKind::FanExpr {
8825                        count,
8826                        block,
8827                        progress,
8828                        capture: true,
8829                    },
8830                    line,
8831                })
8832            }
8833            "async" => {
8834                if !matches!(self.peek(), Token::LBrace) {
8835                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
8836                }
8837                let block = self.parse_block()?;
8838                Ok(Expr {
8839                    kind: ExprKind::AsyncBlock { body: block },
8840                    line,
8841                })
8842            }
8843            "spawn" => {
8844                if !matches!(self.peek(), Token::LBrace) {
8845                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
8846                }
8847                let block = self.parse_block()?;
8848                Ok(Expr {
8849                    kind: ExprKind::SpawnBlock { body: block },
8850                    line,
8851                })
8852            }
8853            "trace" => {
8854                if !matches!(self.peek(), Token::LBrace) {
8855                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
8856                }
8857                let block = self.parse_block()?;
8858                Ok(Expr {
8859                    kind: ExprKind::Trace { body: block },
8860                    line,
8861                })
8862            }
8863            "timer" => {
8864                let block = self.parse_block_or_bareword_block_no_args()?;
8865                Ok(Expr {
8866                    kind: ExprKind::Timer { body: block },
8867                    line,
8868                })
8869            }
8870            "bench" => {
8871                let block = self.parse_block_or_bareword_block_no_args()?;
8872                let times = Box::new(self.parse_expression()?);
8873                Ok(Expr {
8874                    kind: ExprKind::Bench { body: block, times },
8875                    line,
8876                })
8877            }
8878            "spinner" => {
8879                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
8880                let (message, body) = if matches!(self.peek(), Token::LBrace) {
8881                    let body = self.parse_block()?;
8882                    (
8883                        Box::new(Expr {
8884                            kind: ExprKind::String("working".to_string()),
8885                            line,
8886                        }),
8887                        body,
8888                    )
8889                } else {
8890                    let msg = self.parse_assign_expr()?;
8891                    let body = self.parse_block()?;
8892                    (Box::new(msg), body)
8893                };
8894                Ok(Expr {
8895                    kind: ExprKind::Spinner { message, body },
8896                    line,
8897                })
8898            }
8899            "thread" | "t" => {
8900                // `thread EXPR stage1 stage2 ...` — threading macro (like Clojure's ->>)
8901                // `t` is a short alias for `thread`
8902                // Each stage is either:
8903                //   - `ident` — bare function call
8904                //   - `ident { block }` — function with block arg
8905                //   - `ident arg1 arg2 { block }` — function with args and optional block
8906                //   - `sub { block }` — standalone anonymous block
8907                //   - `>{ block }` — shorthand for standalone anonymous block
8908                // Desugars to: EXPR |> stage1 |> stage2 |> ...
8909                self.parse_thread_macro(line)
8910            }
8911            "retry" => {
8912                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
8913                // An optional comma before `times` is allowed in both forms.
8914                let body = if matches!(self.peek(), Token::LBrace) {
8915                    self.parse_block()?
8916                } else {
8917                    let bw_line = self.peek_line();
8918                    let Token::Ident(ref name) = self.peek().clone() else {
8919                        return Err(self
8920                            .syntax_err("retry: expected block or bareword function name", line));
8921                    };
8922                    let name = name.clone();
8923                    self.advance();
8924                    vec![Statement::new(
8925                        StmtKind::Expression(Expr {
8926                            kind: ExprKind::FuncCall { name, args: vec![] },
8927                            line: bw_line,
8928                        }),
8929                        bw_line,
8930                    )]
8931                };
8932                self.eat(&Token::Comma);
8933                match self.peek() {
8934                    Token::Ident(ref s) if s == "times" => {
8935                        self.advance();
8936                    }
8937                    _ => {
8938                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
8939                    }
8940                }
8941                self.expect(&Token::FatArrow)?;
8942                let times = Box::new(self.parse_assign_expr()?);
8943                let mut backoff = RetryBackoff::None;
8944                if self.eat(&Token::Comma) {
8945                    match self.peek() {
8946                        Token::Ident(ref s) if s == "backoff" => {
8947                            self.advance();
8948                        }
8949                        _ => {
8950                            return Err(
8951                                self.syntax_err("retry: expected `backoff =>` after comma", line)
8952                            );
8953                        }
8954                    }
8955                    self.expect(&Token::FatArrow)?;
8956                    let Token::Ident(mode) = self.peek().clone() else {
8957                        return Err(self.syntax_err(
8958                            "retry: expected backoff mode (none, linear, exponential)",
8959                            line,
8960                        ));
8961                    };
8962                    backoff = match mode.as_str() {
8963                        "none" => RetryBackoff::None,
8964                        "linear" => RetryBackoff::Linear,
8965                        "exponential" => RetryBackoff::Exponential,
8966                        _ => {
8967                            return Err(
8968                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
8969                            );
8970                        }
8971                    };
8972                    self.advance();
8973                }
8974                Ok(Expr {
8975                    kind: ExprKind::RetryBlock {
8976                        body,
8977                        times,
8978                        backoff,
8979                    },
8980                    line,
8981                })
8982            }
8983            "rate_limit" => {
8984                self.expect(&Token::LParen)?;
8985                let max = Box::new(self.parse_assign_expr()?);
8986                self.expect(&Token::Comma)?;
8987                let window = Box::new(self.parse_assign_expr()?);
8988                self.expect(&Token::RParen)?;
8989                let body = self.parse_block_or_bareword_block_no_args()?;
8990                let slot = self.alloc_rate_limit_slot();
8991                Ok(Expr {
8992                    kind: ExprKind::RateLimitBlock {
8993                        slot,
8994                        max,
8995                        window,
8996                        body,
8997                    },
8998                    line,
8999                })
9000            }
9001            "every" => {
9002                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
9003                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
9004                let has_paren = self.eat(&Token::LParen);
9005                let interval = Box::new(self.parse_assign_expr()?);
9006                if has_paren {
9007                    self.expect(&Token::RParen)?;
9008                }
9009                let body = if matches!(self.peek(), Token::LBrace) {
9010                    self.parse_block()?
9011                } else {
9012                    let bline = self.peek_line();
9013                    let expr = self.parse_assign_expr()?;
9014                    vec![Statement::new(StmtKind::Expression(expr), bline)]
9015                };
9016                Ok(Expr {
9017                    kind: ExprKind::EveryBlock { interval, body },
9018                    line,
9019                })
9020            }
9021            "gen" => {
9022                if !matches!(self.peek(), Token::LBrace) {
9023                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
9024                }
9025                let body = self.parse_block()?;
9026                Ok(Expr {
9027                    kind: ExprKind::GenBlock { body },
9028                    line,
9029                })
9030            }
9031            "yield" => {
9032                let e = self.parse_assign_expr()?;
9033                Ok(Expr {
9034                    kind: ExprKind::Yield(Box::new(e)),
9035                    line,
9036                })
9037            }
9038            "await" => {
9039                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9040                    return Ok(e);
9041                }
9042                // `await` defaults to `$_` so `map { await } @tasks` works
9043                // (Perl-style topic-defaulting unary).
9044                let a = self.parse_one_arg_or_default()?;
9045                Ok(Expr {
9046                    kind: ExprKind::Await(Box::new(a)),
9047                    line,
9048                })
9049            }
9050            "slurp" | "cat" | "c" => {
9051                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9052                    return Ok(e);
9053                }
9054                let a = self.parse_one_arg_or_default()?;
9055                Ok(Expr {
9056                    kind: ExprKind::Slurp(Box::new(a)),
9057                    line,
9058                })
9059            }
9060            "capture" => {
9061                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9062                    return Ok(e);
9063                }
9064                let a = self.parse_one_arg()?;
9065                Ok(Expr {
9066                    kind: ExprKind::Capture(Box::new(a)),
9067                    line,
9068                })
9069            }
9070            "fetch_url" => {
9071                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9072                    return Ok(e);
9073                }
9074                let a = self.parse_one_arg()?;
9075                Ok(Expr {
9076                    kind: ExprKind::FetchUrl(Box::new(a)),
9077                    line,
9078                })
9079            }
9080            "pchannel" => {
9081                let capacity = if self.eat(&Token::LParen) {
9082                    if matches!(self.peek(), Token::RParen) {
9083                        self.advance();
9084                        None
9085                    } else {
9086                        let e = self.parse_expression()?;
9087                        self.expect(&Token::RParen)?;
9088                        Some(Box::new(e))
9089                    }
9090                } else {
9091                    None
9092                };
9093                Ok(Expr {
9094                    kind: ExprKind::Pchannel { capacity },
9095                    line,
9096                })
9097            }
9098            "psort" => {
9099                if matches!(self.peek(), Token::LBrace)
9100                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
9101                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9102                {
9103                    let block = self.parse_block_or_bareword_cmp_block()?;
9104                    self.eat(&Token::Comma);
9105                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9106                    Ok(Expr {
9107                        kind: ExprKind::PSortExpr {
9108                            cmp: Some(block),
9109                            list: Box::new(list),
9110                            progress: progress.map(Box::new),
9111                        },
9112                        line,
9113                    })
9114                } else {
9115                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9116                    Ok(Expr {
9117                        kind: ExprKind::PSortExpr {
9118                            cmp: None,
9119                            list: Box::new(list),
9120                            progress: progress.map(Box::new),
9121                        },
9122                        line,
9123                    })
9124                }
9125            }
9126            "preduce" => {
9127                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9128                Ok(Expr {
9129                    kind: ExprKind::PReduceExpr {
9130                        block,
9131                        list: Box::new(list),
9132                        progress: progress.map(Box::new),
9133                    },
9134                    line,
9135                })
9136            }
9137            "preduce_init" => {
9138                let (init, block, list, progress) =
9139                    self.parse_init_block_then_list_optional_progress()?;
9140                Ok(Expr {
9141                    kind: ExprKind::PReduceInitExpr {
9142                        init: Box::new(init),
9143                        block,
9144                        list: Box::new(list),
9145                        progress: progress.map(Box::new),
9146                    },
9147                    line,
9148                })
9149            }
9150            "pmap_reduce" => {
9151                let map_block = self.parse_block_or_bareword_block()?;
9152                // After the map block, expect either a `{ REDUCE }` block, or
9153                // after an eaten comma, a blockless reduce expr (`$a + $b`).
9154                let reduce_block = if matches!(self.peek(), Token::LBrace) {
9155                    self.parse_block()?
9156                } else {
9157                    // comma separates blockless map from blockless reduce
9158                    self.expect(&Token::Comma)?;
9159                    self.parse_block_or_bareword_cmp_block()?
9160                };
9161                self.eat(&Token::Comma);
9162                let line = self.peek_line();
9163                if let Token::Ident(ref kw) = self.peek().clone() {
9164                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
9165                        self.advance();
9166                        self.expect(&Token::FatArrow)?;
9167                        let prog = self.parse_assign_expr()?;
9168                        return Ok(Expr {
9169                            kind: ExprKind::PMapReduceExpr {
9170                                map_block,
9171                                reduce_block,
9172                                list: Box::new(Expr {
9173                                    kind: ExprKind::List(vec![]),
9174                                    line,
9175                                }),
9176                                progress: Some(Box::new(prog)),
9177                            },
9178                            line,
9179                        });
9180                    }
9181                }
9182                if matches!(
9183                    self.peek(),
9184                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
9185                ) {
9186                    return Ok(Expr {
9187                        kind: ExprKind::PMapReduceExpr {
9188                            map_block,
9189                            reduce_block,
9190                            list: Box::new(Expr {
9191                                kind: ExprKind::List(vec![]),
9192                                line,
9193                            }),
9194                            progress: None,
9195                        },
9196                        line,
9197                    });
9198                }
9199                let mut parts = vec![self.parse_assign_expr()?];
9200                loop {
9201                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
9202                        break;
9203                    }
9204                    if matches!(
9205                        self.peek(),
9206                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
9207                    ) {
9208                        break;
9209                    }
9210                    if let Token::Ident(ref kw) = self.peek().clone() {
9211                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
9212                            self.advance();
9213                            self.expect(&Token::FatArrow)?;
9214                            let prog = self.parse_assign_expr()?;
9215                            return Ok(Expr {
9216                                kind: ExprKind::PMapReduceExpr {
9217                                    map_block,
9218                                    reduce_block,
9219                                    list: Box::new(merge_expr_list(parts)),
9220                                    progress: Some(Box::new(prog)),
9221                                },
9222                                line,
9223                            });
9224                        }
9225                    }
9226                    parts.push(self.parse_assign_expr()?);
9227                }
9228                Ok(Expr {
9229                    kind: ExprKind::PMapReduceExpr {
9230                        map_block,
9231                        reduce_block,
9232                        list: Box::new(merge_expr_list(parts)),
9233                        progress: None,
9234                    },
9235                    line,
9236                })
9237            }
9238            "puniq" => {
9239                if self.pipe_supplies_slurped_list_operand() {
9240                    return Ok(Expr {
9241                        kind: ExprKind::FuncCall {
9242                            name: "puniq".to_string(),
9243                            args: vec![],
9244                        },
9245                        line,
9246                    });
9247                }
9248                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9249                let mut args = vec![list];
9250                if let Some(p) = progress {
9251                    args.push(p);
9252                }
9253                Ok(Expr {
9254                    kind: ExprKind::FuncCall {
9255                        name: "puniq".to_string(),
9256                        args,
9257                    },
9258                    line,
9259                })
9260            }
9261            "pfirst" => {
9262                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9263                let cr = Expr {
9264                    kind: ExprKind::CodeRef {
9265                        params: vec![],
9266                        body: block,
9267                    },
9268                    line,
9269                };
9270                let mut args = vec![cr, list];
9271                if let Some(p) = progress {
9272                    args.push(p);
9273                }
9274                Ok(Expr {
9275                    kind: ExprKind::FuncCall {
9276                        name: "pfirst".to_string(),
9277                        args,
9278                    },
9279                    line,
9280                })
9281            }
9282            "pany" => {
9283                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9284                let cr = Expr {
9285                    kind: ExprKind::CodeRef {
9286                        params: vec![],
9287                        body: block,
9288                    },
9289                    line,
9290                };
9291                let mut args = vec![cr, list];
9292                if let Some(p) = progress {
9293                    args.push(p);
9294                }
9295                Ok(Expr {
9296                    kind: ExprKind::FuncCall {
9297                        name: "pany".to_string(),
9298                        args,
9299                    },
9300                    line,
9301                })
9302            }
9303            "uniq" | "distinct" => {
9304                if self.pipe_supplies_slurped_list_operand() {
9305                    return Ok(Expr {
9306                        kind: ExprKind::FuncCall {
9307                            name: name.clone(),
9308                            args: vec![],
9309                        },
9310                        line,
9311                    });
9312                }
9313                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9314                if progress.is_some() {
9315                    return Err(self.syntax_err(
9316                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
9317                        line,
9318                    ));
9319                }
9320                Ok(Expr {
9321                    kind: ExprKind::FuncCall {
9322                        name: name.clone(),
9323                        args: vec![list],
9324                    },
9325                    line,
9326                })
9327            }
9328            "flatten" => {
9329                if self.pipe_supplies_slurped_list_operand() {
9330                    return Ok(Expr {
9331                        kind: ExprKind::FuncCall {
9332                            name: "flatten".to_string(),
9333                            args: vec![],
9334                        },
9335                        line,
9336                    });
9337                }
9338                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9339                if progress.is_some() {
9340                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
9341                }
9342                Ok(Expr {
9343                    kind: ExprKind::FuncCall {
9344                        name: "flatten".to_string(),
9345                        args: vec![list],
9346                    },
9347                    line,
9348                })
9349            }
9350            "set" => {
9351                if self.pipe_supplies_slurped_list_operand() {
9352                    return Ok(Expr {
9353                        kind: ExprKind::FuncCall {
9354                            name: "set".to_string(),
9355                            args: vec![],
9356                        },
9357                        line,
9358                    });
9359                }
9360                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9361                if progress.is_some() {
9362                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
9363                }
9364                Ok(Expr {
9365                    kind: ExprKind::FuncCall {
9366                        name: "set".to_string(),
9367                        args: vec![list],
9368                    },
9369                    line,
9370                })
9371            }
9372            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
9373            // Defaults to `$_` when no arg is given, like `length`. See
9374            // `builtin_file_size` in builtins.rs for the runtime behavior.
9375            "size" => {
9376                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9377                    return Ok(e);
9378                }
9379                if self.pipe_supplies_slurped_list_operand() {
9380                    return Ok(Expr {
9381                        kind: ExprKind::FuncCall {
9382                            name: "size".to_string(),
9383                            args: vec![],
9384                        },
9385                        line,
9386                    });
9387                }
9388                let a = self.parse_one_arg_or_default()?;
9389                Ok(Expr {
9390                    kind: ExprKind::FuncCall {
9391                        name: "size".to_string(),
9392                        args: vec![a],
9393                    },
9394                    line,
9395                })
9396            }
9397            "list_count" | "list_size" | "count" | "len" | "cnt" => {
9398                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9399                    return Ok(e);
9400                }
9401                if self.pipe_supplies_slurped_list_operand() {
9402                    return Ok(Expr {
9403                        kind: ExprKind::FuncCall {
9404                            name: name.clone(),
9405                            args: vec![],
9406                        },
9407                        line,
9408                    });
9409                }
9410                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9411                if progress.is_some() {
9412                    return Err(self.syntax_err(
9413                        "`progress =>` is not supported for list_count / list_size / count / cnt",
9414                        line,
9415                    ));
9416                }
9417                Ok(Expr {
9418                    kind: ExprKind::FuncCall {
9419                        name: name.clone(),
9420                        args: vec![list],
9421                    },
9422                    line,
9423                })
9424            }
9425            "shuffle" | "shuffled" => {
9426                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9427                    return Ok(e);
9428                }
9429                if self.pipe_supplies_slurped_list_operand() {
9430                    return Ok(Expr {
9431                        kind: ExprKind::FuncCall {
9432                            name: "shuffle".to_string(),
9433                            args: vec![],
9434                        },
9435                        line,
9436                    });
9437                }
9438                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9439                if progress.is_some() {
9440                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
9441                }
9442                Ok(Expr {
9443                    kind: ExprKind::FuncCall {
9444                        name: "shuffle".to_string(),
9445                        args: vec![list],
9446                    },
9447                    line,
9448                })
9449            }
9450            "chunked" => {
9451                let mut parts = Vec::new();
9452                if self.eat(&Token::LParen) {
9453                    if !matches!(self.peek(), Token::RParen) {
9454                        parts.push(self.parse_assign_expr()?);
9455                        while self.eat(&Token::Comma) {
9456                            if matches!(self.peek(), Token::RParen) {
9457                                break;
9458                            }
9459                            parts.push(self.parse_assign_expr()?);
9460                        }
9461                    }
9462                    self.expect(&Token::RParen)?;
9463                } else {
9464                    // Paren-less `chunked N`: `|>` is a hard terminator, not
9465                    // an operator inside the arg (see
9466                    // `parse_assign_expr_stop_at_pipe`).
9467                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
9468                    loop {
9469                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
9470                            break;
9471                        }
9472                        if matches!(
9473                            self.peek(),
9474                            Token::Semicolon
9475                                | Token::RBrace
9476                                | Token::RParen
9477                                | Token::Eof
9478                                | Token::PipeForward
9479                        ) {
9480                            break;
9481                        }
9482                        if self.peek_is_postfix_stmt_modifier_keyword() {
9483                            break;
9484                        }
9485                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
9486                    }
9487                }
9488                if parts.len() == 1 {
9489                    let n = parts.pop().unwrap();
9490                    return Ok(Expr {
9491                        kind: ExprKind::FuncCall {
9492                            name: "chunked".to_string(),
9493                            args: vec![n],
9494                        },
9495                        line,
9496                    });
9497                }
9498                if parts.is_empty() {
9499                    return Ok(Expr {
9500                        kind: ExprKind::FuncCall {
9501                            name: "chunked".to_string(),
9502                            args: parts,
9503                        },
9504                        line,
9505                    });
9506                }
9507                if parts.len() == 2 {
9508                    let n = parts.pop().unwrap();
9509                    let list = parts.pop().unwrap();
9510                    return Ok(Expr {
9511                        kind: ExprKind::FuncCall {
9512                            name: "chunked".to_string(),
9513                            args: vec![list, n],
9514                        },
9515                        line,
9516                    });
9517                }
9518                Err(self.syntax_err(
9519                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
9520                    line,
9521                ))
9522            }
9523            "windowed" => {
9524                let mut parts = Vec::new();
9525                if self.eat(&Token::LParen) {
9526                    if !matches!(self.peek(), Token::RParen) {
9527                        parts.push(self.parse_assign_expr()?);
9528                        while self.eat(&Token::Comma) {
9529                            if matches!(self.peek(), Token::RParen) {
9530                                break;
9531                            }
9532                            parts.push(self.parse_assign_expr()?);
9533                        }
9534                    }
9535                    self.expect(&Token::RParen)?;
9536                } else {
9537                    // Paren-less `windowed N`: same `|>`-terminator rule as
9538                    // `chunked` above.
9539                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
9540                    loop {
9541                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
9542                            break;
9543                        }
9544                        if matches!(
9545                            self.peek(),
9546                            Token::Semicolon
9547                                | Token::RBrace
9548                                | Token::RParen
9549                                | Token::Eof
9550                                | Token::PipeForward
9551                        ) {
9552                            break;
9553                        }
9554                        if self.peek_is_postfix_stmt_modifier_keyword() {
9555                            break;
9556                        }
9557                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
9558                    }
9559                }
9560                if parts.len() == 1 {
9561                    let n = parts.pop().unwrap();
9562                    return Ok(Expr {
9563                        kind: ExprKind::FuncCall {
9564                            name: "windowed".to_string(),
9565                            args: vec![n],
9566                        },
9567                        line,
9568                    });
9569                }
9570                if parts.is_empty() {
9571                    return Ok(Expr {
9572                        kind: ExprKind::FuncCall {
9573                            name: "windowed".to_string(),
9574                            args: parts,
9575                        },
9576                        line,
9577                    });
9578                }
9579                if parts.len() == 2 {
9580                    let n = parts.pop().unwrap();
9581                    let list = parts.pop().unwrap();
9582                    return Ok(Expr {
9583                        kind: ExprKind::FuncCall {
9584                            name: "windowed".to_string(),
9585                            args: vec![list, n],
9586                        },
9587                        line,
9588                    });
9589                }
9590                Err(self.syntax_err(
9591                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
9592                    line,
9593                ))
9594            }
9595            "any" | "all" | "none" => {
9596                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9597                if progress.is_some() {
9598                    return Err(self.syntax_err(
9599                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
9600                        line,
9601                    ));
9602                }
9603                let cr = Expr {
9604                    kind: ExprKind::CodeRef {
9605                        params: vec![],
9606                        body: block,
9607                    },
9608                    line,
9609                };
9610                Ok(Expr {
9611                    kind: ExprKind::FuncCall {
9612                        name: name.clone(),
9613                        args: vec![cr, list],
9614                    },
9615                    line,
9616                })
9617            }
9618            // Ruby `detect` / `find` — same as `List::Util::first` (first element matching block).
9619            "first" | "detect" | "find" => {
9620                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9621                if progress.is_some() {
9622                    return Err(self.syntax_err(
9623                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
9624                        line,
9625                    ));
9626                }
9627                let cr = Expr {
9628                    kind: ExprKind::CodeRef {
9629                        params: vec![],
9630                        body: block,
9631                    },
9632                    line,
9633                };
9634                Ok(Expr {
9635                    kind: ExprKind::FuncCall {
9636                        name: "first".to_string(),
9637                        args: vec![cr, list],
9638                    },
9639                    line,
9640                })
9641            }
9642            "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9643            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
9644                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9645                if progress.is_some() {
9646                    return Err(
9647                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
9648                    );
9649                }
9650                let cr = Expr {
9651                    kind: ExprKind::CodeRef {
9652                        params: vec![],
9653                        body: block,
9654                    },
9655                    line,
9656                };
9657                Ok(Expr {
9658                    kind: ExprKind::FuncCall {
9659                        name: name.to_string(),
9660                        args: vec![cr, list],
9661                    },
9662                    line,
9663                })
9664            }
9665            "group_by" | "chunk_by" => {
9666                if matches!(self.peek(), Token::LBrace) {
9667                    let (block, list) = self.parse_block_list()?;
9668                    let cr = Expr {
9669                        kind: ExprKind::CodeRef {
9670                            params: vec![],
9671                            body: block,
9672                        },
9673                        line,
9674                    };
9675                    Ok(Expr {
9676                        kind: ExprKind::FuncCall {
9677                            name: name.to_string(),
9678                            args: vec![cr, list],
9679                        },
9680                        line,
9681                    })
9682                } else {
9683                    let key_expr = self.parse_assign_expr()?;
9684                    self.expect(&Token::Comma)?;
9685                    let list_parts = self.parse_list_until_terminator()?;
9686                    let list_expr = if list_parts.len() == 1 {
9687                        list_parts.into_iter().next().unwrap()
9688                    } else {
9689                        Expr {
9690                            kind: ExprKind::List(list_parts),
9691                            line,
9692                        }
9693                    };
9694                    Ok(Expr {
9695                        kind: ExprKind::FuncCall {
9696                            name: name.to_string(),
9697                            args: vec![key_expr, list_expr],
9698                        },
9699                        line,
9700                    })
9701                }
9702            }
9703            "with_index" => {
9704                if self.pipe_supplies_slurped_list_operand() {
9705                    return Ok(Expr {
9706                        kind: ExprKind::FuncCall {
9707                            name: "with_index".to_string(),
9708                            args: vec![],
9709                        },
9710                        line,
9711                    });
9712                }
9713                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9714                if progress.is_some() {
9715                    return Err(
9716                        self.syntax_err("`progress =>` is not supported for with_index", line)
9717                    );
9718                }
9719                Ok(Expr {
9720                    kind: ExprKind::FuncCall {
9721                        name: "with_index".to_string(),
9722                        args: vec![list],
9723                    },
9724                    line,
9725                })
9726            }
9727            "pcache" => {
9728                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9729                Ok(Expr {
9730                    kind: ExprKind::PcacheExpr {
9731                        block,
9732                        list: Box::new(list),
9733                        progress: progress.map(Box::new),
9734                    },
9735                    line,
9736                })
9737            }
9738            "pselect" => {
9739                let paren = self.eat(&Token::LParen);
9740                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
9741                if paren {
9742                    self.expect(&Token::RParen)?;
9743                }
9744                if receivers.is_empty() {
9745                    return Err(self.syntax_err("pselect needs at least one receiver", line));
9746                }
9747                Ok(Expr {
9748                    kind: ExprKind::PselectExpr {
9749                        receivers,
9750                        timeout: timeout.map(Box::new),
9751                    },
9752                    line,
9753                })
9754            }
9755            "open" => {
9756                let paren = matches!(self.peek(), Token::LParen);
9757                if paren {
9758                    self.advance();
9759                }
9760                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
9761                    self.advance();
9762                    let name = self.parse_scalar_var_name()?;
9763                    self.expect(&Token::Comma)?;
9764                    let mode = self.parse_assign_expr()?;
9765                    let file = if self.eat(&Token::Comma) {
9766                        Some(self.parse_assign_expr()?)
9767                    } else {
9768                        None
9769                    };
9770                    if paren {
9771                        self.expect(&Token::RParen)?;
9772                    }
9773                    Ok(Expr {
9774                        kind: ExprKind::Open {
9775                            handle: Box::new(Expr {
9776                                kind: ExprKind::OpenMyHandle { name },
9777                                line,
9778                            }),
9779                            mode: Box::new(mode),
9780                            file: file.map(Box::new),
9781                        },
9782                        line,
9783                    })
9784                } else {
9785                    let args = if paren {
9786                        self.parse_arg_list()?
9787                    } else {
9788                        self.parse_list_until_terminator()?
9789                    };
9790                    if paren {
9791                        self.expect(&Token::RParen)?;
9792                    }
9793                    if args.len() < 2 {
9794                        return Err(self.syntax_err("open requires at least 2 arguments", line));
9795                    }
9796                    Ok(Expr {
9797                        kind: ExprKind::Open {
9798                            handle: Box::new(args[0].clone()),
9799                            mode: Box::new(args[1].clone()),
9800                            file: args.get(2).cloned().map(Box::new),
9801                        },
9802                        line,
9803                    })
9804                }
9805            }
9806            "close" => {
9807                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9808                    return Ok(e);
9809                }
9810                let a = self.parse_one_arg_or_default()?;
9811                Ok(Expr {
9812                    kind: ExprKind::Close(Box::new(a)),
9813                    line,
9814                })
9815            }
9816            "opendir" => {
9817                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9818                    return Ok(e);
9819                }
9820                let args = self.parse_builtin_args()?;
9821                if args.len() != 2 {
9822                    return Err(self.syntax_err("opendir requires two arguments", line));
9823                }
9824                Ok(Expr {
9825                    kind: ExprKind::Opendir {
9826                        handle: Box::new(args[0].clone()),
9827                        path: Box::new(args[1].clone()),
9828                    },
9829                    line,
9830                })
9831            }
9832            "readdir" => {
9833                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9834                    return Ok(e);
9835                }
9836                let a = self.parse_one_arg()?;
9837                Ok(Expr {
9838                    kind: ExprKind::Readdir(Box::new(a)),
9839                    line,
9840                })
9841            }
9842            "closedir" => {
9843                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9844                    return Ok(e);
9845                }
9846                let a = self.parse_one_arg()?;
9847                Ok(Expr {
9848                    kind: ExprKind::Closedir(Box::new(a)),
9849                    line,
9850                })
9851            }
9852            "rewinddir" => {
9853                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9854                    return Ok(e);
9855                }
9856                let a = self.parse_one_arg()?;
9857                Ok(Expr {
9858                    kind: ExprKind::Rewinddir(Box::new(a)),
9859                    line,
9860                })
9861            }
9862            "telldir" => {
9863                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9864                    return Ok(e);
9865                }
9866                let a = self.parse_one_arg()?;
9867                Ok(Expr {
9868                    kind: ExprKind::Telldir(Box::new(a)),
9869                    line,
9870                })
9871            }
9872            "seekdir" => {
9873                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9874                    return Ok(e);
9875                }
9876                let args = self.parse_builtin_args()?;
9877                if args.len() != 2 {
9878                    return Err(self.syntax_err("seekdir requires two arguments", line));
9879                }
9880                Ok(Expr {
9881                    kind: ExprKind::Seekdir {
9882                        handle: Box::new(args[0].clone()),
9883                        position: Box::new(args[1].clone()),
9884                    },
9885                    line,
9886                })
9887            }
9888            "eof" => {
9889                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9890                    return Ok(e);
9891                }
9892                if matches!(self.peek(), Token::LParen) {
9893                    self.advance();
9894                    if matches!(self.peek(), Token::RParen) {
9895                        self.advance();
9896                        Ok(Expr {
9897                            kind: ExprKind::Eof(None),
9898                            line,
9899                        })
9900                    } else {
9901                        let a = self.parse_expression()?;
9902                        self.expect(&Token::RParen)?;
9903                        Ok(Expr {
9904                            kind: ExprKind::Eof(Some(Box::new(a))),
9905                            line,
9906                        })
9907                    }
9908                } else {
9909                    Ok(Expr {
9910                        kind: ExprKind::Eof(None),
9911                        line,
9912                    })
9913                }
9914            }
9915            "system" => {
9916                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9917                    return Ok(e);
9918                }
9919                let args = self.parse_builtin_args()?;
9920                Ok(Expr {
9921                    kind: ExprKind::System(args),
9922                    line,
9923                })
9924            }
9925            "exec" => {
9926                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9927                    return Ok(e);
9928                }
9929                let args = self.parse_builtin_args()?;
9930                Ok(Expr {
9931                    kind: ExprKind::Exec(args),
9932                    line,
9933                })
9934            }
9935            "eval" => {
9936                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9937                    return Ok(e);
9938                }
9939                let a = if matches!(self.peek(), Token::LBrace) {
9940                    let block = self.parse_block()?;
9941                    Expr {
9942                        kind: ExprKind::CodeRef {
9943                            params: vec![],
9944                            body: block,
9945                        },
9946                        line,
9947                    }
9948                } else {
9949                    self.parse_one_arg_or_default()?
9950                };
9951                Ok(Expr {
9952                    kind: ExprKind::Eval(Box::new(a)),
9953                    line,
9954                })
9955            }
9956            "do" => {
9957                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9958                    return Ok(e);
9959                }
9960                let a = self.parse_one_arg()?;
9961                Ok(Expr {
9962                    kind: ExprKind::Do(Box::new(a)),
9963                    line,
9964                })
9965            }
9966            "require" => {
9967                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9968                    return Ok(e);
9969                }
9970                let a = self.parse_one_arg()?;
9971                Ok(Expr {
9972                    kind: ExprKind::Require(Box::new(a)),
9973                    line,
9974                })
9975            }
9976            "exit" => {
9977                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9978                    return Ok(e);
9979                }
9980                if matches!(
9981                    self.peek(),
9982                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
9983                ) {
9984                    Ok(Expr {
9985                        kind: ExprKind::Exit(None),
9986                        line,
9987                    })
9988                } else {
9989                    let a = self.parse_one_arg()?;
9990                    Ok(Expr {
9991                        kind: ExprKind::Exit(Some(Box::new(a))),
9992                        line,
9993                    })
9994                }
9995            }
9996            "chdir" => {
9997                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9998                    return Ok(e);
9999                }
10000                let a = self.parse_one_arg_or_default()?;
10001                Ok(Expr {
10002                    kind: ExprKind::Chdir(Box::new(a)),
10003                    line,
10004                })
10005            }
10006            "mkdir" => {
10007                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10008                    return Ok(e);
10009                }
10010                let args = self.parse_builtin_args()?;
10011                Ok(Expr {
10012                    kind: ExprKind::Mkdir {
10013                        path: Box::new(args[0].clone()),
10014                        mode: args.get(1).cloned().map(Box::new),
10015                    },
10016                    line,
10017                })
10018            }
10019            "unlink" | "rm" => {
10020                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10021                    return Ok(e);
10022                }
10023                let args = self.parse_builtin_args()?;
10024                Ok(Expr {
10025                    kind: ExprKind::Unlink(args),
10026                    line,
10027                })
10028            }
10029            "rename" => {
10030                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10031                    return Ok(e);
10032                }
10033                let args = self.parse_builtin_args()?;
10034                if args.len() != 2 {
10035                    return Err(self.syntax_err("rename requires two arguments", line));
10036                }
10037                Ok(Expr {
10038                    kind: ExprKind::Rename {
10039                        old: Box::new(args[0].clone()),
10040                        new: Box::new(args[1].clone()),
10041                    },
10042                    line,
10043                })
10044            }
10045            "chmod" => {
10046                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10047                    return Ok(e);
10048                }
10049                let args = self.parse_builtin_args()?;
10050                if args.len() < 2 {
10051                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
10052                }
10053                Ok(Expr {
10054                    kind: ExprKind::Chmod(args),
10055                    line,
10056                })
10057            }
10058            "chown" => {
10059                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10060                    return Ok(e);
10061                }
10062                let args = self.parse_builtin_args()?;
10063                if args.len() < 3 {
10064                    return Err(
10065                        self.syntax_err("chown requires uid, gid, and at least one file", line)
10066                    );
10067                }
10068                Ok(Expr {
10069                    kind: ExprKind::Chown(args),
10070                    line,
10071                })
10072            }
10073            "stat" => {
10074                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10075                    return Ok(e);
10076                }
10077                let args = self.parse_builtin_args()?;
10078                let arg = if args.len() == 1 {
10079                    args[0].clone()
10080                } else if args.is_empty() {
10081                    Expr {
10082                        kind: ExprKind::ScalarVar("_".into()),
10083                        line,
10084                    }
10085                } else {
10086                    return Err(self.syntax_err("stat requires zero or one argument", line));
10087                };
10088                Ok(Expr {
10089                    kind: ExprKind::Stat(Box::new(arg)),
10090                    line,
10091                })
10092            }
10093            "lstat" => {
10094                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10095                    return Ok(e);
10096                }
10097                let args = self.parse_builtin_args()?;
10098                let arg = if args.len() == 1 {
10099                    args[0].clone()
10100                } else if args.is_empty() {
10101                    Expr {
10102                        kind: ExprKind::ScalarVar("_".into()),
10103                        line,
10104                    }
10105                } else {
10106                    return Err(self.syntax_err("lstat requires zero or one argument", line));
10107                };
10108                Ok(Expr {
10109                    kind: ExprKind::Lstat(Box::new(arg)),
10110                    line,
10111                })
10112            }
10113            "link" => {
10114                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10115                    return Ok(e);
10116                }
10117                let args = self.parse_builtin_args()?;
10118                if args.len() != 2 {
10119                    return Err(self.syntax_err("link requires two arguments", line));
10120                }
10121                Ok(Expr {
10122                    kind: ExprKind::Link {
10123                        old: Box::new(args[0].clone()),
10124                        new: Box::new(args[1].clone()),
10125                    },
10126                    line,
10127                })
10128            }
10129            "symlink" => {
10130                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10131                    return Ok(e);
10132                }
10133                let args = self.parse_builtin_args()?;
10134                if args.len() != 2 {
10135                    return Err(self.syntax_err("symlink requires two arguments", line));
10136                }
10137                Ok(Expr {
10138                    kind: ExprKind::Symlink {
10139                        old: Box::new(args[0].clone()),
10140                        new: Box::new(args[1].clone()),
10141                    },
10142                    line,
10143                })
10144            }
10145            "readlink" => {
10146                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10147                    return Ok(e);
10148                }
10149                let args = self.parse_builtin_args()?;
10150                let arg = if args.len() == 1 {
10151                    args[0].clone()
10152                } else if args.is_empty() {
10153                    Expr {
10154                        kind: ExprKind::ScalarVar("_".into()),
10155                        line,
10156                    }
10157                } else {
10158                    return Err(self.syntax_err("readlink requires zero or one argument", line));
10159                };
10160                Ok(Expr {
10161                    kind: ExprKind::Readlink(Box::new(arg)),
10162                    line,
10163                })
10164            }
10165            "files" => {
10166                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10167                    return Ok(e);
10168                }
10169                let args = self.parse_builtin_args()?;
10170                Ok(Expr {
10171                    kind: ExprKind::Files(args),
10172                    line,
10173                })
10174            }
10175            "filesf" => {
10176                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10177                    return Ok(e);
10178                }
10179                let args = self.parse_builtin_args()?;
10180                Ok(Expr {
10181                    kind: ExprKind::Filesf(args),
10182                    line,
10183                })
10184            }
10185            "fr" => {
10186                let args = self.parse_builtin_args()?;
10187                Ok(Expr {
10188                    kind: ExprKind::FilesfRecursive(args),
10189                    line,
10190                })
10191            }
10192            "dirs" | "d" => {
10193                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10194                    return Ok(e);
10195                }
10196                let args = self.parse_builtin_args()?;
10197                Ok(Expr {
10198                    kind: ExprKind::Dirs(args),
10199                    line,
10200                })
10201            }
10202            "dr" => {
10203                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10204                    return Ok(e);
10205                }
10206                let args = self.parse_builtin_args()?;
10207                Ok(Expr {
10208                    kind: ExprKind::DirsRecursive(args),
10209                    line,
10210                })
10211            }
10212            "sym_links" => {
10213                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10214                    return Ok(e);
10215                }
10216                let args = self.parse_builtin_args()?;
10217                Ok(Expr {
10218                    kind: ExprKind::SymLinks(args),
10219                    line,
10220                })
10221            }
10222            "sockets" => {
10223                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10224                    return Ok(e);
10225                }
10226                let args = self.parse_builtin_args()?;
10227                Ok(Expr {
10228                    kind: ExprKind::Sockets(args),
10229                    line,
10230                })
10231            }
10232            "pipes" => {
10233                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10234                    return Ok(e);
10235                }
10236                let args = self.parse_builtin_args()?;
10237                Ok(Expr {
10238                    kind: ExprKind::Pipes(args),
10239                    line,
10240                })
10241            }
10242            "block_devices" => {
10243                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10244                    return Ok(e);
10245                }
10246                let args = self.parse_builtin_args()?;
10247                Ok(Expr {
10248                    kind: ExprKind::BlockDevices(args),
10249                    line,
10250                })
10251            }
10252            "char_devices" => {
10253                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10254                    return Ok(e);
10255                }
10256                let args = self.parse_builtin_args()?;
10257                Ok(Expr {
10258                    kind: ExprKind::CharDevices(args),
10259                    line,
10260                })
10261            }
10262            "glob" => {
10263                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10264                    return Ok(e);
10265                }
10266                let args = self.parse_builtin_args()?;
10267                Ok(Expr {
10268                    kind: ExprKind::Glob(args),
10269                    line,
10270                })
10271            }
10272            "glob_par" => {
10273                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10274                    return Ok(e);
10275                }
10276                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
10277                Ok(Expr {
10278                    kind: ExprKind::GlobPar { args, progress },
10279                    line,
10280                })
10281            }
10282            "par_sed" => {
10283                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10284                    return Ok(e);
10285                }
10286                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
10287                Ok(Expr {
10288                    kind: ExprKind::ParSed { args, progress },
10289                    line,
10290                })
10291            }
10292            "bless" => {
10293                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10294                    return Ok(e);
10295                }
10296                let args = self.parse_builtin_args()?;
10297                Ok(Expr {
10298                    kind: ExprKind::Bless {
10299                        ref_expr: Box::new(args[0].clone()),
10300                        class: args.get(1).cloned().map(Box::new),
10301                    },
10302                    line,
10303                })
10304            }
10305            "caller" => {
10306                if matches!(self.peek(), Token::LParen) {
10307                    self.advance();
10308                    if matches!(self.peek(), Token::RParen) {
10309                        self.advance();
10310                        Ok(Expr {
10311                            kind: ExprKind::Caller(None),
10312                            line,
10313                        })
10314                    } else {
10315                        let a = self.parse_expression()?;
10316                        self.expect(&Token::RParen)?;
10317                        Ok(Expr {
10318                            kind: ExprKind::Caller(Some(Box::new(a))),
10319                            line,
10320                        })
10321                    }
10322                } else {
10323                    Ok(Expr {
10324                        kind: ExprKind::Caller(None),
10325                        line,
10326                    })
10327                }
10328            }
10329            "wantarray" => Ok(Expr {
10330                kind: ExprKind::Wantarray,
10331                line,
10332            }),
10333            "sub" | "fn" => {
10334                // Anonymous sub/fn — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
10335                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
10336                let body = self.parse_block()?;
10337                Ok(Expr {
10338                    kind: ExprKind::CodeRef { params, body },
10339                    line,
10340                })
10341            }
10342            _ => {
10343                // Generic function call
10344                // Check for fat arrow (bareword string in hash)
10345                if matches!(self.peek(), Token::FatArrow) {
10346                    return Ok(Expr {
10347                        kind: ExprKind::String(name),
10348                        line,
10349                    });
10350                }
10351                // Function call with optional parens
10352                if matches!(self.peek(), Token::LParen) {
10353                    self.advance();
10354                    let args = self.parse_arg_list()?;
10355                    self.expect(&Token::RParen)?;
10356                    Ok(Expr {
10357                        kind: ExprKind::FuncCall { name, args },
10358                        line,
10359                    })
10360                } else if self.peek().is_term_start()
10361                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
10362                        && matches!(self.peek_at(1), Token::Ident(_)))
10363                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
10364                {
10365                    // Perl allows func arg without parens
10366                    // Guard: `sub <name> { }` is a named sub declaration (new
10367                    // statement), not an argument to the preceding call.
10368                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
10369                    // barewords (used by thread macro so `t Color::Red p` treats
10370                    // `p` as a stage, not an argument to the enum variant), but
10371                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
10372                    let args = self.parse_list_until_terminator()?;
10373                    Ok(Expr {
10374                        kind: ExprKind::FuncCall { name, args },
10375                        line,
10376                    })
10377                } else {
10378                    // No parens, no visible arguments — emit a Bareword.
10379                    // At runtime, Bareword tries sub resolution first (zero-arg
10380                    // call) and falls back to a string value.  stryke extension
10381                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
10382                    // with `$_` injection separately.
10383                    Ok(Expr {
10384                        kind: ExprKind::Bareword(name),
10385                        line,
10386                    })
10387                }
10388            }
10389        }
10390    }
10391
10392    fn parse_print_like(
10393        &mut self,
10394        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
10395    ) -> PerlResult<Expr> {
10396        let line = self.peek_line();
10397        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
10398        let handle = if let Token::Ident(ref h) = self.peek().clone() {
10399            if h.chars().all(|c| c.is_uppercase() || c == '_')
10400                && !matches!(self.peek(), Token::LParen)
10401            {
10402                let h = h.clone();
10403                let saved = self.pos;
10404                self.advance();
10405                // Verify next token is a term start (not operator)
10406                if self.peek().is_term_start()
10407                    || matches!(
10408                        self.peek(),
10409                        Token::DoubleString(_) | Token::BacktickString(_) | Token::SingleString(_)
10410                    )
10411                {
10412                    Some(h)
10413                } else {
10414                    self.pos = saved;
10415                    None
10416                }
10417            } else {
10418                None
10419            }
10420        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
10421            // `print $fh "msg"` — scalar variable as indirect filehandle.
10422            // Treat as handle when the next token (after $var) is a term-start or
10423            // string literal *without* a preceding comma/operator, matching Perl's
10424            // indirect-object heuristic.
10425            // Exclude `$_` — it's virtually always the topic variable, not a handle.
10426            // Exclude `[` and `{` — those are array/hash subscripts on the variable
10427            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
10428            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
10429            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
10430            let v = v.clone();
10431            if v == "_" {
10432                None
10433            } else {
10434                let saved = self.pos;
10435                self.advance();
10436                let next = self.peek().clone();
10437                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
10438                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
10439                if !is_stmt_modifier
10440                    && !matches!(next, Token::LBracket | Token::LBrace)
10441                    && (next.is_term_start()
10442                        || matches!(
10443                            next,
10444                            Token::DoubleString(_)
10445                                | Token::BacktickString(_)
10446                                | Token::SingleString(_)
10447                        ))
10448                {
10449                    // Next token looks like a print argument — $var is the handle.
10450                    Some(format!("${v}"))
10451                } else {
10452                    self.pos = saved;
10453                    None
10454                }
10455            }
10456        } else {
10457            None
10458        };
10459        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
10460        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
10461        // are given, prints $_." (Same convention as the topic-default unary
10462        // builtins handled in `parse_one_arg_or_default`.)
10463        let args =
10464            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
10465                let line_topic = self.peek_line();
10466                self.advance(); // (
10467                self.advance(); // )
10468                vec![Expr {
10469                    kind: ExprKind::ScalarVar("_".into()),
10470                    line: line_topic,
10471                }]
10472            } else {
10473                self.parse_list_until_terminator()?
10474            };
10475        Ok(Expr {
10476            kind: make(handle, args),
10477            line,
10478        })
10479    }
10480
10481    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
10482        let block = self.parse_block()?;
10483        let block_end_line = self.prev_line();
10484        self.eat(&Token::Comma);
10485        // On the RHS of `|>`, the list operand is supplied by the piped LHS
10486        // and will be substituted at desugar time — accept a placeholder when
10487        // we're at a terminator here or on a new line (implicit semicolon).
10488        if self.in_pipe_rhs()
10489            && (matches!(
10490                self.peek(),
10491                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10492            ) || self.peek_line() > block_end_line)
10493        {
10494            let line = self.peek_line();
10495            return Ok((block, self.pipe_placeholder_list(line)));
10496        }
10497        let list = self.parse_expression()?;
10498        Ok((block, list))
10499    }
10500
10501    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
10502    /// When `paren` is true, stops at `)` as well as normal terminators.
10503    fn parse_comma_expr_list_with_timeout_tail(
10504        &mut self,
10505        paren: bool,
10506    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
10507        let mut parts = vec![self.parse_assign_expr()?];
10508        loop {
10509            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10510                break;
10511            }
10512            if paren && matches!(self.peek(), Token::RParen) {
10513                break;
10514            }
10515            if matches!(
10516                self.peek(),
10517                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10518            ) {
10519                break;
10520            }
10521            if self.peek_is_postfix_stmt_modifier_keyword() {
10522                break;
10523            }
10524            if let Token::Ident(ref kw) = self.peek().clone() {
10525                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
10526                    self.advance();
10527                    self.expect(&Token::FatArrow)?;
10528                    let t = self.parse_assign_expr()?;
10529                    return Ok((parts, Some(t)));
10530                }
10531            }
10532            parts.push(self.parse_assign_expr()?);
10533        }
10534        Ok((parts, None))
10535    }
10536
10537    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
10538    fn parse_init_block_then_list_optional_progress(
10539        &mut self,
10540    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
10541        let init = self.parse_assign_expr()?;
10542        self.expect(&Token::Comma)?;
10543        let block = self.parse_block_or_bareword_block()?;
10544        self.eat(&Token::Comma);
10545        let line = self.peek_line();
10546        if let Token::Ident(ref kw) = self.peek().clone() {
10547            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10548                self.advance();
10549                self.expect(&Token::FatArrow)?;
10550                let prog = self.parse_assign_expr()?;
10551                return Ok((
10552                    init,
10553                    block,
10554                    Expr {
10555                        kind: ExprKind::List(vec![]),
10556                        line,
10557                    },
10558                    Some(prog),
10559                ));
10560            }
10561        }
10562        if matches!(
10563            self.peek(),
10564            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10565        ) {
10566            return Ok((
10567                init,
10568                block,
10569                Expr {
10570                    kind: ExprKind::List(vec![]),
10571                    line,
10572                },
10573                None,
10574            ));
10575        }
10576        let mut parts = vec![self.parse_assign_expr()?];
10577        loop {
10578            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10579                break;
10580            }
10581            if matches!(
10582                self.peek(),
10583                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10584            ) {
10585                break;
10586            }
10587            if self.peek_is_postfix_stmt_modifier_keyword() {
10588                break;
10589            }
10590            if let Token::Ident(ref kw) = self.peek().clone() {
10591                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10592                    self.advance();
10593                    self.expect(&Token::FatArrow)?;
10594                    let prog = self.parse_assign_expr()?;
10595                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
10596                }
10597            }
10598            parts.push(self.parse_assign_expr()?);
10599        }
10600        Ok((init, block, merge_expr_list(parts), None))
10601    }
10602
10603    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
10604    fn parse_cluster_block_then_list_optional_progress(
10605        &mut self,
10606    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
10607        let cluster = self.parse_assign_expr()?;
10608        let block = self.parse_block_or_bareword_block()?;
10609        self.eat(&Token::Comma);
10610        let line = self.peek_line();
10611        if let Token::Ident(ref kw) = self.peek().clone() {
10612            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10613                self.advance();
10614                self.expect(&Token::FatArrow)?;
10615                let prog = self.parse_assign_expr_stop_at_pipe()?;
10616                return Ok((
10617                    cluster,
10618                    block,
10619                    Expr {
10620                        kind: ExprKind::List(vec![]),
10621                        line,
10622                    },
10623                    Some(prog),
10624                ));
10625            }
10626        }
10627        let empty_list_ok = matches!(
10628            self.peek(),
10629            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10630        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
10631        if empty_list_ok {
10632            return Ok((
10633                cluster,
10634                block,
10635                Expr {
10636                    kind: ExprKind::List(vec![]),
10637                    line,
10638                },
10639                None,
10640            ));
10641        }
10642        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
10643        loop {
10644            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10645                break;
10646            }
10647            if matches!(
10648                self.peek(),
10649                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10650            ) {
10651                break;
10652            }
10653            if self.peek_is_postfix_stmt_modifier_keyword() {
10654                break;
10655            }
10656            if let Token::Ident(ref kw) = self.peek().clone() {
10657                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10658                    self.advance();
10659                    self.expect(&Token::FatArrow)?;
10660                    let prog = self.parse_assign_expr_stop_at_pipe()?;
10661                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
10662                }
10663            }
10664            parts.push(self.parse_assign_expr_stop_at_pipe()?);
10665        }
10666        Ok((cluster, block, merge_expr_list(parts), None))
10667    }
10668
10669    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
10670    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
10671    ///
10672    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
10673    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
10674    /// stage — individual list parts and the progress value parse through
10675    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
10676    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
10677    fn parse_block_then_list_optional_progress(
10678        &mut self,
10679    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
10680        let block = self.parse_block_or_bareword_block()?;
10681        self.eat(&Token::Comma);
10682        let line = self.peek_line();
10683        if let Token::Ident(ref kw) = self.peek().clone() {
10684            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10685                self.advance();
10686                self.expect(&Token::FatArrow)?;
10687                let prog = self.parse_assign_expr_stop_at_pipe()?;
10688                return Ok((
10689                    block,
10690                    Expr {
10691                        kind: ExprKind::List(vec![]),
10692                        line,
10693                    },
10694                    Some(prog),
10695                ));
10696            }
10697        }
10698        // An empty list operand is allowed when the next token terminates the
10699        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
10700        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
10701        // terminator — left-associative chaining leaves the outer `|>` for
10702        // the enclosing pipe-forward loop.
10703        let empty_list_ok = matches!(
10704            self.peek(),
10705            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10706        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
10707        if empty_list_ok {
10708            return Ok((
10709                block,
10710                Expr {
10711                    kind: ExprKind::List(vec![]),
10712                    line,
10713                },
10714                None,
10715            ));
10716        }
10717        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
10718        loop {
10719            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10720                break;
10721            }
10722            if matches!(
10723                self.peek(),
10724                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10725            ) {
10726                break;
10727            }
10728            if self.peek_is_postfix_stmt_modifier_keyword() {
10729                break;
10730            }
10731            if let Token::Ident(ref kw) = self.peek().clone() {
10732                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10733                    self.advance();
10734                    self.expect(&Token::FatArrow)?;
10735                    let prog = self.parse_assign_expr_stop_at_pipe()?;
10736                    return Ok((block, merge_expr_list(parts), Some(prog)));
10737                }
10738            }
10739            parts.push(self.parse_assign_expr_stop_at_pipe()?);
10740        }
10741        Ok((block, merge_expr_list(parts), None))
10742    }
10743
10744    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
10745    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
10746        // `fan { BLOCK }` — no count
10747        if matches!(self.peek(), Token::LBrace) {
10748            let block = self.parse_block()?;
10749            return Ok((None, block));
10750        }
10751        let saved = self.pos;
10752        // Not a brace — first expr could be count or body
10753        let first = self.parse_postfix()?;
10754        if matches!(self.peek(), Token::LBrace) {
10755            // `fan COUNT { BLOCK }`
10756            let block = self.parse_block()?;
10757            Ok((Some(Box::new(first)), block))
10758        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
10759            || (matches!(self.peek(), Token::Comma)
10760                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
10761        {
10762            // `fan EXPR;` — no count, first is the body
10763            let block = self.bareword_to_no_arg_block(first);
10764            Ok((None, block))
10765        } else if matches!(first.kind, ExprKind::Integer(_)) {
10766            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
10767            self.eat(&Token::Comma);
10768            let body = self.parse_fan_blockless_body(line)?;
10769            Ok((Some(Box::new(first)), body))
10770        } else {
10771            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
10772            // — backtrack and re-parse as a full body expression.
10773            self.pos = saved;
10774            let body = self.parse_fan_blockless_body(line)?;
10775            Ok((None, body))
10776        }
10777    }
10778
10779    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
10780    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
10781        if matches!(self.peek(), Token::LBrace) {
10782            return self.parse_block();
10783        }
10784        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
10785        if let Token::Ident(ref name) = self.peek().clone() {
10786            if matches!(
10787                self.peek_at(1),
10788                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
10789            ) {
10790                let name = name.clone();
10791                self.advance();
10792                let body = Expr {
10793                    kind: ExprKind::FuncCall { name, args: vec![] },
10794                    line,
10795                };
10796                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
10797            }
10798        }
10799        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
10800        let expr = self.parse_assign_expr_stop_at_pipe()?;
10801        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
10802    }
10803
10804    /// Wrap a parsed expression as a single-statement block, converting bare
10805    /// identifiers to zero-arg calls (`work` → `work()`).
10806    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
10807        let line = expr.line;
10808        let body = match &expr.kind {
10809            ExprKind::Bareword(name) => Expr {
10810                kind: ExprKind::FuncCall {
10811                    name: name.clone(),
10812                    args: vec![],
10813                },
10814                line,
10815            },
10816            _ => expr,
10817        };
10818        vec![Statement::new(StmtKind::Expression(body), line)]
10819    }
10820
10821    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
10822    ///
10823    /// When the next token is `{`, delegates to [`Self::parse_block`].
10824    /// Otherwise parses a single postfix expression and wraps it as a call
10825    /// with `$_` as argument (for barewords) or a plain expression statement:
10826    ///
10827    /// - Bareword `foo` → `{ foo($_) }`
10828    /// - Other expr     → `{ EXPR }`
10829    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
10830        if matches!(self.peek(), Token::LBrace) {
10831            return self.parse_block();
10832        }
10833        let line = self.peek_line();
10834        // A lone identifier followed by a list-terminator is a bare sub name:
10835        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
10836        if let Token::Ident(ref name) = self.peek().clone() {
10837            if matches!(
10838                self.peek_at(1),
10839                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
10840            ) {
10841                let name = name.clone();
10842                self.advance();
10843                let body = Expr {
10844                    kind: ExprKind::FuncCall {
10845                        name,
10846                        args: vec![Expr {
10847                            kind: ExprKind::ScalarVar("_".to_string()),
10848                            line,
10849                        }],
10850                    },
10851                    line,
10852                };
10853                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
10854            }
10855        }
10856        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
10857        let expr = self.parse_assign_expr_stop_at_pipe()?;
10858        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
10859    }
10860
10861    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
10862    /// bare function takes no args (body runs stand-alone, not per-element).
10863    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
10864    /// greedily swallow subsequent tokens as function arguments.
10865    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
10866        if matches!(self.peek(), Token::LBrace) {
10867            return self.parse_block();
10868        }
10869        let line = self.peek_line();
10870        if let Token::Ident(ref name) = self.peek().clone() {
10871            if matches!(
10872                self.peek_at(1),
10873                Token::Comma
10874                    | Token::Semicolon
10875                    | Token::RBrace
10876                    | Token::Eof
10877                    | Token::PipeForward
10878                    | Token::Integer(_)
10879            ) {
10880                let name = name.clone();
10881                self.advance();
10882                let body = Expr {
10883                    kind: ExprKind::FuncCall { name, args: vec![] },
10884                    line,
10885                };
10886                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
10887            }
10888        }
10889        let expr = self.parse_postfix()?;
10890        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
10891    }
10892
10893    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
10894    /// treated as a bare sub name (e.g. inside `sort`).
10895    /// True for any bareword the parser treats as a known builtin / keyword —
10896    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
10897    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
10898    /// as a comparator name if it *isn't* a known bareword). Previously named
10899    /// `is_perl_keyword`, which was misleading.
10900    fn is_known_bareword(name: &str) -> bool {
10901        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
10902    }
10903
10904    /// True iff `name` appears as any spelling (primary *or* alias) in a
10905    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
10906    /// up in the parser-level keyword lists but are still callable at
10907    /// runtime — so `map { tj }` can default to `tj($_)` the same way
10908    /// `map { to_json }` does.
10909    fn is_try_builtin_name(name: &str) -> bool {
10910        crate::builtins::BUILTIN_ARMS
10911            .iter()
10912            .any(|arm| arm.contains(&name))
10913    }
10914
10915    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
10916    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
10917    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
10918    /// is derived from this list by `build.rs`.
10919    fn is_perl5_core(name: &str) -> bool {
10920        matches!(
10921            name,
10922            // ── array / list ────────────────────────────────────────────
10923            "map" | "grep" | "sort" | "reverse" | "join" | "split"
10924            | "push" | "pop" | "shift" | "unshift" | "splice"
10925            | "pack" | "unpack"
10926            // ── hash ────────────────────────────────────────────────────
10927            | "keys" | "values" | "each"
10928            // ── string ──────────────────────────────────────────────────
10929            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
10930            | "lc" | "uc" | "lcfirst" | "ucfirst"
10931            | "length" | "substr" | "index" | "rindex"
10932            | "sprintf" | "printf" | "print" | "say"
10933            | "pos" | "quotemeta" | "study"
10934            // ── numeric ─────────────────────────────────────────────────
10935            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
10936            | "exp" | "log" | "rand" | "srand"
10937            // ── time ────────────────────────────────────────────────────
10938            | "time" | "localtime" | "gmtime"
10939            // ── type / reflection ───────────────────────────────────────
10940            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
10941            | "caller" | "delete" | "exists" | "bless" | "prototype"
10942            | "tie" | "untie" | "tied"
10943            // ── io ──────────────────────────────────────────────────────
10944            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
10945            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
10946            | "format" | "formline" | "select" | "vec"
10947            | "sysopen" | "sysread" | "sysseek" | "syswrite"
10948            // ── filesystem ──────────────────────────────────────────────
10949            | "stat" | "lstat" | "rename" | "unlink" | "utime"
10950            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
10951            | "glob" | "opendir" | "readdir" | "closedir"
10952            | "link" | "readlink" | "symlink"
10953            // ── ipc ─────────────────────────────────────────────────────
10954            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
10955            // ── sysv ipc ────────────────────────────────────────────────
10956            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
10957            | "semctl" | "semget" | "semop"
10958            | "shmctl" | "shmget" | "shmread" | "shmwrite"
10959            // ── process / system ────────────────────────────────────────
10960            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
10961            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
10962            | "chroot" | "times" | "umask" | "reset"
10963            | "getpgrp" | "setpgrp" | "getppid"
10964            | "getpriority" | "setpriority"
10965            // ── socket ──────────────────────────────────────────────────
10966            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
10967            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
10968            | "getpeername" | "getsockname"
10969            // ── posix metadata ──────────────────────────────────────────
10970            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
10971            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
10972            | "getlogin"
10973            | "gethostbyname" | "gethostbyaddr" | "gethostent"
10974            | "getnetbyname" | "getnetent"
10975            | "getprotobyname" | "getprotoent"
10976            | "getservbyname" | "getservent"
10977            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
10978            | "endpwent" | "endgrent"
10979            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
10980            // ── control flow ────────────────────────────────────────────
10981            | "return" | "do" | "eval" | "require"
10982            | "my" | "our" | "local" | "use" | "no"
10983            | "sub" | "if" | "unless" | "while" | "until"
10984            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
10985            | "not" | "and" | "or"
10986            // ── quoting ─────────────────────────────────────────────────
10987            | "qw" | "qq" | "q"
10988            // ── phase blocks ────────────────────────────────────────────
10989            | "BEGIN" | "END"
10990        )
10991    }
10992
10993    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
10994    /// Used by `--compat` to reject extensions at parse time.
10995    fn stryke_extension_name(name: &str) -> Option<&str> {
10996        match name {
10997            // ── parallel ────────────────────────────────────────────────────
10998            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
10999            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
11000            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
11001            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
11002            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
11003            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
11004            // ── functional / iterator ───────────────────────────────────────
11005            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "f" | "find_all" | "reduce" | "fold"
11006            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
11007            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "flatten" | "set"
11008            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
11009            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
11010            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
11011            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
11012            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
11013            | "zip_with" | "count_by" | "skip" | "first_or"
11014            // ── pipeline / string helpers ───────────────────────────────────
11015            | "input" | "lines" | "words" | "chars" | "digits" | "sentences" | "sents"
11016            | "paragraphs" | "paras" | "sections" | "sects"
11017            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
11018            | "trim" | "avg" | "stddev"
11019            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
11020            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
11021            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
11022            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
11023            | "to_html" | "to_markdown" | "to_table" | "xopen"
11024            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste"
11025            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
11026            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
11027            | "to_hash" | "to_set"
11028            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
11029            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
11030            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
11031            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
11032            | "inc" | "dec" | "elapsed"
11033            // ── filesystem extensions ───────────────────────────────────────
11034            | "files" | "filesf" | "fr" | "dirs" | "d" | "dr" | "sym_links"
11035            | "sockets" | "pipes" | "block_devices" | "char_devices"
11036            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
11037            | "copy" | "move" | "spurt" | "read_bytes" | "which"
11038            | "getcwd" | "touch" | "gethostname" | "uname"
11039            // ── data / network ──────────────────────────────────────────────
11040            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
11041            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
11042            | "par_fetch" | "par_csv_read" | "par_pipeline"
11043            | "json_encode" | "json_decode" | "json_jq"
11044            | "http_request" | "serve" | "ssh"
11045            // ── serialization (stryke-only encoders) ────────────────────────
11046            | "toml_encode" | "toml_decode"
11047            | "yaml_encode" | "yaml_decode"
11048            | "xml_encode" | "xml_decode"
11049            // ── crypto / encoding ───────────────────────────────────────────
11050            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
11051            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
11052            | "shake128" | "shake256"
11053            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
11054            | "uuid" | "crc32"
11055            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
11056            | "ripemd160" | "rmd160" | "md4"
11057            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
11058            | "murmur3" | "murmur3_32" | "murmur3_128"
11059            | "siphash" | "siphash_keyed"
11060            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
11061            | "poly1305" | "poly1305_mac"
11062            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
11063            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
11064            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
11065            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
11066            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
11067            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
11068            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
11069            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
11070            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
11071            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
11072            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
11073            | "secretbox" | "secretbox_seal" | "secretbox_open"
11074            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
11075            | "nacl_box_open" | "box_open"
11076            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
11077            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
11078            | "barcode_ean13" | "ean13" | "barcode_svg"
11079            | "argon2_hash" | "argon2" | "argon2_verify"
11080            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
11081            | "scrypt_hash" | "scrypt" | "scrypt_verify"
11082            | "pbkdf2" | "pbkdf2_derive"
11083            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
11084            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
11085            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
11086            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
11087            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
11088            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
11089            | "ecdsa_p256_verify" | "p256_verify"
11090            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
11091            | "ecdsa_p384_verify" | "p384_verify"
11092            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
11093            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
11094            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
11095            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
11096            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
11097            | "ed25519_verify" | "ed_verify"
11098            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
11099            | "base64_encode" | "base64_decode"
11100            | "hex_encode" | "hex_decode"
11101            | "url_encode" | "url_decode"
11102            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
11103            | "brotli" | "br" | "brotli_decode" | "ubr"
11104            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
11105            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
11106            | "lz4" | "lz4_decode" | "unlz4"
11107            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
11108            | "lzw" | "lzw_decode" | "unlzw"
11109            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
11110            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
11111            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
11112            // ── special math functions ────────────────────────────────────────
11113            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
11114            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
11115            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
11116            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
11117            | "gammaincc_reg" | "gamma_ur"
11118            // ── date / time ─────────────────────────────────────────────────
11119            | "datetime_utc" | "datetime_now_tz"
11120            | "datetime_format_tz" | "datetime_add_seconds"
11121            | "datetime_from_epoch"
11122            | "datetime_parse_rfc3339" | "datetime_parse_local"
11123            | "datetime_strftime"
11124            // ── jwt ─────────────────────────────────────────────────────────
11125            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
11126            // ── logging ─────────────────────────────────────────────────────
11127            | "log_info" | "log_warn" | "log_error"
11128            | "log_debug" | "log_trace" | "log_json" | "log_level"
11129            // ── concurrency / timing ────────────────────────────────────────
11130            | "async" | "spawn" | "trace" | "timer" | "bench"
11131            | "eval_timeout" | "retry" | "rate_limit" | "every"
11132            | "gen" | "watch"
11133            // ── I/O extensions ──────────────────────────────────────────────
11134            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
11135            | "stdin"
11136            // ── internal ────────────────────────────────────────────────────
11137            | "__stryke_rust_compile"
11138            // ── short aliases ───────────────────────────────────────────────
11139            | "p" | "rev"
11140            // ── trivial numeric / predicate builtins ────────────────────────
11141            | "even" | "odd" | "zero" | "nonzero"
11142            | "positive" | "pos_n" | "negative" | "neg_n"
11143            | "sign" | "negate" | "double" | "triple" | "half"
11144            | "identity" | "id"
11145            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
11146            | "gcd" | "lcm" | "min2" | "max2"
11147            | "log2" | "log10" | "hypot"
11148            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
11149            | "pow2" | "abs_diff"
11150            | "factorial" | "fact" | "fibonacci" | "fib"
11151            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
11152            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
11153            | "median" | "mode_val" | "variance"
11154            // ── trivial string ops ──────────────────────────────────────────
11155            | "is_empty" | "is_blank" | "is_numeric"
11156            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
11157            | "is_space" | "is_whitespace"
11158            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
11159            | "capitalize" | "cap" | "swap_case" | "repeat"
11160            | "title_case" | "title" | "squish"
11161            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
11162            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
11163            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
11164            // ── trivial type predicates ─────────────────────────────────────
11165            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
11166            | "is_code" | "is_coderef" | "is_ref"
11167            | "is_undef" | "is_defined" | "is_def"
11168            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
11169            // ── hash helpers ────────────────────────────────────────────────
11170            | "invert" | "merge_hash"
11171            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
11172            // ── boolean combinators ─────────────────────────────────────────
11173            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
11174            // ── collection helpers (trivial) ────────────────────────────────
11175            | "riffle" | "intersperse" | "every_nth"
11176            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
11177            // ── base conversion ─────────────────────────────────────────────
11178            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
11179            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
11180            | "bits_count" | "popcount" | "leading_zeros" | "lz"
11181            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
11182            // ── bit ops ─────────────────────────────────────────────────────
11183            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
11184            | "shift_left" | "shl" | "shift_right" | "shr"
11185            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
11186            // ── unit conversions: temperature ───────────────────────────────
11187            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
11188            // ── unit conversions: distance ──────────────────────────────────
11189            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
11190            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
11191            | "yards_to_m" | "m_to_yards"
11192            // ── unit conversions: mass ──────────────────────────────────────
11193            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
11194            | "stone_to_kg" | "kg_to_stone"
11195            // ── unit conversions: digital ───────────────────────────────────
11196            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
11197            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
11198            | "kb_to_mb" | "mb_to_gb"
11199            | "bits_to_bytes" | "bytes_to_bits"
11200            // ── unit conversions: time ──────────────────────────────────────
11201            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
11202            | "seconds_to_hours" | "hours_to_seconds"
11203            | "seconds_to_days" | "days_to_seconds"
11204            | "minutes_to_hours" | "hours_to_minutes"
11205            | "hours_to_days" | "days_to_hours"
11206            // ── date helpers ────────────────────────────────────────────────
11207            | "is_leap_year" | "is_leap" | "days_in_month"
11208            | "month_name" | "month_short"
11209            | "weekday_name" | "weekday_short" | "quarter_of"
11210            // ── now / timestamp ─────────────────────────────────────────────
11211            | "now_ms" | "now_us" | "now_ns"
11212            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
11213            // ── color / ANSI ────────────────────────────────────────────────
11214            | "rgb_to_hex" | "hex_to_rgb"
11215            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
11216            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
11217            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
11218            | "strip_ansi"
11219            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
11220            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
11221            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
11222            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
11223            | "bright_magenta" | "bright_cyan" | "bright_white"
11224            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
11225            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
11226            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
11227            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
11228            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
11229            | "white_bold" | "bold_white"
11230            | "blink" | "rapid_blink" | "hidden" | "overline"
11231            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
11232            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
11233            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
11234            // ── network / validation ────────────────────────────────────────
11235            | "ipv4_to_int" | "int_to_ipv4"
11236            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
11237            // ── path helpers ────────────────────────────────────────────────
11238            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
11239            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
11240            // ── functional primitives ───────────────────────────────────────
11241            | "const_fn" | "always_true" | "always_false"
11242            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
11243            // ── more list helpers ───────────────────────────────────────────
11244            | "count_eq" | "count_ne" | "all_eq"
11245            | "all_distinct" | "all_unique" | "has_duplicates"
11246            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
11247            // ── string quote / escape ───────────────────────────────────────
11248            | "quote" | "single_quote" | "unquote"
11249            | "extract_between" | "ellipsis"
11250            // ── random ──────────────────────────────────────────────────────
11251            | "coin_flip" | "dice_roll"
11252            | "random_int" | "random_float" | "random_bool"
11253            | "random_choice" | "random_between"
11254            | "random_string" | "random_alpha" | "random_digit"
11255            // ── system introspection ────────────────────────────────────────
11256            | "os_name" | "os_arch" | "num_cpus"
11257            | "pid" | "ppid" | "uid" | "gid"
11258            | "username" | "home_dir" | "temp_dir"
11259            | "mem_total" | "mem_free" | "mem_used"
11260            | "swap_total" | "swap_free" | "swap_used"
11261            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
11262            | "load_avg" | "sys_uptime" | "page_size"
11263            | "os_version" | "os_family" | "endianness" | "pointer_width"
11264            | "proc_mem" | "rss"
11265            // ── collection more ─────────────────────────────────────────────
11266            | "transpose" | "unzip"
11267            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
11268            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
11269            // ── trig / math (batch 2) ───────────────────────────────────────
11270            | "tan" | "asin" | "acos" | "atan"
11271            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
11272            | "sqr" | "cube_fn"
11273            | "mod_op" | "ceil_div" | "floor_div"
11274            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
11275            | "degrees" | "radians"
11276            | "min_abs" | "max_abs"
11277            | "saturate" | "sat01" | "wrap_around"
11278            // ── string (batch 2) ────────────────────────────────────────────
11279            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
11280            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
11281            | "first_word" | "last_word"
11282            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
11283            | "lowercase" | "uppercase"
11284            | "pascal_case" | "pc_case"
11285            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
11286            | "is_palindrome" | "hamming_distance"
11287            | "longest_common_prefix" | "lcp"
11288            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
11289            | "replace_first" | "replace_all_str"
11290            | "contains_any" | "contains_all"
11291            | "starts_with_any" | "ends_with_any"
11292            // ── predicates (batch 2) ────────────────────────────────────────
11293            | "is_pair" | "is_triple"
11294            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
11295            | "is_empty_arr" | "is_empty_hash"
11296            | "is_subset" | "is_superset" | "is_permutation"
11297            // ── collection (batch 2) ────────────────────────────────────────
11298            | "first_eq" | "last_eq"
11299            | "index_of" | "last_index_of" | "positions_of"
11300            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
11301            | "distinct_count" | "longest" | "shortest"
11302            | "array_union" | "list_union"
11303            | "array_intersection" | "list_intersection"
11304            | "array_difference" | "list_difference"
11305            | "symmetric_diff" | "group_of_n" | "chunk_n"
11306            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
11307            // ── hash ops (batch 2) ──────────────────────────────────────────
11308            | "pick_keys" | "pick" | "omit_keys" | "omit"
11309            | "map_keys_fn" | "map_values_fn"
11310            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
11311            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
11312            // ── date (batch 2) ──────────────────────────────────────────────
11313            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
11314            // ── json helpers ────────────────────────────────────────────────
11315            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
11316            // ── process / env ───────────────────────────────────────────────
11317            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
11318            | "argc" | "script_name"
11319            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
11320            // ── id helpers ──────────────────────────────────────────────────
11321            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
11322            // ── url / email parts ───────────────────────────────────────────
11323            | "email_domain" | "email_local"
11324            | "url_host" | "url_path" | "url_query" | "url_scheme"
11325            // ── file stat / path ────────────────────────────────────────────
11326            | "file_size" | "fsize" | "file_mtime" | "mtime"
11327            | "file_atime" | "atime" | "file_ctime" | "ctime"
11328            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
11329            | "path_is_abs" | "path_is_rel"
11330            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
11331            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
11332            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
11333            | "reverse_list" | "list_reverse"
11334            | "without" | "without_nth" | "take_last" | "drop_last"
11335            | "pairwise" | "zipmap"
11336            | "format_bytes" | "human_bytes"
11337            | "format_duration" | "human_duration"
11338            | "format_number" | "group_number"
11339            | "format_percent" | "pad_number"
11340            | "spaceship" | "cmp_num" | "cmp_str"
11341            | "compare_versions" | "version_cmp"
11342            | "hash_insert" | "hash_update" | "hash_delete"
11343            | "matches_regex" | "re_match"
11344            | "count_regex_matches" | "regex_extract"
11345            | "regex_split_str" | "regex_replace_str"
11346            | "shuffle_chars" | "random_char" | "nth_word"
11347            | "head_lines" | "tail_lines" | "count_substring"
11348            | "is_valid_hex" | "hex_upper" | "hex_lower"
11349            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
11350            | "us_to_ns" | "ns_to_us"
11351            | "liters_to_gallons" | "gallons_to_liters"
11352            | "liters_to_ml" | "ml_to_liters"
11353            | "cups_to_ml" | "ml_to_cups"
11354            | "newtons_to_lbf" | "lbf_to_newtons"
11355            | "joules_to_cal" | "cal_to_joules"
11356            | "watts_to_hp" | "hp_to_watts"
11357            | "pascals_to_psi" | "psi_to_pascals"
11358            | "bar_to_pascals" | "pascals_to_bar"
11359            // ── algebraic match ─────────────────────────────────────────────
11360            | "match"
11361            // ── clojure stdlib (only names not matched above) ─────────────────
11362            | "fst" | "rest" | "rst" | "second" | "snd"
11363            | "last_clj" | "lastc" | "butlast" | "bl"
11364            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
11365            | "cons" | "conj"
11366            | "peek_clj" | "pkc" | "pop_clj" | "popc"
11367            | "some" | "not_any" | "not_every"
11368            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
11369            | "fnil" | "juxt"
11370            | "memoize" | "memo" | "curry" | "once"
11371            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
11372            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
11373            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
11374            | "reductions" | "rdcs"
11375            | "partition_by" | "pby" | "partition_all" | "pall"
11376            | "split_at" | "spat" | "split_with" | "spw"
11377            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
11378            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
11379            | "apply" | "appl"
11380            // ── python/ruby stdlib ───────────────────────────────────────────
11381            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
11382            | "zip_longest" | "zipl" | "combinations" | "comb" | "permutations" | "perm"
11383            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
11384            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
11385            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
11386            | "each_slice" | "eslice" | "each_cons" | "econs"
11387            | "one" | "none_match" | "nonem"
11388            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
11389            | "minmax" | "mmx" | "minmax_by" | "mmxb"
11390            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
11391            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
11392            | "sum_by" | "sumb" | "uniq_by" | "uqb"
11393            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
11394            | "step" | "upto" | "downto"
11395            // ── javascript array/object methods ─────────────────────────────
11396            | "find_last" | "fndl" | "find_last_index" | "fndli"
11397            | "at_index" | "ati" | "replace_at" | "repa"
11398            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
11399            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
11400            | "object_keys" | "okeys" | "object_values" | "ovals"
11401            | "object_entries" | "oents" | "object_from_entries" | "ofents"
11402            // ── haskell list functions ──────────────────────────────────────
11403            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
11404            | "nub" | "sort_on" | "srton"
11405            | "intersperse_val" | "isp" | "intercalate" | "ical"
11406            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
11407            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
11408            // ── rust iterator methods ───────────────────────────────────────
11409            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
11410            | "partition_either" | "peith" | "try_fold" | "tfld"
11411            | "map_while" | "mapw" | "inspect" | "insp"
11412            // ── ruby enumerable extras ──────────────────────────────────────
11413            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
11414            // ── go/general functional utilities ─────────────────────────────
11415            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
11416            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
11417            | "lines_from" | "lfrm" | "unlines" | "unlns"
11418            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
11419            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
11420            | "interpose" | "ipos" | "partition_n" | "partn"
11421            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
11422            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
11423            // ── additional missing stdlib functions ─────────────────────────
11424            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
11425            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
11426            | "each_with_object" | "ewo" | "reduce_right" | "redr"
11427            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
11428            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
11429            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
11430            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
11431            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
11432            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
11433            | "union_list" | "unionl" | "intersect_list" | "intl"
11434            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
11435            // ── Extended stdlib: Text Processing ─────────────────────────────
11436            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
11437            | "split_regex" | "splre" | "replace_regex" | "replre"
11438            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
11439            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
11440            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
11441            | "pluralize" | "plur" | "ordinalize" | "ordn"
11442            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
11443            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
11444            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
11445            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
11446            // ── Extended stdlib: Advanced Numeric ────────────────────────────
11447            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
11448            | "dot_product" | "dotp" | "cross_product" | "crossp"
11449            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
11450            | "distance" | "dist" | "manhattan_distance" | "mdist"
11451            | "covariance" | "cov" | "correlation" | "corr"
11452            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
11453            | "in_range" | "inrng" | "wrap_range" | "wrprng"
11454            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
11455            // ── Extended stdlib: Date/Time ───────────────────────────────────
11456            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
11457            | "diff_days" | "diffd" | "diff_hours" | "diffh"
11458            | "start_of_day" | "sod" | "end_of_day" | "eod"
11459            | "start_of_hour" | "soh" | "start_of_minute" | "som"
11460            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
11461            | "urle" | "urld"
11462            | "html_encode" | "htmle" | "html_decode" | "htmld"
11463            | "adler32" | "adl32" | "fnv1a" | "djb2"
11464            // ── Extended stdlib: Validation ──────────────────────────────────
11465            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
11466            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
11467            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
11468            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
11469            // ── Extended stdlib: Collection Advanced ─────────────────────────
11470            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
11471            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
11472            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
11473            | "partition_point" | "ppt" | "lower_bound" | "lbound"
11474            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
11475            // ── Extended stdlib: Matrix Operations ───────────────────────────
11476            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
11477            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
11478            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
11479            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
11480            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
11481            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
11482            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
11483            // ── Extended stdlib: Graph Algorithms ────────────────────────────
11484            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
11485            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
11486            | "connected_components_graph" | "ccgraph"
11487            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
11488            // ── Extended stdlib: Data Validation ─────────────────────────────
11489            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
11490            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
11491            | "is_hostname_valid" | "ishost"
11492            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
11493            | "is_iso_datetime" | "isisodtm"
11494            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
11495            // ── Extended stdlib: String Utilities Novel ──────────────────────
11496            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
11497            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
11498            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
11499            | "find_all_indices" | "fndalli"
11500            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
11501            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
11502            // ── Extended stdlib: Math Novel ──────────────────────────────────
11503            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
11504            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
11505            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
11506            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
11507            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
11508            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
11509            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
11510            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
11511            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
11512            | "longest_run" | "lrun" | "longest_increasing" | "linc"
11513            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
11514            | "majority_element" | "majority" | "kth_largest" | "kthl"
11515            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
11516            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
11517            // ── Extended stdlib batch 3: Set Operations ──────────────────────
11518            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
11519            | "overlap_coefficient" | "overlapcoef"
11520            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
11521            // ── Extended stdlib batch 3: Advanced String ─────────────────────
11522            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
11523            | "hamdist" | "jaro_similarity" | "jarosim"
11524            | "longest_common_substring" | "lcsub"
11525            | "longest_common_subsequence" | "lcseq"
11526            | "count_words" | "wcount" | "count_lines" | "lcount"
11527            | "count_chars" | "ccount" | "count_bytes" | "bcount"
11528            // ── Extended stdlib batch 3: More Math ───────────────────────────
11529            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
11530            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
11531            | "mobius" | "mob" | "is_squarefree" | "issqfr"
11532            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
11533            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
11534            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
11535            | "day_of_year" | "doy" | "week_of_year" | "woy"
11536            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
11537            | "age_in_years" | "ageyrs"
11538            // ── functional combinators ──────────────────────────────────────
11539
11540            | "when_true" | "when_false" | "if_else" | "clamp_fn"
11541            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
11542            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
11543            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
11544            | "coalesce" | "default_to" | "fallback"
11545            | "apply_list" | "zip_apply" | "scan"
11546            | "keep_if" | "reject_if" | "group_consecutive"
11547            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
11548
11549            // ── matrix / linear algebra ─────────────────────────────────────
11550
11551
11552            | "matrix_multiply" | "mat_mul"
11553            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
11554
11555
11556
11557            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
11558            | "linspace" | "arange"
11559            // ── more regex ──────────────────────────────────────────────────
11560            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
11561            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
11562            // ── more process / system ───────────────────────────────────────
11563            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
11564            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
11565            // ── data structure helpers ───────────────────────────────────────
11566            | "stack_new" | "queue_new" | "lru_new"
11567            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
11568            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
11569            // ── trivial numeric helpers (batch 4) ─────────────────────────────
11570            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
11571            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
11572            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
11573            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
11574            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
11575            // ── math / physics constants ──────────────────────────────────────
11576            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
11577            | "planck" | "speed_of_light" | "sqrt2"
11578            // ── physics formulas ──────────────────────────────────────────────
11579            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
11580            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
11581            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
11582            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
11583            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
11584            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
11585            // ── math functions ────────────────────────────────────────────────
11586            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
11587            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
11588            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
11589            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
11590            | "sigmoid" | "signum" | "square_root"
11591            // ── sequences ─────────────────────────────────────────────────────
11592            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
11593            | "squares_seq" | "triangular_seq"
11594            // ── string helpers (batch 4) ──────────────────────────────────────
11595            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
11596            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
11597            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
11598            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
11599            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
11600            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
11601            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
11602            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
11603            | "xor_strings"
11604            // ── list helpers (batch 4) ─────────────────────────────────────────
11605            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
11606            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
11607            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
11608            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
11609            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
11610            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
11611            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
11612            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
11613            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
11614            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
11615            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
11616            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
11617            | "wrap_index" | "digits_of"
11618            // ── predicates (batch 4) ──────────────────────────────────────────
11619            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
11620            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
11621            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
11622            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
11623            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
11624            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
11625            // ── counters (batch 4) ────────────────────────────────────────────
11626            | "count_digits" | "count_letters" | "count_lower" | "count_match"
11627            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
11628            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
11629            | "truthy_count" | "undef_count"
11630            // ── conversion / utility (batch 4) ────────────────────────────────
11631            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
11632            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
11633            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
11634            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
11635            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
11636            | "range_exclusive" | "range_inclusive"
11637            // ── math / numeric (uncategorized batch) ────────────────────────────
11638            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
11639            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
11640            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
11641            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
11642            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
11643            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
11644            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
11645            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
11646            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
11647            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
11648            | "tribonacci" | "weighted_mean" | "winsorize"
11649            // ── statistics (extended) ─────────────────────────────────────────
11650            | "chi_square_stat" | "describe" | "five_number_summary"
11651            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
11652            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
11653            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
11654            | "z_score" | "z_scores"
11655            // ── number theory / primes ──────────────────────────────────────────
11656            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
11657            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
11658            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
11659            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
11660            // ── geometry / physics ──────────────────────────────────────────────
11661            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
11662            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
11663            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
11664            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
11665            // ── geometry (extended) ───────────────────────────────────────────
11666            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
11667            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
11668            | "frustum_volume" | "haversine_distance" | "line_intersection"
11669            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
11670            | "reflect_point" | "scale_point" | "sector_area"
11671            | "torus_surface" | "torus_volume" | "translate_point"
11672            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
11673            // ── constants ───────────────────────────────────────────────────────
11674            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
11675            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
11676            | "sol" | "tau"
11677            // ── finance ─────────────────────────────────────────────────────────
11678            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
11679            // ── finance (extended) ────────────────────────────────────────────
11680            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
11681            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
11682            | "discounted_payback" | "duration" | "irr"
11683            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
11684            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
11685            | "wacc" | "xirr"
11686            // ── string processing (uncategorized batch) ─────────────────────────
11687            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
11688            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
11689            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
11690            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
11691            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
11692            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
11693            // ── encoding / phonetics ────────────────────────────────────────────
11694            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
11695            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
11696            | "to_emoji_num"
11697            // ── roman numerals ──────────────────────────────────────────────────
11698            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
11699            // ── base / gray code ────────────────────────────────────────────────
11700            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
11701            // ── color operations ────────────────────────────────────────────────
11702            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
11703            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
11704            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
11705            | "rgb_to_hsl" | "rgb_to_hsv"
11706            // ── matrix operations (uncategorized batch) ─────────────────────────
11707            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
11708            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
11709            | "matrix_transpose"
11710            // ── array / list operations (uncategorized batch) ───────────────────
11711            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
11712            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
11713            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
11714            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
11715            | "zero_crossings"
11716            // ── DSP / signal (extended) ───────────────────────────────────────
11717            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
11718            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
11719            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
11720            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
11721            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
11722            // ── validation predicates (uncategorized batch) ─────────────────────
11723            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
11724            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
11725            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
11726            // ── algorithms / puzzles ────────────────────────────────────────────
11727            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
11728            | "sierpinski" | "tower_of_hanoi" | "truth_table"
11729            // ── misc / utility ──────────────────────────────────────────────────
11730            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
11731            // ── math formulas ───────────────────────────────────────────────────
11732            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
11733            | "geometric_series" | "stirling_approx"
11734            | "double_factorial" | "rising_factorial" | "falling_factorial"
11735            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
11736            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
11737            | "map_range"
11738            // ── physics formulas ────────────────────────────────────────────────
11739            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
11740            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
11741            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
11742            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
11743            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
11744            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
11745            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
11746            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
11747            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
11748            | "projectile_range" | "projectile_max_height" | "projectile_time"
11749            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
11750            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
11751            | "lens_power" | "thin_lens" | "magnification_lens"
11752            // ── math constants ──────────────────────────────────────────────────
11753            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
11754            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
11755            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
11756            // ── physics constants ───────────────────────────────────────────────
11757            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
11758            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
11759            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
11760            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
11761            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
11762            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
11763            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
11764            // ── linear algebra (extended) ──────────────────────────────────
11765            | "matrix_solve" | "msolve" | "solve"
11766            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
11767            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
11768            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
11769            | "matrix_pinv" | "mpinv" | "pinv"
11770            | "matrix_cholesky" | "mchol" | "cholesky"
11771            | "matrix_det_general" | "mdetg" | "det"
11772            // ── statistics tests (extended) ────────────────────────────────
11773            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
11774            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
11775            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
11776            | "confidence_interval" | "ci"
11777            // ── distributions (extended) ──────────────────────────────────
11778            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
11779            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
11780            | "t_pdf" | "tpdf" | "student_pdf"
11781            | "f_pdf" | "fpdf" | "fisher_pdf"
11782            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
11783            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
11784            | "pareto_pdf" | "paretopdf"
11785            // ── interpolation & curve fitting ─────────────────────────────
11786            | "lagrange_interp" | "lagrange" | "linterp"
11787            | "cubic_spline" | "cspline" | "spline"
11788            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
11789            // ── numerical integration & differentiation ───────────────────
11790            | "trapz" | "trapezoid" | "simpson" | "simps"
11791            | "numerical_diff" | "numdiff" | "diff_array"
11792            | "cumtrapz" | "cumulative_trapz"
11793            // ── optimization / root finding ────────────────────────────────
11794            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
11795            | "golden_section" | "golden" | "gss"
11796            // ── ODE solvers ───────────────────────────────────────────────
11797            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
11798            // ── graph algorithms (extended) ────────────────────────────────
11799            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
11800            | "floyd_warshall" | "floydwarshall" | "apsp"
11801            | "prim_mst" | "mst" | "prim"
11802            // ── trig extensions ───────────────────────────────────────────
11803            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
11804            // ── ML activation functions ───────────────────────────────────
11805            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
11806            | "silu" | "swish" | "mish" | "softplus"
11807            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
11808            // ── special functions ─────────────────────────────────────────
11809            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
11810            | "lambert_w" | "lambertw" | "productlog"
11811            // ── number theory (extended) ──────────────────────────────────
11812            | "mod_exp" | "modexp" | "powmod"
11813            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
11814            | "miller_rabin" | "millerrabin" | "is_probable_prime"
11815            // ── combinatorics (extended) ──────────────────────────────────
11816            | "derangements" | "stirling2" | "stirling_second"
11817            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
11818            // ── physics (new) ─────────────────────────────────────────────
11819            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
11820            // ── financial greeks & risk ───────────────────────────────────
11821            | "bs_delta" | "bsdelta" | "option_delta"
11822            | "bs_gamma" | "bsgamma" | "option_gamma"
11823            | "bs_vega" | "bsvega" | "option_vega"
11824            | "bs_theta" | "bstheta" | "option_theta"
11825            | "bs_rho" | "bsrho" | "option_rho"
11826            | "bond_duration" | "mac_duration"
11827            // ── DSP extensions ────────────────────────────────────────────
11828            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
11829            // ── encoding extensions ───────────────────────────────────────
11830            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
11831            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
11832            // ── R base: distributions ─────────────────────────────────────
11833            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
11834            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
11835            // ── R base: matrix ops ────────────────────────────────────────
11836            | "rbind" | "cbind"
11837            | "row_sums" | "rowSums" | "col_sums" | "colSums"
11838            | "row_means" | "rowMeans" | "col_means" | "colMeans"
11839            | "outer_product" | "outer" | "crossprod" | "tcrossprod"
11840            | "nrow" | "ncol" | "prop_table" | "proptable"
11841            // ── R base: vector ops ────────────────────────────────────────
11842            | "cummax" | "cummin" | "scale_vec" | "scale"
11843            | "which_fn" | "tabulate"
11844            | "duplicated" | "duped" | "rev_vec"
11845            | "seq_fn" | "rep_fn" | "rep"
11846            | "cut_bins" | "cut" | "find_interval" | "findInterval"
11847            | "ecdf_fn" | "ecdf" | "density_est" | "density"
11848            | "embed_ts" | "embed"
11849            // ── R base: stats tests ───────────────────────────────────────
11850            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
11851            | "wilcox_test" | "wilcox" | "mann_whitney"
11852            | "prop_test" | "proptest" | "binom_test" | "binomtest"
11853            // ── R base: apply / functional ────────────────────────────────
11854            | "sapply" | "tapply" | "do_call" | "docall"
11855            // ── R base: ML / clustering ───────────────────────────────────
11856            | "kmeans" | "prcomp" | "pca"
11857            // ── R base: random generators ─────────────────────────────────
11858            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
11859            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
11860            | "rweibull" | "rlnorm" | "rcauchy"
11861            // ── R base: quantile functions ────────────────────────────────
11862            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
11863            // ── R base: additional CDFs ───────────────────────────────────
11864            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
11865            // ── R base: additional PMFs ───────────────────────────────────
11866            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
11867            // ── R base: smoothing / interpolation ─────────────────────────
11868            | "lowess" | "loess" | "approx_fn" | "approx"
11869            // ── R base: linear models ─────────────────────────────────────
11870            | "lm_fit" | "lm"
11871            // ── R base: remaining quantiles ───────────────────────────────
11872            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
11873            | "qbinom" | "qpois"
11874            // ── R base: time series ───────────────────────────────────────
11875            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
11876            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
11877            // ── R base: regression diagnostics ────────────────────────────
11878            | "predict_lm" | "predict" | "confint_lm" | "confint"
11879            // ── R base: multivariate stats ────────────────────────────────
11880            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
11881            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
11882            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
11883            // ── SVG plotting ──────────────────────────────────────────────
11884            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
11885            | "plot_svg" | "hist_svg" | "histogram_svg"
11886            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
11887            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
11888            // ── Cyberpunk terminal art ────────────────────────────────
11889            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
11890            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
11891            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
11892            => Some(name),
11893            _ => None,
11894        }
11895    }
11896
11897    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
11898    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
11899    /// Also accepts a bare function name: `psort my_cmp, @list`.
11900    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
11901        if matches!(self.peek(), Token::LBrace) {
11902            return self.parse_block();
11903        }
11904        let line = self.peek_line();
11905        // Bare sub name: `psort my_cmp, @list`
11906        if let Token::Ident(ref name) = self.peek().clone() {
11907            if matches!(
11908                self.peek_at(1),
11909                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11910            ) {
11911                let name = name.clone();
11912                self.advance();
11913                let body = Expr {
11914                    kind: ExprKind::FuncCall {
11915                        name,
11916                        args: vec![
11917                            Expr {
11918                                kind: ExprKind::ScalarVar("a".to_string()),
11919                                line,
11920                            },
11921                            Expr {
11922                                kind: ExprKind::ScalarVar("b".to_string()),
11923                                line,
11924                            },
11925                        ],
11926                    },
11927                    line,
11928                };
11929                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11930            }
11931        }
11932        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
11933        let expr = self.parse_assign_expr_stop_at_pipe()?;
11934        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11935    }
11936
11937    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
11938    fn parse_fan_optional_progress(
11939        &mut self,
11940        which: &'static str,
11941    ) -> PerlResult<Option<Box<Expr>>> {
11942        let line = self.peek_line();
11943        if self.eat(&Token::Comma) {
11944            match self.peek() {
11945                Token::Ident(ref kw)
11946                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
11947                {
11948                    self.advance();
11949                    self.expect(&Token::FatArrow)?;
11950                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
11951                }
11952                _ => {
11953                    return Err(self.syntax_err(
11954                        format!("{which}: expected `progress => EXPR` after comma"),
11955                        line,
11956                    ));
11957                }
11958            }
11959        }
11960        if let Token::Ident(ref kw) = self.peek().clone() {
11961            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11962                self.advance();
11963                self.expect(&Token::FatArrow)?;
11964                return Ok(Some(Box::new(self.parse_assign_expr()?)));
11965            }
11966        }
11967        Ok(None)
11968    }
11969
11970    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
11971    /// (for `pmap_chunked`, `psort`, etc.).
11972    ///
11973    /// Paren-less — individual parts parse through
11974    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
11975    /// the enclosing pipe-forward loop (left-associative chaining).
11976    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
11977        // On the RHS of `|>`, list-taking builtins may be written bare with no
11978        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
11979        // When the next token is a list-terminator, yield an empty placeholder
11980        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
11981        // desugar time, so the placeholder is never evaluated.
11982        if self.in_pipe_rhs()
11983            && matches!(
11984                self.peek(),
11985                Token::Semicolon
11986                    | Token::RBrace
11987                    | Token::RParen
11988                    | Token::Eof
11989                    | Token::PipeForward
11990                    | Token::Comma
11991            )
11992        {
11993            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
11994        }
11995        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11996        loop {
11997            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11998                break;
11999            }
12000            if matches!(
12001                self.peek(),
12002                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12003            ) {
12004                break;
12005            }
12006            if self.peek_is_postfix_stmt_modifier_keyword() {
12007                break;
12008            }
12009            if let Token::Ident(ref kw) = self.peek().clone() {
12010                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12011                    self.advance();
12012                    self.expect(&Token::FatArrow)?;
12013                    let prog = self.parse_assign_expr_stop_at_pipe()?;
12014                    return Ok((merge_expr_list(parts), Some(prog)));
12015                }
12016            }
12017            parts.push(self.parse_assign_expr_stop_at_pipe()?);
12018        }
12019        Ok((merge_expr_list(parts), None))
12020    }
12021
12022    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
12023        if matches!(self.peek(), Token::LParen) {
12024            self.advance();
12025            let expr = self.parse_expression()?;
12026            self.expect(&Token::RParen)?;
12027            Ok(expr)
12028        } else {
12029            self.parse_assign_expr_stop_at_pipe()
12030        }
12031    }
12032
12033    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
12034        // Default to `$_` when the next token cannot start an argument expression
12035        // because it has lower precedence than a named unary operator. Perl 5
12036        // named unary precedence sits above ternary / comparison / logical / bitwise
12037        // / assignment / list ops; everything below should terminate the implicit
12038        // argument and let the surrounding expression continue.
12039        // See `perldoc perlop` ("Named Unary Operators").
12040        if matches!(
12041            self.peek(),
12042            // Statement / list / call boundaries
12043            Token::Semicolon
12044                | Token::RBrace
12045                | Token::RParen
12046                | Token::RBracket
12047                | Token::Eof
12048                | Token::Comma
12049                | Token::FatArrow
12050                | Token::PipeForward
12051            // Ternary `? :`
12052                | Token::Question
12053                | Token::Colon
12054            // Comparison / equality (numeric + string)
12055                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
12056                | Token::NumLe | Token::NumGe | Token::Spaceship
12057                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
12058                | Token::StrLe | Token::StrGe | Token::StrCmp
12059            // Logical (symbolic and word forms) + defined-or
12060                | Token::LogAnd | Token::LogOr | Token::LogNot
12061                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
12062                | Token::DefinedOr
12063            // Range (lower precedence than named unary)
12064                | Token::Range | Token::RangeExclusive
12065            // Assignment (any compound form)
12066                | Token::Assign | Token::PlusAssign | Token::MinusAssign
12067                | Token::MulAssign | Token::DivAssign | Token::ModAssign
12068                | Token::PowAssign | Token::DotAssign | Token::AndAssign
12069                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
12070                | Token::ShiftLeftAssign | Token::ShiftRightAssign
12071                | Token::BitAndAssign | Token::BitOrAssign
12072        ) {
12073            return Ok(Expr {
12074                kind: ExprKind::ScalarVar("_".into()),
12075                line: self.peek_line(),
12076            });
12077        }
12078        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
12079        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
12080        // Perl accepts both `length` and `length()` as `length($_)`.
12081        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
12082            let line = self.peek_line();
12083            self.advance(); // (
12084            self.advance(); // )
12085            return Ok(Expr {
12086                kind: ExprKind::ScalarVar("_".into()),
12087                line,
12088            });
12089        }
12090        self.parse_one_arg()
12091    }
12092
12093    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
12094    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
12095        let line = self.prev_line(); // line where shift/pop keyword was
12096        if matches!(self.peek(), Token::LParen) {
12097            self.advance();
12098            if matches!(self.peek(), Token::RParen) {
12099                self.advance();
12100                return Ok(Expr {
12101                    kind: ExprKind::ArrayVar("_".into()),
12102                    line: self.peek_line(),
12103                });
12104            }
12105            let expr = self.parse_expression()?;
12106            self.expect(&Token::RParen)?;
12107            return Ok(expr);
12108        }
12109        // Implicit semicolon: if next token is on a different line, don't consume it
12110        if matches!(
12111            self.peek(),
12112            Token::Semicolon
12113                | Token::RBrace
12114                | Token::RParen
12115                | Token::Eof
12116                | Token::Comma
12117                | Token::PipeForward
12118        ) || self.peek_line() > line
12119        {
12120            Ok(Expr {
12121                kind: ExprKind::ArrayVar("_".into()),
12122                line,
12123            })
12124        } else {
12125            self.parse_assign_expr()
12126        }
12127    }
12128
12129    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
12130        if matches!(self.peek(), Token::LParen) {
12131            self.advance();
12132            let args = self.parse_arg_list()?;
12133            self.expect(&Token::RParen)?;
12134            Ok(args)
12135        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
12136            // In thread context, don't consume barewords as arguments
12137            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
12138            Ok(vec![])
12139        } else {
12140            self.parse_list_until_terminator()
12141        }
12142    }
12143
12144    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
12145    /// should be treated as an auto-quoted string (hash key), not a function call.
12146    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
12147    #[inline]
12148    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
12149        if matches!(self.peek(), Token::FatArrow) {
12150            Some(Expr {
12151                kind: ExprKind::String(name.to_string()),
12152                line,
12153            })
12154        } else {
12155            None
12156        }
12157    }
12158
12159    /// Parse a hash subscript key inside `{…}`.
12160    ///
12161    /// Perl auto-quotes a single bareword before `}`, even for keywords:
12162    /// `$h{print}`, `$r->{f}` etc. all yield the string key.
12163    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
12164        let line = self.peek_line();
12165        if let Token::Ident(ref k) = self.peek().clone() {
12166            if matches!(self.peek_at(1), Token::RBrace) {
12167                let s = k.clone();
12168                self.advance();
12169                return Ok(Expr {
12170                    kind: ExprKind::String(s),
12171                    line,
12172                });
12173            }
12174        }
12175        self.parse_expression()
12176    }
12177
12178    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
12179    #[inline]
12180    fn peek_is_glob_par_progress_kw(&self) -> bool {
12181        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
12182            && matches!(self.peek_at(1), Token::FatArrow)
12183    }
12184
12185    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
12186    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
12187        let mut args = Vec::new();
12188        loop {
12189            if matches!(self.peek(), Token::RParen | Token::Eof) {
12190                break;
12191            }
12192            if self.peek_is_glob_par_progress_kw() {
12193                break;
12194            }
12195            args.push(self.parse_assign_expr()?);
12196            match self.peek() {
12197                Token::RParen => break,
12198                Token::Comma => {
12199                    self.advance();
12200                    if matches!(self.peek(), Token::RParen) {
12201                        break;
12202                    }
12203                    if self.peek_is_glob_par_progress_kw() {
12204                        break;
12205                    }
12206                }
12207                _ => {
12208                    return Err(self.syntax_err(
12209                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
12210                        self.peek_line(),
12211                    ));
12212                }
12213            }
12214        }
12215        Ok(args)
12216    }
12217
12218    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
12219    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
12220        let mut args = Vec::new();
12221        loop {
12222            if matches!(
12223                self.peek(),
12224                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12225            ) {
12226                break;
12227            }
12228            if self.peek_is_postfix_stmt_modifier_keyword() {
12229                break;
12230            }
12231            if self.peek_is_glob_par_progress_kw() {
12232                break;
12233            }
12234            args.push(self.parse_assign_expr()?);
12235            if !self.eat(&Token::Comma) {
12236                break;
12237            }
12238            if self.peek_is_glob_par_progress_kw() {
12239                break;
12240            }
12241        }
12242        Ok(args)
12243    }
12244
12245    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
12246    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
12247        if matches!(self.peek(), Token::LParen) {
12248            self.advance();
12249            let args = self.parse_pattern_list_until_rparen_or_progress()?;
12250            let progress = if self.peek_is_glob_par_progress_kw() {
12251                self.advance();
12252                self.expect(&Token::FatArrow)?;
12253                Some(Box::new(self.parse_assign_expr()?))
12254            } else {
12255                None
12256            };
12257            self.expect(&Token::RParen)?;
12258            Ok((args, progress))
12259        } else {
12260            let args = self.parse_pattern_list_glob_par_bare()?;
12261            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
12262            let progress = if self.peek_is_glob_par_progress_kw() {
12263                self.advance();
12264                self.expect(&Token::FatArrow)?;
12265                Some(Box::new(self.parse_assign_expr()?))
12266            } else {
12267                None
12268            };
12269            Ok((args, progress))
12270        }
12271    }
12272
12273    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
12274        let mut args = Vec::new();
12275        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
12276        // so shadow any outer paren-less-arg suppression from
12277        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
12278        let saved_no_pf = self.no_pipe_forward_depth;
12279        self.no_pipe_forward_depth = 0;
12280        while !matches!(
12281            self.peek(),
12282            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
12283        ) {
12284            let arg = match self.parse_assign_expr() {
12285                Ok(e) => e,
12286                Err(err) => {
12287                    self.no_pipe_forward_depth = saved_no_pf;
12288                    return Err(err);
12289                }
12290            };
12291            args.push(arg);
12292            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12293                break;
12294            }
12295        }
12296        self.no_pipe_forward_depth = saved_no_pf;
12297        Ok(args)
12298    }
12299
12300    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
12301    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
12302    /// no-arg method call; we must not consume that `+` as the start of a first argument.
12303    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
12304        let mut args = Vec::new();
12305        loop {
12306            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
12307            // hash argument to `next` (paren-less method call has no args here).
12308            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
12309                break;
12310            }
12311            if matches!(
12312                self.peek(),
12313                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12314            ) {
12315                break;
12316            }
12317            if let Token::Ident(ref kw) = self.peek().clone() {
12318                if matches!(
12319                    kw.as_str(),
12320                    "if" | "unless" | "while" | "until" | "for" | "foreach"
12321                ) {
12322                    break;
12323                }
12324            }
12325            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
12326            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
12327            if args.is_empty()
12328                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
12329            {
12330                break;
12331            }
12332            args.push(self.parse_assign_expr()?);
12333            if !self.eat(&Token::Comma) {
12334                break;
12335            }
12336        }
12337        Ok(args)
12338    }
12339
12340    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
12341    /// the whole `->meth` expression).
12342    fn peek_method_arg_infix_terminator(&self) -> bool {
12343        matches!(
12344            self.peek(),
12345            Token::Plus
12346                | Token::Minus
12347                | Token::Star
12348                | Token::Slash
12349                | Token::Percent
12350                | Token::Power
12351                | Token::Dot
12352                | Token::X
12353                | Token::NumEq
12354                | Token::NumNe
12355                | Token::NumLt
12356                | Token::NumGt
12357                | Token::NumLe
12358                | Token::NumGe
12359                | Token::Spaceship
12360                | Token::StrEq
12361                | Token::StrNe
12362                | Token::StrLt
12363                | Token::StrGt
12364                | Token::StrLe
12365                | Token::StrGe
12366                | Token::StrCmp
12367                | Token::LogAnd
12368                | Token::LogOr
12369                | Token::LogAndWord
12370                | Token::LogOrWord
12371                | Token::DefinedOr
12372                | Token::BitAnd
12373                | Token::BitOr
12374                | Token::BitXor
12375                | Token::ShiftLeft
12376                | Token::ShiftRight
12377                | Token::Range
12378                | Token::RangeExclusive
12379                | Token::BindMatch
12380                | Token::BindNotMatch
12381                | Token::Arrow
12382                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
12383                | Token::Question
12384                | Token::Colon
12385        )
12386    }
12387
12388    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
12389        let mut args = Vec::new();
12390        // Line of the last consumed token (the keyword / function name that
12391        // triggered this arg parse).  Used for implicit-semicolon: if no args
12392        // have been parsed yet and the next token is on a *different* line,
12393        // treat the newline as a statement boundary and stop.
12394        let call_line = self.prev_line();
12395        loop {
12396            if matches!(
12397                self.peek(),
12398                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12399            ) {
12400                break;
12401            }
12402            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
12403            if let Token::Ident(ref kw) = self.peek().clone() {
12404                if matches!(
12405                    kw.as_str(),
12406                    "if" | "unless" | "while" | "until" | "for" | "foreach"
12407                ) {
12408                    break;
12409                }
12410            }
12411            // Implicit semicolons: if no args have been collected yet and the
12412            // next token is on a different line from the call keyword, treat
12413            // the newline as a statement boundary.  This prevents paren-less
12414            // calls (`say`, `print`, user subs) from greedily swallowing the
12415            // *next* statement when the author omitted a semicolon.
12416            // After a comma continuation, multi-line arg lists still work.
12417            if args.is_empty() && self.peek_line() > call_line {
12418                break;
12419            }
12420            // Paren-less builtin args: `|>` terminates the whole call list, so
12421            // individual args must not absorb a following `|>`.
12422            args.push(self.parse_assign_expr_stop_at_pipe()?);
12423            if !self.eat(&Token::Comma) {
12424                break;
12425            }
12426        }
12427        Ok(args)
12428    }
12429
12430    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
12431        let mut pairs = Vec::new();
12432        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
12433            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
12434            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
12435            let line = self.peek_line();
12436            let key = if let Token::Ident(ref name) = self.peek().clone() {
12437                if matches!(self.peek_at(1), Token::FatArrow) {
12438                    self.advance();
12439                    Expr {
12440                        kind: ExprKind::String(name.clone()),
12441                        line,
12442                    }
12443                } else {
12444                    self.parse_assign_expr()?
12445                }
12446            } else {
12447                self.parse_assign_expr()?
12448            };
12449            // If the key expression is a hash/array variable and is followed by `}` or `,`
12450            // with no `=>`, treat the whole thing as a hash-from-expression construction.
12451            // This handles `{ %a }`, `{ %a, key => val }`, etc.
12452            if matches!(self.peek(), Token::RBrace | Token::Comma)
12453                && matches!(
12454                    key.kind,
12455                    ExprKind::HashVar(_)
12456                        | ExprKind::Deref {
12457                            kind: Sigil::Hash,
12458                            ..
12459                        }
12460                )
12461            {
12462                // Synthesize a pair whose key/value is spread from the hash expression.
12463                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
12464                // The evaluator will flatten this.
12465                let sentinel_key = Expr {
12466                    kind: ExprKind::String("__HASH_SPREAD__".into()),
12467                    line,
12468                };
12469                pairs.push((sentinel_key, key));
12470                self.eat(&Token::Comma);
12471                continue;
12472            }
12473            // Expect => or , after key
12474            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
12475                let val = self.parse_assign_expr()?;
12476                pairs.push((key, val));
12477                self.eat(&Token::Comma);
12478            } else {
12479                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
12480            }
12481        }
12482        self.expect(&Token::RBrace)?;
12483        Ok(pairs)
12484    }
12485
12486    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
12487    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
12488    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
12489    /// navigates. Caller expects and consumes `term` itself.
12490    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
12491        let mut pairs = Vec::new();
12492        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
12493            && !matches!(self.peek(), Token::Eof)
12494        {
12495            let line = self.peek_line();
12496            let key = if let Token::Ident(ref name) = self.peek().clone() {
12497                if matches!(self.peek_at(1), Token::FatArrow) {
12498                    self.advance();
12499                    Expr {
12500                        kind: ExprKind::String(name.clone()),
12501                        line,
12502                    }
12503                } else {
12504                    self.parse_assign_expr()?
12505                }
12506            } else {
12507                self.parse_assign_expr()?
12508            };
12509            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
12510                let val = self.parse_assign_expr()?;
12511                pairs.push((key, val));
12512                self.eat(&Token::Comma);
12513            } else {
12514                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
12515            }
12516        }
12517        Ok(pairs)
12518    }
12519
12520    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
12521    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
12522    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
12523    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
12524    /// Each step wraps the current expression in an `ArrowDeref`.
12525    fn interp_chain_subscripts(
12526        &self,
12527        chars: &[char],
12528        i: &mut usize,
12529        mut base: Expr,
12530        line: usize,
12531    ) -> Expr {
12532        loop {
12533            // Optional `->` connector
12534            let (after, requires_subscript) =
12535                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
12536                    (*i + 2, true)
12537                } else {
12538                    (*i, false)
12539                };
12540            if after >= chars.len() {
12541                break;
12542            }
12543            match chars[after] {
12544                '[' => {
12545                    *i = after + 1;
12546                    let mut idx_str = String::new();
12547                    while *i < chars.len() && chars[*i] != ']' {
12548                        idx_str.push(chars[*i]);
12549                        *i += 1;
12550                    }
12551                    if *i < chars.len() {
12552                        *i += 1;
12553                    }
12554                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
12555                        Expr {
12556                            kind: ExprKind::ScalarVar(rest.to_string()),
12557                            line,
12558                        }
12559                    } else if let Ok(n) = idx_str.parse::<i64>() {
12560                        Expr {
12561                            kind: ExprKind::Integer(n),
12562                            line,
12563                        }
12564                    } else {
12565                        Expr {
12566                            kind: ExprKind::String(idx_str),
12567                            line,
12568                        }
12569                    };
12570                    base = Expr {
12571                        kind: ExprKind::ArrowDeref {
12572                            expr: Box::new(base),
12573                            index: Box::new(idx_expr),
12574                            kind: DerefKind::Array,
12575                        },
12576                        line,
12577                    };
12578                }
12579                '{' => {
12580                    *i = after + 1;
12581                    let mut key = String::new();
12582                    let mut depth = 1usize;
12583                    while *i < chars.len() && depth > 0 {
12584                        if chars[*i] == '{' {
12585                            depth += 1;
12586                        } else if chars[*i] == '}' {
12587                            depth -= 1;
12588                            if depth == 0 {
12589                                break;
12590                            }
12591                        }
12592                        key.push(chars[*i]);
12593                        *i += 1;
12594                    }
12595                    if *i < chars.len() {
12596                        *i += 1;
12597                    }
12598                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
12599                        Expr {
12600                            kind: ExprKind::ScalarVar(rest.to_string()),
12601                            line,
12602                        }
12603                    } else {
12604                        Expr {
12605                            kind: ExprKind::String(key),
12606                            line,
12607                        }
12608                    };
12609                    base = Expr {
12610                        kind: ExprKind::ArrowDeref {
12611                            expr: Box::new(base),
12612                            index: Box::new(key_expr),
12613                            kind: DerefKind::Hash,
12614                        },
12615                        line,
12616                    };
12617                }
12618                _ => {
12619                    if requires_subscript {
12620                        // `->method()` etc — not interpolated, leave for literal output.
12621                    }
12622                    break;
12623                }
12624            }
12625        }
12626        base
12627    }
12628
12629    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
12630        // Parse $var and @var inside double-quoted strings
12631        let mut parts = Vec::new();
12632        let mut literal = String::new();
12633        let chars: Vec<char> = s.chars().collect();
12634        let mut i = 0;
12635
12636        'istr: while i < chars.len() {
12637            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
12638                literal.push('$');
12639                i += 1;
12640                continue;
12641            }
12642            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
12643            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
12644                literal.push('\\');
12645                i += 1;
12646                // i now points at '$' — fall through to $ handling below
12647            }
12648            if chars[i] == '$' && i + 1 < chars.len() {
12649                if !literal.is_empty() {
12650                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
12651                }
12652                i += 1; // past `$`
12653                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
12654                while i < chars.len() && chars[i].is_whitespace() {
12655                    i += 1;
12656                }
12657                if i >= chars.len() {
12658                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
12659                }
12660                // `$#name` — last index of `@name` (Perl `$#array`).
12661                if chars[i] == '#' {
12662                    i += 1;
12663                    let mut sname = String::from("#");
12664                    while i < chars.len()
12665                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
12666                    {
12667                        sname.push(chars[i]);
12668                        i += 1;
12669                    }
12670                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
12671                        sname.push_str("::");
12672                        i += 2;
12673                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
12674                            sname.push(chars[i]);
12675                            i += 1;
12676                        }
12677                    }
12678                    parts.push(StringPart::ScalarVar(sname));
12679                    continue;
12680                }
12681                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
12682                // between) and the second `$` is not followed by a word character or digit (`$$x`
12683                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
12684                if chars[i] == '$' {
12685                    let next_c = chars.get(i + 1).copied();
12686                    let is_pid = match next_c {
12687                        None => true,
12688                        Some(c)
12689                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
12690                        {
12691                            true
12692                        }
12693                        _ => false,
12694                    };
12695                    if is_pid {
12696                        parts.push(StringPart::ScalarVar("$$".to_string()));
12697                        i += 1; // consume second `$`
12698                        continue;
12699                    }
12700                    i += 1; // skip second `$` — same as a single `$` before the identifier
12701                }
12702                if chars[i] == '{' {
12703                    // `${…}` — braced variable OR expression interpolation.
12704                    //   `${name}`              → ScalarVar(name)        (Perl standard)
12705                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
12706                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
12707                    // stryke's prior `#{expr}` form remains supported elsewhere.
12708                    i += 1;
12709                    let mut inner = String::new();
12710                    let mut depth = 1usize;
12711                    while i < chars.len() && depth > 0 {
12712                        match chars[i] {
12713                            '{' => depth += 1,
12714                            '}' => {
12715                                depth -= 1;
12716                                if depth == 0 {
12717                                    break;
12718                                }
12719                            }
12720                            _ => {}
12721                        }
12722                        inner.push(chars[i]);
12723                        i += 1;
12724                    }
12725                    if i < chars.len() {
12726                        i += 1; // skip closing }
12727                    }
12728
12729                    // Distinguish "name" from "expression". If trimmed inner starts with
12730                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
12731                    // expression and emit a scalar deref. Otherwise, plain variable name.
12732                    let trimmed = inner.trim();
12733                    let is_expr = trimmed.starts_with('$')
12734                        || trimmed.starts_with('\\')
12735                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
12736                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
12737                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
12738                    let mut base: Expr = if is_expr {
12739                        // Re-parse the inner content as a Perl expression. Wrap in
12740                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
12741                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
12742                        match parse_expression_from_str(trimmed, "<interp>") {
12743                            Ok(e) => Expr {
12744                                kind: ExprKind::Deref {
12745                                    expr: Box::new(e),
12746                                    kind: Sigil::Scalar,
12747                                },
12748                                line,
12749                            },
12750                            Err(_) => Expr {
12751                                kind: ExprKind::ScalarVar(inner.clone()),
12752                                line,
12753                            },
12754                        }
12755                    } else {
12756                        // Treat as a plain (possibly qualified) variable name.
12757                        Expr {
12758                            kind: ExprKind::ScalarVar(inner),
12759                            line,
12760                        }
12761                    };
12762
12763                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
12764                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
12765                    // chains thereafter.
12766                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
12767                    parts.push(StringPart::Expr(base));
12768                } else if chars[i] == '^' {
12769                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
12770                    let mut name = String::from("^");
12771                    i += 1;
12772                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
12773                        name.push(chars[i]);
12774                        i += 1;
12775                    }
12776                    if i < chars.len() && chars[i] == '{' {
12777                        i += 1; // skip {
12778                        let mut key = String::new();
12779                        let mut depth = 1;
12780                        while i < chars.len() && depth > 0 {
12781                            if chars[i] == '{' {
12782                                depth += 1;
12783                            } else if chars[i] == '}' {
12784                                depth -= 1;
12785                                if depth == 0 {
12786                                    break;
12787                                }
12788                            }
12789                            key.push(chars[i]);
12790                            i += 1;
12791                        }
12792                        if i < chars.len() {
12793                            i += 1;
12794                        }
12795                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
12796                            Expr {
12797                                kind: ExprKind::ScalarVar(rest.to_string()),
12798                                line,
12799                            }
12800                        } else {
12801                            Expr {
12802                                kind: ExprKind::String(key),
12803                                line,
12804                            }
12805                        };
12806                        parts.push(StringPart::Expr(Expr {
12807                            kind: ExprKind::HashElement {
12808                                hash: name,
12809                                key: Box::new(key_expr),
12810                            },
12811                            line,
12812                        }));
12813                    } else if i < chars.len() && chars[i] == '[' {
12814                        i += 1;
12815                        let mut idx_str = String::new();
12816                        while i < chars.len() && chars[i] != ']' {
12817                            idx_str.push(chars[i]);
12818                            i += 1;
12819                        }
12820                        if i < chars.len() {
12821                            i += 1;
12822                        }
12823                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
12824                            Expr {
12825                                kind: ExprKind::ScalarVar(rest.to_string()),
12826                                line,
12827                            }
12828                        } else if let Ok(n) = idx_str.parse::<i64>() {
12829                            Expr {
12830                                kind: ExprKind::Integer(n),
12831                                line,
12832                            }
12833                        } else {
12834                            Expr {
12835                                kind: ExprKind::String(idx_str),
12836                                line,
12837                            }
12838                        };
12839                        parts.push(StringPart::Expr(Expr {
12840                            kind: ExprKind::ArrayElement {
12841                                array: name,
12842                                index: Box::new(idx_expr),
12843                            },
12844                            line,
12845                        }));
12846                    } else {
12847                        parts.push(StringPart::ScalarVar(name));
12848                    }
12849                } else if chars[i].is_alphabetic() || chars[i] == '_' {
12850                    let mut name = String::new();
12851                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
12852                        name.push(chars[i]);
12853                        i += 1;
12854                    }
12855                    // `$_<`, `$_<<`, … — outer topic (stryke extension); only for bare `_`.
12856                    if name == "_" {
12857                        while i < chars.len() && chars[i] == '<' {
12858                            name.push('<');
12859                            i += 1;
12860                        }
12861                    }
12862                    // Build the base expression, then thread arrow-deref chains
12863                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
12864                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
12865                    // correctly inside double-quoted strings (Perl convention).
12866                    let mut base = if i < chars.len() && chars[i] == '{' {
12867                        // $hash{key}
12868                        i += 1; // skip {
12869                        let mut key = String::new();
12870                        let mut depth = 1;
12871                        while i < chars.len() && depth > 0 {
12872                            if chars[i] == '{' {
12873                                depth += 1;
12874                            } else if chars[i] == '}' {
12875                                depth -= 1;
12876                                if depth == 0 {
12877                                    break;
12878                                }
12879                            }
12880                            key.push(chars[i]);
12881                            i += 1;
12882                        }
12883                        if i < chars.len() {
12884                            i += 1;
12885                        } // skip }
12886                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
12887                            Expr {
12888                                kind: ExprKind::ScalarVar(rest.to_string()),
12889                                line,
12890                            }
12891                        } else {
12892                            Expr {
12893                                kind: ExprKind::String(key),
12894                                line,
12895                            }
12896                        };
12897                        Expr {
12898                            kind: ExprKind::HashElement {
12899                                hash: name,
12900                                key: Box::new(key_expr),
12901                            },
12902                            line,
12903                        }
12904                    } else if i < chars.len() && chars[i] == '[' {
12905                        // $array[idx]
12906                        i += 1;
12907                        let mut idx_str = String::new();
12908                        while i < chars.len() && chars[i] != ']' {
12909                            idx_str.push(chars[i]);
12910                            i += 1;
12911                        }
12912                        if i < chars.len() {
12913                            i += 1;
12914                        }
12915                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
12916                            Expr {
12917                                kind: ExprKind::ScalarVar(rest.to_string()),
12918                                line,
12919                            }
12920                        } else if let Ok(n) = idx_str.parse::<i64>() {
12921                            Expr {
12922                                kind: ExprKind::Integer(n),
12923                                line,
12924                            }
12925                        } else {
12926                            Expr {
12927                                kind: ExprKind::String(idx_str),
12928                                line,
12929                            }
12930                        };
12931                        Expr {
12932                            kind: ExprKind::ArrayElement {
12933                                array: name,
12934                                index: Box::new(idx_expr),
12935                            },
12936                            line,
12937                        }
12938                    } else {
12939                        // Bare $name — defer to the chain-extension loop below.
12940                        Expr {
12941                            kind: ExprKind::ScalarVar(name),
12942                            line,
12943                        }
12944                    };
12945
12946                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
12947                    // implies `->` between consecutive subscripts (`$m[1][2]`
12948                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
12949                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
12950                    parts.push(StringPart::Expr(base));
12951                } else if chars[i].is_ascii_digit() {
12952                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
12953                    if chars[i] == '0' {
12954                        i += 1;
12955                        if i < chars.len() && chars[i].is_ascii_digit() {
12956                            return Err(self.syntax_err(
12957                                "Numeric variables with more than one digit may not start with '0'",
12958                                line,
12959                            ));
12960                        }
12961                        parts.push(StringPart::ScalarVar("0".into()));
12962                    } else {
12963                        let start = i;
12964                        while i < chars.len() && chars[i].is_ascii_digit() {
12965                            i += 1;
12966                        }
12967                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
12968                    }
12969                } else {
12970                    let c = chars[i];
12971                    let probe = c.to_string();
12972                    if Interpreter::is_special_scalar_name_for_get(&probe)
12973                        || matches!(c, '\'' | '`')
12974                    {
12975                        i += 1;
12976                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
12977                        if i < chars.len() && chars[i] == '{' {
12978                            i += 1; // skip {
12979                            let mut key = String::new();
12980                            let mut depth = 1;
12981                            while i < chars.len() && depth > 0 {
12982                                if chars[i] == '{' {
12983                                    depth += 1;
12984                                } else if chars[i] == '}' {
12985                                    depth -= 1;
12986                                    if depth == 0 {
12987                                        break;
12988                                    }
12989                                }
12990                                key.push(chars[i]);
12991                                i += 1;
12992                            }
12993                            if i < chars.len() {
12994                                i += 1;
12995                            } // skip }
12996                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
12997                                Expr {
12998                                    kind: ExprKind::ScalarVar(rest.to_string()),
12999                                    line,
13000                                }
13001                            } else {
13002                                Expr {
13003                                    kind: ExprKind::String(key),
13004                                    line,
13005                                }
13006                            };
13007                            let mut base = Expr {
13008                                kind: ExprKind::HashElement {
13009                                    hash: probe,
13010                                    key: Box::new(key_expr),
13011                                },
13012                                line,
13013                            };
13014                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13015                            parts.push(StringPart::Expr(base));
13016                        } else {
13017                            // Check for arrow deref chain: `$@->{key}`, etc.
13018                            let mut base = Expr {
13019                                kind: ExprKind::ScalarVar(probe),
13020                                line,
13021                            };
13022                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13023                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
13024                                // No chain extension — use the simpler ScalarVar part
13025                                if let ExprKind::ScalarVar(name) = base.kind {
13026                                    parts.push(StringPart::ScalarVar(name));
13027                                }
13028                            } else {
13029                                parts.push(StringPart::Expr(base));
13030                            }
13031                        }
13032                    } else {
13033                        literal.push('$');
13034                        literal.push(c);
13035                        i += 1;
13036                    }
13037                }
13038            } else if chars[i] == '@' && i + 1 < chars.len() {
13039                let next = chars[i + 1];
13040                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
13041                if next == '$' {
13042                    if !literal.is_empty() {
13043                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13044                    }
13045                    i += 1; // past `@`
13046                    debug_assert_eq!(chars[i], '$');
13047                    i += 1; // past `$`
13048                    while i < chars.len() && chars[i].is_whitespace() {
13049                        i += 1;
13050                    }
13051                    if i >= chars.len() {
13052                        return Err(self.syntax_err(
13053                            "Expected variable or block after `@$` in double-quoted string",
13054                            line,
13055                        ));
13056                    }
13057                    let inner_expr = if chars[i] == '{' {
13058                        i += 1;
13059                        let start = i;
13060                        let mut depth = 1usize;
13061                        while i < chars.len() && depth > 0 {
13062                            match chars[i] {
13063                                '{' => depth += 1,
13064                                '}' => {
13065                                    depth -= 1;
13066                                    if depth == 0 {
13067                                        break;
13068                                    }
13069                                }
13070                                _ => {}
13071                            }
13072                            i += 1;
13073                        }
13074                        if depth != 0 {
13075                            return Err(self.syntax_err(
13076                                "Unterminated `${ ... }` after `@` in double-quoted string",
13077                                line,
13078                            ));
13079                        }
13080                        let inner: String = chars[start..i].iter().collect();
13081                        i += 1; // closing `}`
13082                        parse_expression_from_str(inner.trim(), "-e")?
13083                    } else {
13084                        let mut name = String::new();
13085                        if chars[i] == '^' {
13086                            name.push('^');
13087                            i += 1;
13088                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
13089                            {
13090                                name.push(chars[i]);
13091                                i += 1;
13092                            }
13093                        } else {
13094                            while i < chars.len()
13095                                && (chars[i].is_alphanumeric()
13096                                    || chars[i] == '_'
13097                                    || chars[i] == ':')
13098                            {
13099                                name.push(chars[i]);
13100                                i += 1;
13101                            }
13102                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
13103                                name.push_str("::");
13104                                i += 2;
13105                                while i < chars.len()
13106                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
13107                                {
13108                                    name.push(chars[i]);
13109                                    i += 1;
13110                                }
13111                            }
13112                        }
13113                        if name.is_empty() {
13114                            return Err(self.syntax_err(
13115                                "Expected identifier after `@$` in double-quoted string",
13116                                line,
13117                            ));
13118                        }
13119                        Expr {
13120                            kind: ExprKind::ScalarVar(name),
13121                            line,
13122                        }
13123                    };
13124                    parts.push(StringPart::Expr(Expr {
13125                        kind: ExprKind::Deref {
13126                            expr: Box::new(inner_expr),
13127                            kind: Sigil::Array,
13128                        },
13129                        line,
13130                    }));
13131                    continue 'istr;
13132                }
13133                if next == '{' {
13134                    if !literal.is_empty() {
13135                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13136                    }
13137                    i += 2; // `@{`
13138                    let start = i;
13139                    let mut depth = 1usize;
13140                    while i < chars.len() && depth > 0 {
13141                        match chars[i] {
13142                            '{' => depth += 1,
13143                            '}' => {
13144                                depth -= 1;
13145                                if depth == 0 {
13146                                    break;
13147                                }
13148                            }
13149                            _ => {}
13150                        }
13151                        i += 1;
13152                    }
13153                    if depth != 0 {
13154                        return Err(
13155                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
13156                        );
13157                    }
13158                    let inner: String = chars[start..i].iter().collect();
13159                    i += 1; // closing `}`
13160                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
13161                    parts.push(StringPart::Expr(Expr {
13162                        kind: ExprKind::Deref {
13163                            expr: Box::new(inner_expr),
13164                            kind: Sigil::Array,
13165                        },
13166                        line,
13167                    }));
13168                    continue 'istr;
13169                }
13170                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
13171                    literal.push(chars[i]);
13172                    i += 1;
13173                } else {
13174                    if !literal.is_empty() {
13175                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13176                    }
13177                    i += 1;
13178                    let mut name = String::new();
13179                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
13180                        name.push(chars[i]);
13181                        i += 1;
13182                    } else {
13183                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13184                            name.push(chars[i]);
13185                            i += 1;
13186                        }
13187                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
13188                            name.push_str("::");
13189                            i += 2;
13190                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
13191                            {
13192                                name.push(chars[i]);
13193                                i += 1;
13194                            }
13195                        }
13196                    }
13197                    if i < chars.len() && chars[i] == '[' {
13198                        i += 1;
13199                        let start_inner = i;
13200                        let mut depth = 1usize;
13201                        while i < chars.len() && depth > 0 {
13202                            match chars[i] {
13203                                '[' => depth += 1,
13204                                ']' => depth -= 1,
13205                                _ => {}
13206                            }
13207                            if depth == 0 {
13208                                let inner: String = chars[start_inner..i].iter().collect();
13209                                i += 1; // closing ]
13210                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
13211                                parts.push(StringPart::Expr(Expr {
13212                                    kind: ExprKind::ArraySlice {
13213                                        array: name.clone(),
13214                                        indices,
13215                                    },
13216                                    line,
13217                                }));
13218                                continue 'istr;
13219                            }
13220                            i += 1;
13221                        }
13222                        return Err(self.syntax_err(
13223                            "Unterminated [ in array slice inside quoted string",
13224                            line,
13225                        ));
13226                    }
13227                    parts.push(StringPart::ArrayVar(name));
13228                }
13229            } else if chars[i] == '#'
13230                && i + 1 < chars.len()
13231                && chars[i + 1] == '{'
13232                && !crate::compat_mode()
13233            {
13234                // #{expr} — Ruby-style expression interpolation (stryke extension).
13235                if !literal.is_empty() {
13236                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13237                }
13238                i += 2; // skip `#{`
13239                let mut inner = String::new();
13240                let mut depth = 1usize;
13241                while i < chars.len() && depth > 0 {
13242                    match chars[i] {
13243                        '{' => depth += 1,
13244                        '}' => {
13245                            depth -= 1;
13246                            if depth == 0 {
13247                                break;
13248                            }
13249                        }
13250                        _ => {}
13251                    }
13252                    inner.push(chars[i]);
13253                    i += 1;
13254                }
13255                if i < chars.len() {
13256                    i += 1; // skip closing `}`
13257                }
13258                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
13259                parts.push(StringPart::Expr(expr));
13260            } else {
13261                literal.push(chars[i]);
13262                i += 1;
13263            }
13264        }
13265        if !literal.is_empty() {
13266            parts.push(StringPart::Literal(literal));
13267        }
13268
13269        if parts.len() == 1 {
13270            if let StringPart::Literal(s) = &parts[0] {
13271                return Ok(Expr {
13272                    kind: ExprKind::String(s.clone()),
13273                    line,
13274                });
13275            }
13276        }
13277        if parts.is_empty() {
13278            return Ok(Expr {
13279                kind: ExprKind::String(String::new()),
13280                line,
13281            });
13282        }
13283
13284        Ok(Expr {
13285            kind: ExprKind::InterpolatedString(parts),
13286            line,
13287        })
13288    }
13289
13290    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
13291        match &e.kind {
13292            ExprKind::String(s) => Ok(s.clone()),
13293            _ => Err(self.syntax_err(
13294                "overload key must be a string literal (e.g. '\"\"' or '+')",
13295                e.line,
13296            )),
13297        }
13298    }
13299
13300    fn expr_to_overload_sub(&self, e: &Expr) -> PerlResult<String> {
13301        match &e.kind {
13302            ExprKind::String(s) => Ok(s.clone()),
13303            ExprKind::Integer(n) => Ok(n.to_string()),
13304            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
13305            _ => Err(self.syntax_err(
13306                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
13307                e.line,
13308            )),
13309        }
13310    }
13311}
13312
13313fn merge_expr_list(parts: Vec<Expr>) -> Expr {
13314    if parts.len() == 1 {
13315        parts.into_iter().next().unwrap()
13316    } else {
13317        let line = parts.first().map(|e| e.line).unwrap_or(0);
13318        Expr {
13319            kind: ExprKind::List(parts),
13320            line,
13321        }
13322    }
13323}
13324
13325/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
13326pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
13327    let mut lexer = Lexer::new_with_file(s, file);
13328    let tokens = lexer.tokenize()?;
13329    let mut parser = Parser::new_with_file(tokens, file);
13330    let e = parser.parse_expression()?;
13331    if !parser.at_eof() {
13332        return Err(parser.syntax_err(
13333            "Extra tokens in embedded string expression",
13334            parser.peek_line(),
13335        ));
13336    }
13337    Ok(e)
13338}
13339
13340/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
13341pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
13342    let mut lexer = Lexer::new_with_file(s, file);
13343    let tokens = lexer.tokenize()?;
13344    let mut parser = Parser::new_with_file(tokens, file);
13345    let stmts = parser.parse_statements()?;
13346    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
13347    let inner = Expr {
13348        kind: ExprKind::CodeRef {
13349            params: vec![],
13350            body: stmts,
13351        },
13352        line: inner_line,
13353    };
13354    Ok(Expr {
13355        kind: ExprKind::Do(Box::new(inner)),
13356        line,
13357    })
13358}
13359
13360/// Comma-separated expressions on a `format` value line (below a picture line).
13361/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
13362pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
13363    let mut lexer = Lexer::new_with_file(s, file);
13364    let tokens = lexer.tokenize()?;
13365    let mut parser = Parser::new_with_file(tokens, file);
13366    parser.parse_arg_list()
13367}
13368
13369pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
13370    let trimmed = line.trim();
13371    if trimmed.is_empty() {
13372        return Ok(vec![]);
13373    }
13374    let mut lexer = Lexer::new(trimmed);
13375    let tokens = lexer.tokenize()?;
13376    let mut parser = Parser::new(tokens);
13377    let mut exprs = Vec::new();
13378    loop {
13379        if parser.at_eof() {
13380            break;
13381        }
13382        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
13383        exprs.push(parser.parse_assign_expr()?);
13384        if parser.eat(&Token::Comma) {
13385            continue;
13386        }
13387        if !parser.at_eof() {
13388            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
13389        }
13390        break;
13391    }
13392    Ok(exprs)
13393}
13394
13395#[cfg(test)]
13396mod tests {
13397    use super::*;
13398
13399    fn parse_ok(code: &str) -> Program {
13400        let mut lexer = Lexer::new(code);
13401        let tokens = lexer.tokenize().expect("tokenize");
13402        let mut parser = Parser::new(tokens);
13403        parser.parse_program().expect("parse")
13404    }
13405
13406    fn parse_err(code: &str) -> String {
13407        let mut lexer = Lexer::new(code);
13408        let tokens = match lexer.tokenize() {
13409            Ok(t) => t,
13410            Err(e) => return e.message,
13411        };
13412        let mut parser = Parser::new(tokens);
13413        parser.parse_program().unwrap_err().message
13414    }
13415
13416    #[test]
13417    fn parse_empty_program() {
13418        let p = parse_ok("");
13419        assert!(p.statements.is_empty());
13420    }
13421
13422    #[test]
13423    fn parse_semicolons_only() {
13424        let p = parse_ok(";;;");
13425        assert!(p.statements.len() <= 3);
13426    }
13427
13428    #[test]
13429    fn parse_simple_scalar_assignment() {
13430        let p = parse_ok("$x = 1;");
13431        assert_eq!(p.statements.len(), 1);
13432    }
13433
13434    #[test]
13435    fn parse_simple_array_assignment() {
13436        let p = parse_ok("@arr = (1, 2, 3);");
13437        assert_eq!(p.statements.len(), 1);
13438    }
13439
13440    #[test]
13441    fn parse_simple_hash_assignment() {
13442        let p = parse_ok("%h = (a => 1, b => 2);");
13443        assert_eq!(p.statements.len(), 1);
13444    }
13445
13446    #[test]
13447    fn parse_subroutine_decl() {
13448        let p = parse_ok("sub foo { 1 }");
13449        assert_eq!(p.statements.len(), 1);
13450        match &p.statements[0].kind {
13451            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
13452            _ => panic!("expected SubDecl"),
13453        }
13454    }
13455
13456    #[test]
13457    fn parse_subroutine_with_prototype() {
13458        let p = parse_ok("sub foo ($$) { 1 }");
13459        assert_eq!(p.statements.len(), 1);
13460        match &p.statements[0].kind {
13461            StmtKind::SubDecl { prototype, .. } => {
13462                assert!(prototype.is_some());
13463            }
13464            _ => panic!("expected SubDecl"),
13465        }
13466    }
13467
13468    #[test]
13469    fn parse_anonymous_sub() {
13470        let p = parse_ok("my $f = sub { 1 };");
13471        assert_eq!(p.statements.len(), 1);
13472    }
13473
13474    #[test]
13475    fn parse_if_statement() {
13476        let p = parse_ok("if (1) { 2 }");
13477        assert_eq!(p.statements.len(), 1);
13478        matches!(&p.statements[0].kind, StmtKind::If { .. });
13479    }
13480
13481    #[test]
13482    fn parse_if_elsif_else() {
13483        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
13484        assert_eq!(p.statements.len(), 1);
13485    }
13486
13487    #[test]
13488    fn parse_unless_statement() {
13489        let p = parse_ok("unless (0) { 1 }");
13490        assert_eq!(p.statements.len(), 1);
13491    }
13492
13493    #[test]
13494    fn parse_while_loop() {
13495        let p = parse_ok("while ($x) { $x-- }");
13496        assert_eq!(p.statements.len(), 1);
13497    }
13498
13499    #[test]
13500    fn parse_until_loop() {
13501        let p = parse_ok("until ($x) { $x++ }");
13502        assert_eq!(p.statements.len(), 1);
13503    }
13504
13505    #[test]
13506    fn parse_for_c_style() {
13507        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
13508        assert_eq!(p.statements.len(), 1);
13509    }
13510
13511    #[test]
13512    fn parse_foreach_loop() {
13513        let p = parse_ok("foreach my $x (@arr) { 1 }");
13514        assert_eq!(p.statements.len(), 1);
13515    }
13516
13517    #[test]
13518    fn parse_loop_with_label() {
13519        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
13520        assert_eq!(p.statements.len(), 1);
13521        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
13522    }
13523
13524    #[test]
13525    fn parse_begin_block() {
13526        let p = parse_ok("BEGIN { 1 }");
13527        assert_eq!(p.statements.len(), 1);
13528        matches!(&p.statements[0].kind, StmtKind::Begin(_));
13529    }
13530
13531    #[test]
13532    fn parse_end_block() {
13533        let p = parse_ok("END { 1 }");
13534        assert_eq!(p.statements.len(), 1);
13535        matches!(&p.statements[0].kind, StmtKind::End(_));
13536    }
13537
13538    #[test]
13539    fn parse_package_statement() {
13540        let p = parse_ok("package Foo::Bar;");
13541        assert_eq!(p.statements.len(), 1);
13542        match &p.statements[0].kind {
13543            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
13544            _ => panic!("expected Package"),
13545        }
13546    }
13547
13548    #[test]
13549    fn parse_use_statement() {
13550        let p = parse_ok("use strict;");
13551        assert_eq!(p.statements.len(), 1);
13552    }
13553
13554    #[test]
13555    fn parse_no_statement() {
13556        let p = parse_ok("no warnings;");
13557        assert_eq!(p.statements.len(), 1);
13558    }
13559
13560    #[test]
13561    fn parse_require_bareword() {
13562        let p = parse_ok("require Foo::Bar;");
13563        assert_eq!(p.statements.len(), 1);
13564    }
13565
13566    #[test]
13567    fn parse_require_string() {
13568        let p = parse_ok(r#"require "foo.pl";"#);
13569        assert_eq!(p.statements.len(), 1);
13570    }
13571
13572    #[test]
13573    fn parse_eval_block() {
13574        let p = parse_ok("eval { 1 };");
13575        assert_eq!(p.statements.len(), 1);
13576    }
13577
13578    #[test]
13579    fn parse_eval_string() {
13580        let p = parse_ok(r#"eval "1 + 2";"#);
13581        assert_eq!(p.statements.len(), 1);
13582    }
13583
13584    #[test]
13585    fn parse_qw_word_list() {
13586        let p = parse_ok("my @a = qw(foo bar baz);");
13587        assert_eq!(p.statements.len(), 1);
13588    }
13589
13590    #[test]
13591    fn parse_q_string() {
13592        let p = parse_ok("my $s = q{hello};");
13593        assert_eq!(p.statements.len(), 1);
13594    }
13595
13596    #[test]
13597    fn parse_qq_string() {
13598        let p = parse_ok(r#"my $s = qq(hello $x);"#);
13599        assert_eq!(p.statements.len(), 1);
13600    }
13601
13602    #[test]
13603    fn parse_regex_match() {
13604        let p = parse_ok(r#"$x =~ /foo/;"#);
13605        assert_eq!(p.statements.len(), 1);
13606    }
13607
13608    #[test]
13609    fn parse_regex_substitution() {
13610        let p = parse_ok(r#"$x =~ s/foo/bar/g;"#);
13611        assert_eq!(p.statements.len(), 1);
13612    }
13613
13614    #[test]
13615    fn parse_transliterate() {
13616        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/;"#);
13617        assert_eq!(p.statements.len(), 1);
13618    }
13619
13620    #[test]
13621    fn parse_ternary_operator() {
13622        let p = parse_ok("my $x = $a ? 1 : 2;");
13623        assert_eq!(p.statements.len(), 1);
13624    }
13625
13626    #[test]
13627    fn parse_arrow_method_call() {
13628        let p = parse_ok("$obj->method();");
13629        assert_eq!(p.statements.len(), 1);
13630    }
13631
13632    #[test]
13633    fn parse_arrow_deref_hash() {
13634        let p = parse_ok("$r->{key};");
13635        assert_eq!(p.statements.len(), 1);
13636    }
13637
13638    #[test]
13639    fn parse_arrow_deref_array() {
13640        let p = parse_ok("$r->[0];");
13641        assert_eq!(p.statements.len(), 1);
13642    }
13643
13644    #[test]
13645    fn parse_chained_arrow_deref() {
13646        let p = parse_ok("$r->{a}[0]{b};");
13647        assert_eq!(p.statements.len(), 1);
13648    }
13649
13650    #[test]
13651    fn parse_my_multiple_vars() {
13652        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3);");
13653        assert_eq!(p.statements.len(), 1);
13654    }
13655
13656    #[test]
13657    fn parse_our_scalar() {
13658        let p = parse_ok("our $VERSION = '1.0';");
13659        assert_eq!(p.statements.len(), 1);
13660    }
13661
13662    #[test]
13663    fn parse_local_scalar() {
13664        let p = parse_ok("local $/ = undef;");
13665        assert_eq!(p.statements.len(), 1);
13666    }
13667
13668    #[test]
13669    fn parse_state_variable() {
13670        let p = parse_ok("sub counter { state $n = 0; $n++ }");
13671        assert_eq!(p.statements.len(), 1);
13672    }
13673
13674    #[test]
13675    fn parse_postfix_if() {
13676        let p = parse_ok("print 1 if $x;");
13677        assert_eq!(p.statements.len(), 1);
13678    }
13679
13680    #[test]
13681    fn parse_postfix_unless() {
13682        let p = parse_ok("die 'error' unless $ok;");
13683        assert_eq!(p.statements.len(), 1);
13684    }
13685
13686    #[test]
13687    fn parse_postfix_while() {
13688        let p = parse_ok("$x++ while $x < 10;");
13689        assert_eq!(p.statements.len(), 1);
13690    }
13691
13692    #[test]
13693    fn parse_postfix_for() {
13694        let p = parse_ok("print for @arr;");
13695        assert_eq!(p.statements.len(), 1);
13696    }
13697
13698    #[test]
13699    fn parse_last_next_redo() {
13700        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
13701        assert_eq!(p.statements.len(), 1);
13702    }
13703
13704    #[test]
13705    fn parse_return_statement() {
13706        let p = parse_ok("sub foo { return 42 }");
13707        assert_eq!(p.statements.len(), 1);
13708    }
13709
13710    #[test]
13711    fn parse_wantarray() {
13712        let p = parse_ok("sub foo { wantarray ? @a : $a }");
13713        assert_eq!(p.statements.len(), 1);
13714    }
13715
13716    #[test]
13717    fn parse_caller_builtin() {
13718        let p = parse_ok("my @c = caller;");
13719        assert_eq!(p.statements.len(), 1);
13720    }
13721
13722    #[test]
13723    fn parse_ref_to_array() {
13724        let p = parse_ok("my $r = \\@arr;");
13725        assert_eq!(p.statements.len(), 1);
13726    }
13727
13728    #[test]
13729    fn parse_ref_to_hash() {
13730        let p = parse_ok("my $r = \\%hash;");
13731        assert_eq!(p.statements.len(), 1);
13732    }
13733
13734    #[test]
13735    fn parse_ref_to_scalar() {
13736        let p = parse_ok("my $r = \\$x;");
13737        assert_eq!(p.statements.len(), 1);
13738    }
13739
13740    #[test]
13741    fn parse_deref_scalar() {
13742        let p = parse_ok("my $v = $$r;");
13743        assert_eq!(p.statements.len(), 1);
13744    }
13745
13746    #[test]
13747    fn parse_deref_array() {
13748        let p = parse_ok("my @a = @$r;");
13749        assert_eq!(p.statements.len(), 1);
13750    }
13751
13752    #[test]
13753    fn parse_deref_hash() {
13754        let p = parse_ok("my %h = %$r;");
13755        assert_eq!(p.statements.len(), 1);
13756    }
13757
13758    #[test]
13759    fn parse_blessed_ref() {
13760        let p = parse_ok("bless $r, 'Foo';");
13761        assert_eq!(p.statements.len(), 1);
13762    }
13763
13764    #[test]
13765    fn parse_heredoc_basic() {
13766        let p = parse_ok("my $s = <<END;\nfoo\nEND");
13767        assert_eq!(p.statements.len(), 1);
13768    }
13769
13770    #[test]
13771    fn parse_heredoc_quoted() {
13772        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
13773        assert_eq!(p.statements.len(), 1);
13774    }
13775
13776    #[test]
13777    fn parse_do_block() {
13778        let p = parse_ok("my $x = do { 1 + 2 };");
13779        assert_eq!(p.statements.len(), 1);
13780    }
13781
13782    #[test]
13783    fn parse_do_file() {
13784        let p = parse_ok(r#"do "foo.pl";"#);
13785        assert_eq!(p.statements.len(), 1);
13786    }
13787
13788    #[test]
13789    fn parse_map_expression() {
13790        let p = parse_ok("my @b = map { $_ * 2 } @a;");
13791        assert_eq!(p.statements.len(), 1);
13792    }
13793
13794    #[test]
13795    fn parse_grep_expression() {
13796        let p = parse_ok("my @b = grep { $_ > 0 } @a;");
13797        assert_eq!(p.statements.len(), 1);
13798    }
13799
13800    #[test]
13801    fn parse_sort_expression() {
13802        let p = parse_ok("my @b = sort { $a <=> $b } @a;");
13803        assert_eq!(p.statements.len(), 1);
13804    }
13805
13806    #[test]
13807    fn parse_pipe_forward() {
13808        let p = parse_ok("@a |> map { $_ * 2 };");
13809        assert_eq!(p.statements.len(), 1);
13810    }
13811
13812    #[test]
13813    fn parse_expression_from_str_simple() {
13814        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
13815        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
13816    }
13817
13818    #[test]
13819    fn parse_expression_from_str_extra_tokens_error() {
13820        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
13821        assert!(err.message.contains("Extra tokens"));
13822    }
13823
13824    #[test]
13825    fn parse_slice_indices_from_str_basic() {
13826        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
13827        assert_eq!(indices.len(), 3);
13828    }
13829
13830    #[test]
13831    fn parse_format_value_line_empty() {
13832        let exprs = parse_format_value_line("").unwrap();
13833        assert!(exprs.is_empty());
13834    }
13835
13836    #[test]
13837    fn parse_format_value_line_single() {
13838        let exprs = parse_format_value_line("$x").unwrap();
13839        assert_eq!(exprs.len(), 1);
13840    }
13841
13842    #[test]
13843    fn parse_format_value_line_multiple() {
13844        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
13845        assert_eq!(exprs.len(), 3);
13846    }
13847
13848    #[test]
13849    fn parse_unclosed_brace_error() {
13850        let err = parse_err("sub foo {");
13851        assert!(!err.is_empty());
13852    }
13853
13854    #[test]
13855    fn parse_unclosed_paren_error() {
13856        let err = parse_err("print (1, 2");
13857        assert!(!err.is_empty());
13858    }
13859
13860    #[test]
13861    fn parse_invalid_statement_error() {
13862        let err = parse_err("???");
13863        assert!(!err.is_empty());
13864    }
13865
13866    #[test]
13867    fn merge_expr_list_single() {
13868        let e = Expr {
13869            kind: ExprKind::Integer(1),
13870            line: 1,
13871        };
13872        let merged = merge_expr_list(vec![e.clone()]);
13873        matches!(merged.kind, ExprKind::Integer(1));
13874    }
13875
13876    #[test]
13877    fn merge_expr_list_multiple() {
13878        let e1 = Expr {
13879            kind: ExprKind::Integer(1),
13880            line: 1,
13881        };
13882        let e2 = Expr {
13883            kind: ExprKind::Integer(2),
13884            line: 1,
13885        };
13886        let merged = merge_expr_list(vec![e1, e2]);
13887        matches!(merged.kind, ExprKind::List(_));
13888    }
13889}