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    /// True when the next token is on a different line from `stmt_line` and could
362    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
363    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
364    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
365        if crate::compat_mode() {
366            return false;
367        }
368        if self.peek_line() == stmt_line {
369            return false;
370        }
371        matches!(
372            self.peek(),
373            Token::ScalarVar(_)
374                | Token::DerefScalarVar(_)
375                | Token::ArrayVar(_)
376                | Token::HashVar(_)
377                | Token::LBrace
378        ) || self.next_is_new_stmt_keyword(stmt_line)
379    }
380
381    // ── Top level ──
382
383    pub fn parse_program(&mut self) -> PerlResult<Program> {
384        let statements = self.parse_statements()?;
385        Ok(Program { statements })
386    }
387
388    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
389    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
390        let mut statements = Vec::new();
391        while !self.at_eof() {
392            if matches!(self.peek(), Token::Semicolon) {
393                let line = self.peek_line();
394                self.advance();
395                statements.push(Statement {
396                    label: None,
397                    kind: StmtKind::Empty,
398                    line,
399                });
400                continue;
401            }
402            statements.push(self.parse_statement()?);
403        }
404        Ok(statements)
405    }
406
407    // ── Statements ──
408
409    fn parse_statement(&mut self) -> PerlResult<Statement> {
410        let line = self.peek_line();
411
412        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
413        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
414        let label = match self.peek().clone() {
415            Token::Ident(_) => {
416                if matches!(self.peek_at(1), Token::Colon)
417                    && !matches!(self.peek_at(2), Token::Colon)
418                {
419                    let (tok, _) = self.advance();
420                    let l = match tok {
421                        Token::Ident(l) => l,
422                        _ => unreachable!(),
423                    };
424                    self.advance(); // ':'
425                    Some(l)
426                } else {
427                    None
428                }
429            }
430            _ => None,
431        };
432
433        let mut stmt = match self.peek().clone() {
434            Token::FormatDecl { .. } => {
435                let tok_line = self.peek_line();
436                let (tok, _) = self.advance();
437                match tok {
438                    Token::FormatDecl { name, lines } => Statement {
439                        label: label.clone(),
440                        kind: StmtKind::FormatDecl { name, lines },
441                        line: tok_line,
442                    },
443                    _ => unreachable!(),
444                }
445            }
446            Token::Ident(ref kw) => match kw.as_str() {
447                "if" => self.parse_if()?,
448                "unless" => self.parse_unless()?,
449                "while" => {
450                    let mut s = self.parse_while()?;
451                    if let StmtKind::While {
452                        label: ref mut lbl, ..
453                    } = s.kind
454                    {
455                        *lbl = label.clone();
456                    }
457                    s
458                }
459                "until" => {
460                    let mut s = self.parse_until()?;
461                    if let StmtKind::Until {
462                        label: ref mut lbl, ..
463                    } = s.kind
464                    {
465                        *lbl = label.clone();
466                    }
467                    s
468                }
469                "for" => {
470                    let mut s = self.parse_for_or_foreach()?;
471                    match s.kind {
472                        StmtKind::For {
473                            label: ref mut lbl, ..
474                        }
475                        | StmtKind::Foreach {
476                            label: ref mut lbl, ..
477                        } => *lbl = label.clone(),
478                        _ => {}
479                    }
480                    s
481                }
482                "foreach" => {
483                    let mut s = self.parse_foreach()?;
484                    if let StmtKind::Foreach {
485                        label: ref mut lbl, ..
486                    } = s.kind
487                    {
488                        *lbl = label.clone();
489                    }
490                    s
491                }
492                "sub" | "fn" => self.parse_sub_decl()?,
493                "struct" => {
494                    if crate::compat_mode() {
495                        return Err(self.syntax_err(
496                            "`struct` is a stryke extension (disabled by --compat)",
497                            self.peek_line(),
498                        ));
499                    }
500                    self.parse_struct_decl()?
501                }
502                "enum" => {
503                    if crate::compat_mode() {
504                        return Err(self.syntax_err(
505                            "`enum` is a stryke extension (disabled by --compat)",
506                            self.peek_line(),
507                        ));
508                    }
509                    self.parse_enum_decl()?
510                }
511                "class" => {
512                    if crate::compat_mode() {
513                        // TODO: parse Perl 5.38 class syntax with :isa()
514                        return Err(self.syntax_err(
515                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
516                            self.peek_line(),
517                        ));
518                    }
519                    self.parse_class_decl(false, false)?
520                }
521                "abstract" => {
522                    self.advance(); // abstract
523                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
524                        return Err(self.syntax_err(
525                            "`abstract` must be followed by `class`",
526                            self.peek_line(),
527                        ));
528                    }
529                    self.parse_class_decl(true, false)?
530                }
531                "final" => {
532                    self.advance(); // final
533                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
534                        return Err(self
535                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
536                    }
537                    self.parse_class_decl(false, true)?
538                }
539                "trait" => {
540                    if crate::compat_mode() {
541                        return Err(self.syntax_err(
542                            "`trait` is a stryke extension (disabled by --compat)",
543                            self.peek_line(),
544                        ));
545                    }
546                    self.parse_trait_decl()?
547                }
548                "my" => self.parse_my_our_local("my", false)?,
549                "state" => self.parse_my_our_local("state", false)?,
550                "mysync" => {
551                    if crate::compat_mode() {
552                        return Err(self.syntax_err(
553                            "`mysync` is a stryke extension (disabled by --compat)",
554                            self.peek_line(),
555                        ));
556                    }
557                    self.parse_my_our_local("mysync", false)?
558                }
559                "frozen" | "const" => {
560                    let leading = kw.as_str().to_string();
561                    if crate::compat_mode() {
562                        return Err(self.syntax_err(
563                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
564                            self.peek_line(),
565                        ));
566                    }
567                    // `frozen my $x = val;` / `const my $x = val;` — the
568                    // two spellings are interchangeable (`const` is the
569                    // more-familiar name for new users). Expects `my`
570                    // to follow.
571                    self.advance(); // consume "frozen"/"const"
572                    if let Token::Ident(ref kw) = self.peek().clone() {
573                        if kw == "my" {
574                            let mut stmt = self.parse_my_our_local("my", false)?;
575                            if let StmtKind::My(ref mut decls) = stmt.kind {
576                                for decl in decls.iter_mut() {
577                                    decl.frozen = true;
578                                }
579                            }
580                            stmt
581                        } else {
582                            return Err(self.syntax_err(
583                                format!("Expected 'my' after '{leading}'"),
584                                self.peek_line(),
585                            ));
586                        }
587                    } else {
588                        return Err(self.syntax_err(
589                            format!("Expected 'my' after '{leading}'"),
590                            self.peek_line(),
591                        ));
592                    }
593                }
594                "typed" => {
595                    if crate::compat_mode() {
596                        return Err(self.syntax_err(
597                            "`typed` is a stryke extension (disabled by --compat)",
598                            self.peek_line(),
599                        ));
600                    }
601                    self.advance();
602                    if let Token::Ident(ref kw) = self.peek().clone() {
603                        if kw == "my" {
604                            self.parse_my_our_local("my", true)?
605                        } else {
606                            return Err(
607                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
608                            );
609                        }
610                    } else {
611                        return Err(
612                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
613                        );
614                    }
615                }
616                "our" => self.parse_my_our_local("our", false)?,
617                "local" => self.parse_my_our_local("local", false)?,
618                "package" => self.parse_package()?,
619                "use" => self.parse_use()?,
620                "no" => self.parse_no()?,
621                "return" => self.parse_return()?,
622                "last" => {
623                    self.advance();
624                    let lbl = if let Token::Ident(ref s) = self.peek() {
625                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
626                            let (Token::Ident(l), _) = self.advance() else {
627                                unreachable!()
628                            };
629                            Some(l)
630                        } else {
631                            None
632                        }
633                    } else {
634                        None
635                    };
636                    let stmt = Statement {
637                        label: None,
638                        kind: StmtKind::Last(lbl.or(label.clone())),
639                        line,
640                    };
641                    self.parse_stmt_postfix_modifier(stmt)?
642                }
643                "next" => {
644                    self.advance();
645                    let lbl = if let Token::Ident(ref s) = self.peek() {
646                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
647                            let (Token::Ident(l), _) = self.advance() else {
648                                unreachable!()
649                            };
650                            Some(l)
651                        } else {
652                            None
653                        }
654                    } else {
655                        None
656                    };
657                    let stmt = Statement {
658                        label: None,
659                        kind: StmtKind::Next(lbl.or(label.clone())),
660                        line,
661                    };
662                    self.parse_stmt_postfix_modifier(stmt)?
663                }
664                "redo" => {
665                    self.advance();
666                    self.eat(&Token::Semicolon);
667                    Statement {
668                        label: None,
669                        kind: StmtKind::Redo(label.clone()),
670                        line,
671                    }
672                }
673                "BEGIN" => {
674                    self.advance();
675                    let block = self.parse_block()?;
676                    Statement {
677                        label: None,
678                        kind: StmtKind::Begin(block),
679                        line,
680                    }
681                }
682                "END" => {
683                    self.advance();
684                    let block = self.parse_block()?;
685                    Statement {
686                        label: None,
687                        kind: StmtKind::End(block),
688                        line,
689                    }
690                }
691                "UNITCHECK" => {
692                    self.advance();
693                    let block = self.parse_block()?;
694                    Statement {
695                        label: None,
696                        kind: StmtKind::UnitCheck(block),
697                        line,
698                    }
699                }
700                "CHECK" => {
701                    self.advance();
702                    let block = self.parse_block()?;
703                    Statement {
704                        label: None,
705                        kind: StmtKind::Check(block),
706                        line,
707                    }
708                }
709                "INIT" => {
710                    self.advance();
711                    let block = self.parse_block()?;
712                    Statement {
713                        label: None,
714                        kind: StmtKind::Init(block),
715                        line,
716                    }
717                }
718                "goto" => {
719                    self.advance();
720                    let target = self.parse_expression()?;
721                    let stmt = Statement {
722                        label: None,
723                        kind: StmtKind::Goto {
724                            target: Box::new(target),
725                        },
726                        line,
727                    };
728                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
729                    self.parse_stmt_postfix_modifier(stmt)?
730                }
731                "continue" => {
732                    self.advance();
733                    let block = self.parse_block()?;
734                    Statement {
735                        label: None,
736                        kind: StmtKind::Continue(block),
737                        line,
738                    }
739                }
740                "try" => self.parse_try_catch()?,
741                "defer" => self.parse_defer_stmt()?,
742                "tie" => self.parse_tie_stmt()?,
743                "given" => self.parse_given()?,
744                "when" => self.parse_when_stmt()?,
745                "default" => self.parse_default_stmt()?,
746                "eval_timeout" => self.parse_eval_timeout()?,
747                "do" => {
748                    if matches!(self.peek_at(1), Token::LBrace) {
749                        self.advance();
750                        let body = self.parse_block()?;
751                        if let Token::Ident(ref w) = self.peek().clone() {
752                            if w == "while" {
753                                self.advance();
754                                self.expect(&Token::LParen)?;
755                                let mut condition = self.parse_expression()?;
756                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
757                                self.expect(&Token::RParen)?;
758                                self.eat(&Token::Semicolon);
759                                Statement {
760                                    label: label.clone(),
761                                    kind: StmtKind::DoWhile { body, condition },
762                                    line,
763                                }
764                            } else {
765                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
766                                let inner = Expr {
767                                    kind: ExprKind::CodeRef {
768                                        params: vec![],
769                                        body,
770                                    },
771                                    line: inner_line,
772                                };
773                                let expr = Expr {
774                                    kind: ExprKind::Do(Box::new(inner)),
775                                    line,
776                                };
777                                let stmt = Statement {
778                                    label: label.clone(),
779                                    kind: StmtKind::Expression(expr),
780                                    line,
781                                };
782                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
783                                self.parse_stmt_postfix_modifier(stmt)?
784                            }
785                        } else {
786                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
787                            let inner = Expr {
788                                kind: ExprKind::CodeRef {
789                                    params: vec![],
790                                    body,
791                                },
792                                line: inner_line,
793                            };
794                            let expr = Expr {
795                                kind: ExprKind::Do(Box::new(inner)),
796                                line,
797                            };
798                            let stmt = Statement {
799                                label: label.clone(),
800                                kind: StmtKind::Expression(expr),
801                                line,
802                            };
803                            self.parse_stmt_postfix_modifier(stmt)?
804                        }
805                    } else {
806                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
807                            let stmt = self.maybe_postfix_modifier(expr)?;
808                            self.parse_stmt_postfix_modifier(stmt)?
809                        } else {
810                            let expr = self.parse_expression()?;
811                            let stmt = self.maybe_postfix_modifier(expr)?;
812                            self.parse_stmt_postfix_modifier(stmt)?
813                        }
814                    }
815                }
816                _ => {
817                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
818                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
819                        let stmt = self.maybe_postfix_modifier(expr)?;
820                        self.parse_stmt_postfix_modifier(stmt)?
821                    } else {
822                        let expr = self.parse_expression()?;
823                        let stmt = self.maybe_postfix_modifier(expr)?;
824                        self.parse_stmt_postfix_modifier(stmt)?
825                    }
826                }
827            },
828            Token::LBrace => {
829                let block = self.parse_block()?;
830                let stmt = Statement {
831                    label: None,
832                    kind: StmtKind::Block(block),
833                    line,
834                };
835                // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
836                self.parse_stmt_postfix_modifier(stmt)?
837            }
838            _ => {
839                let expr = self.parse_expression()?;
840                let stmt = self.maybe_postfix_modifier(expr)?;
841                self.parse_stmt_postfix_modifier(stmt)?
842            }
843        };
844
845        stmt.label = label;
846        Ok(stmt)
847    }
848
849    /// Handle postfix if/unless on statement-level keywords like last/next.
850    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
851        let line = stmt.line;
852        // Implicit semicolon: a modifier keyword on a new line is a new
853        // statement, not a postfix modifier.  This prevents semicolon-less
854        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
855        // as `my $x = "val" if ($x) { ... }`.
856        if self.peek_line() > self.prev_line() {
857            self.eat(&Token::Semicolon);
858            return Ok(stmt);
859        }
860        if let Token::Ident(ref kw) = self.peek().clone() {
861            match kw.as_str() {
862                "if" => {
863                    self.advance();
864                    let mut cond = self.parse_expression()?;
865                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
866                    self.eat(&Token::Semicolon);
867                    return Ok(Statement {
868                        label: None,
869                        kind: StmtKind::If {
870                            condition: cond,
871                            body: vec![stmt],
872                            elsifs: vec![],
873                            else_block: None,
874                        },
875                        line,
876                    });
877                }
878                "unless" => {
879                    self.advance();
880                    let mut cond = self.parse_expression()?;
881                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
882                    self.eat(&Token::Semicolon);
883                    return Ok(Statement {
884                        label: None,
885                        kind: StmtKind::Unless {
886                            condition: cond,
887                            body: vec![stmt],
888                            else_block: None,
889                        },
890                        line,
891                    });
892                }
893                "while" | "until" | "for" | "foreach" => {
894                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
895                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
896                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
897                        let out = self.maybe_postfix_modifier(expr)?;
898                        self.eat(&Token::Semicolon);
899                        return Ok(out);
900                    }
901                    return Err(self.syntax_err(
902                        format!("postfix `{}` is not supported on this statement form", kw),
903                        self.peek_line(),
904                    ));
905                }
906                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
907                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
908                    let line = stmt.line;
909                    let block = self.stmt_into_parallel_block(stmt)?;
910                    let which = kw.as_str();
911                    self.advance();
912                    self.eat(&Token::Comma);
913                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
914                    self.eat(&Token::Semicolon);
915                    let list = Box::new(list);
916                    let progress = progress.map(Box::new);
917                    let kind = match which {
918                        "pmap" => ExprKind::PMapExpr {
919                            block,
920                            list,
921                            progress,
922                            flat_outputs: false,
923                            on_cluster: None,
924                            stream: false,
925                        },
926                        "pflat_map" => ExprKind::PMapExpr {
927                            block,
928                            list,
929                            progress,
930                            flat_outputs: true,
931                            on_cluster: None,
932                            stream: false,
933                        },
934                        "pgrep" => ExprKind::PGrepExpr {
935                            block,
936                            list,
937                            progress,
938                            stream: false,
939                        },
940                        "pfor" => ExprKind::PForExpr {
941                            block,
942                            list,
943                            progress,
944                        },
945                        "preduce" => ExprKind::PReduceExpr {
946                            block,
947                            list,
948                            progress,
949                        },
950                        "pcache" => ExprKind::PcacheExpr {
951                            block,
952                            list,
953                            progress,
954                        },
955                        _ => unreachable!(),
956                    };
957                    return Ok(Statement {
958                        label: None,
959                        kind: StmtKind::Expression(Expr { kind, line }),
960                        line,
961                    });
962                }
963                _ => {}
964            }
965        }
966        self.eat(&Token::Semicolon);
967        Ok(stmt)
968    }
969
970    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
971    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
972    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
973        let line = stmt.line;
974        match stmt.kind {
975            StmtKind::Block(block) => Ok(block),
976            StmtKind::Expression(expr) => {
977                if let ExprKind::Do(ref inner) = expr.kind {
978                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
979                        return Ok(body.clone());
980                    }
981                }
982                Ok(vec![Statement {
983                    label: None,
984                    kind: StmtKind::Expression(expr),
985                    line,
986                }])
987            }
988            _ => Err(self.syntax_err(
989                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
990                line,
991            )),
992        }
993    }
994
995    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
996    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`](ExprKind::Do)([`CodeRef`](ExprKind::CodeRef))).
997    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
998        match stmt.kind {
999            StmtKind::Expression(expr) => Some(expr),
1000            StmtKind::Block(block) => {
1001                let line = stmt.line;
1002                let inner = Expr {
1003                    kind: ExprKind::CodeRef {
1004                        params: vec![],
1005                        body: block,
1006                    },
1007                    line,
1008                };
1009                Some(Expr {
1010                    kind: ExprKind::Do(Box::new(inner)),
1011                    line,
1012                })
1013            }
1014            _ => None,
1015        }
1016    }
1017
1018    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1019    /// (same set as [`parse_list_until_terminator`]).
1020    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1021        matches!(
1022            self.peek(),
1023            Token::Ident(ref kw)
1024                if matches!(
1025                    kw.as_str(),
1026                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1027                )
1028        )
1029    }
1030
1031    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1032        let line = expr.line;
1033        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1034        if self.peek_line() > self.prev_line() {
1035            return Ok(Statement {
1036                label: None,
1037                kind: StmtKind::Expression(expr),
1038                line,
1039            });
1040        }
1041        match self.peek() {
1042            Token::Ident(ref kw) => match kw.as_str() {
1043                "if" => {
1044                    self.advance();
1045                    let cond = self.parse_expression()?;
1046                    Ok(Statement {
1047                        label: None,
1048                        kind: StmtKind::Expression(Expr {
1049                            kind: ExprKind::PostfixIf {
1050                                expr: Box::new(expr),
1051                                condition: Box::new(cond),
1052                            },
1053                            line,
1054                        }),
1055                        line,
1056                    })
1057                }
1058                "unless" => {
1059                    self.advance();
1060                    let cond = self.parse_expression()?;
1061                    Ok(Statement {
1062                        label: None,
1063                        kind: StmtKind::Expression(Expr {
1064                            kind: ExprKind::PostfixUnless {
1065                                expr: Box::new(expr),
1066                                condition: Box::new(cond),
1067                            },
1068                            line,
1069                        }),
1070                        line,
1071                    })
1072                }
1073                "while" => {
1074                    self.advance();
1075                    let cond = self.parse_expression()?;
1076                    Ok(Statement {
1077                        label: None,
1078                        kind: StmtKind::Expression(Expr {
1079                            kind: ExprKind::PostfixWhile {
1080                                expr: Box::new(expr),
1081                                condition: Box::new(cond),
1082                            },
1083                            line,
1084                        }),
1085                        line,
1086                    })
1087                }
1088                "until" => {
1089                    self.advance();
1090                    let cond = self.parse_expression()?;
1091                    Ok(Statement {
1092                        label: None,
1093                        kind: StmtKind::Expression(Expr {
1094                            kind: ExprKind::PostfixUntil {
1095                                expr: Box::new(expr),
1096                                condition: Box::new(cond),
1097                            },
1098                            line,
1099                        }),
1100                        line,
1101                    })
1102                }
1103                "for" | "foreach" => {
1104                    self.advance();
1105                    let list = self.parse_expression()?;
1106                    Ok(Statement {
1107                        label: None,
1108                        kind: StmtKind::Expression(Expr {
1109                            kind: ExprKind::PostfixForeach {
1110                                expr: Box::new(expr),
1111                                list: Box::new(list),
1112                            },
1113                            line,
1114                        }),
1115                        line,
1116                    })
1117                }
1118                _ => Ok(Statement {
1119                    label: None,
1120                    kind: StmtKind::Expression(expr),
1121                    line,
1122                }),
1123            },
1124            _ => Ok(Statement {
1125                label: None,
1126                kind: StmtKind::Expression(expr),
1127                line,
1128            }),
1129        }
1130    }
1131
1132    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1133    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1134        let saved = self.pos;
1135        let line = self.peek_line();
1136        let mut name = match self.peek() {
1137            Token::Ident(n) => n.clone(),
1138            _ => return None,
1139        };
1140        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1141        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1142            return None;
1143        }
1144        self.advance();
1145        while self.eat(&Token::PackageSep) {
1146            match self.advance() {
1147                (Token::Ident(part), _) => {
1148                    name = format!("{}::{}", name, part);
1149                }
1150                _ => {
1151                    self.pos = saved;
1152                    return None;
1153                }
1154            }
1155        }
1156        match self.peek() {
1157            Token::Semicolon | Token::RBrace => Some(Expr {
1158                kind: ExprKind::FuncCall { name, args: vec![] },
1159                line,
1160            }),
1161            _ => {
1162                self.pos = saved;
1163                None
1164            }
1165        }
1166    }
1167
1168    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1169    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1170        !matches!(
1171            name,
1172            "__FILE__"
1173                | "__LINE__"
1174                | "abs"
1175                | "async"
1176                | "spawn"
1177                | "atan2"
1178                | "await"
1179                | "barrier"
1180                | "bless"
1181                | "caller"
1182                | "capture"
1183                | "cat"
1184                | "chdir"
1185                | "chmod"
1186                | "chomp"
1187                | "chop"
1188                | "chr"
1189                | "chown"
1190                | "closedir"
1191                | "close"
1192                | "collect"
1193                | "cos"
1194                | "crypt"
1195                | "defined"
1196                | "dec"
1197                | "delete"
1198                | "die"
1199                | "deque"
1200                | "do"
1201                | "each"
1202                | "eof"
1203                | "fore"
1204                | "eval"
1205                | "exec"
1206                | "exists"
1207                | "exit"
1208                | "exp"
1209                | "fan"
1210                | "fan_cap"
1211                | "fc"
1212                | "fetch_url"
1213                | "d"
1214                | "dirs"
1215                | "dr"
1216                | "f"
1217                | "files"
1218                | "filesf"
1219                | "filter"
1220                | "fr"
1221                | "getcwd"
1222                | "glob_par"
1223                | "par_sed"
1224                | "glob"
1225                | "grep"
1226                | "greps"
1227                | "heap"
1228                | "hex"
1229                | "inc"
1230                | "index"
1231                | "int"
1232                | "join"
1233                | "keys"
1234                | "lcfirst"
1235                | "lc"
1236                | "length"
1237                | "link"
1238                | "log"
1239                | "lstat"
1240                | "map"
1241                | "flat_map"
1242                | "maps"
1243                | "flat_maps"
1244                | "flatten"
1245                | "frequencies"
1246                | "freq"
1247                | "interleave"
1248                | "ddump"
1249                | "stringify"
1250                | "str"
1251                | "s"
1252                | "input"
1253                | "lines"
1254                | "words"
1255                | "chars"
1256                | "digits"
1257                | "letters"
1258                | "letters_uc"
1259                | "letters_lc"
1260                | "punctuation"
1261                | "sentences"
1262                | "paragraphs"
1263                | "sections"
1264                | "numbers"
1265                | "graphemes"
1266                | "columns"
1267                | "trim"
1268                | "avg"
1269                | "top"
1270                | "pager"
1271                | "pg"
1272                | "less"
1273                | "count_by"
1274                | "to_file"
1275                | "to_json"
1276                | "to_csv"
1277                | "grep_v"
1278                | "select_keys"
1279                | "pluck"
1280                | "clamp"
1281                | "normalize"
1282                | "stddev"
1283                | "squared"
1284                | "square"
1285                | "cubed"
1286                | "cube"
1287                | "expt"
1288                | "pow"
1289                | "pw"
1290                | "snake_case"
1291                | "camel_case"
1292                | "kebab_case"
1293                | "to_toml"
1294                | "to_yaml"
1295                | "to_xml"
1296                | "to_html"
1297                | "to_markdown"
1298                | "xopen"
1299                | "clip"
1300                | "paste"
1301                | "to_table"
1302                | "sparkline"
1303                | "bar_chart"
1304                | "flame"
1305                | "set"
1306                | "list_count"
1307                | "list_size"
1308                | "count"
1309                | "size"
1310                | "cnt"
1311                | "len"
1312                | "all"
1313                | "any"
1314                | "none"
1315                | "take_while"
1316                | "drop_while"
1317                | "skip_while"
1318                | "skip"
1319                | "first_or"
1320                | "tap"
1321                | "peek"
1322                | "partition"
1323                | "min_by"
1324                | "max_by"
1325                | "zip_with"
1326                | "group_by"
1327                | "chunk_by"
1328                | "with_index"
1329                | "puniq"
1330                | "pfirst"
1331                | "pany"
1332                | "uniq"
1333                | "distinct"
1334                | "shuffle"
1335                | "shuffled"
1336                | "chunked"
1337                | "windowed"
1338                | "match"
1339                | "mkdir"
1340                | "every"
1341                | "gen"
1342                | "oct"
1343                | "open"
1344                | "p"
1345                | "opendir"
1346                | "ord"
1347                | "par_lines"
1348                | "par_walk"
1349                | "pipe"
1350                | "pipes"
1351                | "block_devices"
1352                | "char_devices"
1353                | "rate_limit"
1354                | "retry"
1355                | "pcache"
1356                | "pchannel"
1357                | "pfor"
1358                | "pgrep"
1359                | "pgreps"
1360                | "pipeline"
1361                | "pmap_chunked"
1362                | "pmap_reduce"
1363                | "pmap_on"
1364                | "pflat_map_on"
1365                | "pmap"
1366                | "pmaps"
1367                | "pflat_map"
1368                | "pflat_maps"
1369                | "pop"
1370                | "pos"
1371                | "ppool"
1372                | "preduce_init"
1373                | "preduce"
1374                | "pselect"
1375                | "printf"
1376                | "print"
1377                | "pr"
1378                | "psort"
1379                | "push"
1380                | "pwatch"
1381                | "rand"
1382                | "readdir"
1383                | "readlink"
1384                | "reduce"
1385                | "fold"
1386                | "inject"
1387                | "first"
1388                | "detect"
1389                | "find"
1390                | "find_all"
1391                | "ref"
1392                | "rename"
1393                | "require"
1394                | "rev"
1395                | "reverse"
1396                | "reversed"
1397                | "rewinddir"
1398                | "rindex"
1399                | "rmdir"
1400                | "rm"
1401                | "say"
1402                | "scalar"
1403                | "seekdir"
1404                | "shift"
1405                | "sin"
1406                | "slurp"
1407                | "sockets"
1408                | "sort"
1409                | "splice"
1410                | "split"
1411                | "sprintf"
1412                | "sqrt"
1413                | "srand"
1414                | "stat"
1415                | "study"
1416                | "substr"
1417                | "symlink"
1418                | "sym_links"
1419                | "system"
1420                | "telldir"
1421                | "timer"
1422                | "trace"
1423                | "ucfirst"
1424                | "uc"
1425                | "undef"
1426                | "umask"
1427                | "unlink"
1428                | "unshift"
1429                | "utime"
1430                | "values"
1431                | "wantarray"
1432                | "warn"
1433                | "watch"
1434                | "yield"
1435                | "sub"
1436        )
1437    }
1438
1439    fn parse_block(&mut self) -> PerlResult<Block> {
1440        self.expect(&Token::LBrace)?;
1441        let mut stmts = Vec::new();
1442        // `{ |$a, $b| body }` — Ruby-style block params.
1443        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1444        // or `my $p = $_N` for positional N≥3.
1445        if let Some(param_stmts) = self.try_parse_block_params()? {
1446            stmts.extend(param_stmts);
1447        }
1448        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1449            if self.eat(&Token::Semicolon) {
1450                continue;
1451            }
1452            stmts.push(self.parse_statement()?);
1453        }
1454        self.expect(&Token::RBrace)?;
1455        Self::default_topic_for_sole_bareword(&mut stmts);
1456        Ok(stmts)
1457    }
1458
1459    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1460    /// Returns `None` if the leading `|` is not block-param syntax.
1461    /// When successful, returns `my $var = <implicit>` assignment statements
1462    /// that alias the block's positional arguments.
1463    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1464        if !matches!(self.peek(), Token::BitOr) {
1465            return Ok(None);
1466        }
1467        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1468        let mut i = 1; // skip the opening `|`
1469        loop {
1470            match self.peek_at(i) {
1471                Token::ScalarVar(_) => i += 1,
1472                _ => return Ok(None), // not `|$var...|`
1473            }
1474            match self.peek_at(i) {
1475                Token::BitOr => break,  // closing `|`
1476                Token::Comma => i += 1, // more params
1477                _ => return Ok(None),   // not block params
1478            }
1479        }
1480        // Confirmed — consume and build assignments.
1481        let line = self.peek_line();
1482        self.advance(); // eat opening `|`
1483        let mut names = Vec::new();
1484        loop {
1485            if let Token::ScalarVar(ref name) = self.peek().clone() {
1486                names.push(name.clone());
1487                self.advance();
1488            }
1489            if self.eat(&Token::BitOr) {
1490                break;
1491            }
1492            self.expect(&Token::Comma)?;
1493        }
1494        // Generate `my $name = <source>` for each param.
1495        // 1 param  → source is `$_` (map/grep/each/for topic)
1496        // 2 params → sources are `$a`, `$b` (sort/reduce)
1497        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1498        let sources: Vec<&str> = match names.len() {
1499            1 => vec!["_"],
1500            2 => vec!["a", "b"],
1501            n => {
1502                // Can't return borrowed from a generated vec, handle below.
1503                let _ = n;
1504                vec![] // sentinel — handled in the else branch
1505            }
1506        };
1507        let mut stmts = Vec::with_capacity(names.len());
1508        if !sources.is_empty() {
1509            for (name, src) in names.iter().zip(sources.iter()) {
1510                stmts.push(Statement {
1511                    label: None,
1512                    kind: StmtKind::My(vec![VarDecl {
1513                        sigil: Sigil::Scalar,
1514                        name: name.clone(),
1515                        initializer: Some(Expr {
1516                            kind: ExprKind::ScalarVar(src.to_string()),
1517                            line,
1518                        }),
1519                        frozen: false,
1520                        type_annotation: None,
1521                    }]),
1522                    line,
1523                });
1524            }
1525        } else {
1526            // N≥3: positional `$_`, `$_1`, `$_2`, …
1527            for (idx, name) in names.iter().enumerate() {
1528                let src = if idx == 0 {
1529                    "_".to_string()
1530                } else {
1531                    format!("_{idx}")
1532                };
1533                stmts.push(Statement {
1534                    label: None,
1535                    kind: StmtKind::My(vec![VarDecl {
1536                        sigil: Sigil::Scalar,
1537                        name: name.clone(),
1538                        initializer: Some(Expr {
1539                            kind: ExprKind::ScalarVar(src),
1540                            line,
1541                        }),
1542                        frozen: false,
1543                        type_annotation: None,
1544                    }]),
1545                    line,
1546                });
1547            }
1548        }
1549        Ok(Some(stmts))
1550    }
1551
1552    /// Block shorthand: when the body is literally one bare builtin call
1553    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1554    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1555    ///
1556    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1557    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1558    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1559    /// empty args and return the wrong value. This rewrite levels the
1560    /// playing field at parse time — no per-builtin handling needed.
1561    ///
1562    /// Narrow by design: fires only when the block has *exactly one*
1563    /// expression statement whose sole content is a known-bareword call
1564    /// with zero args. Multi-statement blocks and blocks with any other
1565    /// content are untouched.
1566    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1567        let [only] = stmts else { return };
1568        let StmtKind::Expression(ref mut expr) = only.kind else {
1569            return;
1570        };
1571        let topic_line = expr.line;
1572        let topic_arg = || Expr {
1573            kind: ExprKind::ScalarVar("_".to_string()),
1574            line: topic_line,
1575        };
1576        match expr.kind {
1577            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1578            ExprKind::FuncCall {
1579                ref name,
1580                ref mut args,
1581            } if args.is_empty()
1582                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1583            {
1584                args.push(topic_arg());
1585            }
1586            // Lone bareword (the parser sometimes keeps a bareword as a
1587            // `Bareword` node instead of a zero-arg `FuncCall` —
1588            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1589            ExprKind::Bareword(ref name)
1590                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1591            {
1592                let n = name.clone();
1593                expr.kind = ExprKind::FuncCall {
1594                    name: n,
1595                    args: vec![topic_arg()],
1596                };
1597            }
1598            _ => {}
1599        }
1600    }
1601
1602    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1603    /// Desugars to a `defer__internal(sub { BLOCK })` function call that the compiler
1604    /// handles specially by emitting Op::DeferBlock.
1605    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1606        let line = self.peek_line();
1607        self.advance(); // defer
1608        let body = self.parse_block()?;
1609        self.eat(&Token::Semicolon);
1610        // Desugar: defer { BLOCK } → defer__internal(sub { BLOCK })
1611        let coderef = Expr {
1612            kind: ExprKind::CodeRef {
1613                params: vec![],
1614                body,
1615            },
1616            line,
1617        };
1618        Ok(Statement {
1619            label: None,
1620            kind: StmtKind::Expression(Expr {
1621                kind: ExprKind::FuncCall {
1622                    name: "defer__internal".to_string(),
1623                    args: vec![coderef],
1624                },
1625                line,
1626            }),
1627            line,
1628        })
1629    }
1630
1631    /// `try { } catch ($err) { }` with optional `finally { }`
1632    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
1633        let line = self.peek_line();
1634        self.advance(); // try
1635        let try_block = self.parse_block()?;
1636        match self.peek() {
1637            Token::Ident(ref k) if k == "catch" => {
1638                self.advance();
1639            }
1640            _ => {
1641                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
1642            }
1643        }
1644        self.expect(&Token::LParen)?;
1645        let catch_var = self.parse_scalar_var_name()?;
1646        self.expect(&Token::RParen)?;
1647        let catch_block = self.parse_block()?;
1648        let finally_block = match self.peek() {
1649            Token::Ident(ref k) if k == "finally" => {
1650                self.advance();
1651                Some(self.parse_block()?)
1652            }
1653            _ => None,
1654        };
1655        self.eat(&Token::Semicolon);
1656        Ok(Statement {
1657            label: None,
1658            kind: StmtKind::TryCatch {
1659                try_block,
1660                catch_var,
1661                catch_block,
1662                finally_block,
1663            },
1664            line,
1665        })
1666    }
1667
1668    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
1669    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
1670    ///
1671    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
1672    /// is not parsed from tokens — using `parse_unary()` there lets the first
1673    /// bareword greedily consume the next token as its arg, which misparses
1674    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
1675    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
1676    /// token through the stage loop, and wrap the resulting chain in a
1677    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
1678    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
1679    fn parse_thread_macro(&mut self, _line: usize) -> PerlResult<Expr> {
1680        let pipe_rhs_wrap = self.in_pipe_rhs();
1681        let mut result = if pipe_rhs_wrap {
1682            Expr {
1683                kind: ExprKind::ArrayElement {
1684                    array: "_".to_string(),
1685                    index: Box::new(Expr {
1686                        kind: ExprKind::Integer(0),
1687                        line: _line,
1688                    }),
1689                },
1690                line: _line,
1691            }
1692        } else {
1693            // Suppress paren-less function calls so `t Color::Red p` parses
1694            // the enum variant without consuming `p` as an argument.
1695            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
1696            let expr = self.parse_thread_input();
1697            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
1698            expr?
1699        };
1700
1701        // Parse stages until we hit a statement terminator
1702        loop {
1703            // Check for terminators - |> ends thread and allows piping the result
1704            match self.peek() {
1705                Token::Semicolon
1706                | Token::Newline
1707                | Token::RBrace
1708                | Token::RParen
1709                | Token::RBracket
1710                | Token::PipeForward
1711                | Token::Eof => break,
1712                _ => {}
1713            }
1714
1715            let stage_line = self.peek_line();
1716
1717            // Parse a stage and apply it to result via pipe
1718            match self.peek().clone() {
1719                // `>{ block }` — standalone anonymous block (sugar for sub { })
1720                Token::ArrowBrace => {
1721                    self.advance(); // consume `>{`
1722                    let mut stmts = Vec::new();
1723                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1724                        if self.eat(&Token::Semicolon) {
1725                            continue;
1726                        }
1727                        stmts.push(self.parse_statement()?);
1728                    }
1729                    self.expect(&Token::RBrace)?;
1730                    let code_ref = Expr {
1731                        kind: ExprKind::CodeRef {
1732                            params: vec![],
1733                            body: stmts,
1734                        },
1735                        line: stage_line,
1736                    };
1737                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1738                }
1739                // `sub { block }` or `fn { block }` — explicit anonymous block
1740                Token::Ident(ref name) if name == "sub" || name == "fn" => {
1741                    self.advance(); // consume `sub`
1742                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
1743                    let body = self.parse_block()?;
1744                    let code_ref = Expr {
1745                        kind: ExprKind::CodeRef { params, body },
1746                        line: stage_line,
1747                    };
1748                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1749                }
1750                // `ident` possibly followed by block
1751                Token::Ident(ref name) => {
1752                    let func_name = name.clone();
1753                    self.advance();
1754
1755                    // Handle s/// and tr/// encoded tokens
1756                    if func_name.starts_with('\x00') {
1757                        let parts: Vec<&str> = func_name.split('\x00').collect();
1758                        if parts.len() >= 4 && parts[1] == "s" {
1759                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1760                            let stage = Expr {
1761                                kind: ExprKind::Substitution {
1762                                    expr: Box::new(result.clone()),
1763                                    pattern: parts[2].to_string(),
1764                                    replacement: parts[3].to_string(),
1765                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1766                                    delim,
1767                                },
1768                                line: stage_line,
1769                            };
1770                            result = stage;
1771                            continue;
1772                        }
1773                        if parts.len() >= 4 && parts[1] == "tr" {
1774                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1775                            let stage = Expr {
1776                                kind: ExprKind::Transliterate {
1777                                    expr: Box::new(result.clone()),
1778                                    from: parts[2].to_string(),
1779                                    to: parts[3].to_string(),
1780                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1781                                    delim,
1782                                },
1783                                line: stage_line,
1784                            };
1785                            result = stage;
1786                            continue;
1787                        }
1788                        return Err(
1789                            self.syntax_err("Unexpected encoded token in thread", stage_line)
1790                        );
1791                    }
1792
1793                    // `map +{ ... }` — hashref expression form (not a code block).
1794                    // The `+` disambiguates: `+{` is always a hashref constructor.
1795                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
1796                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
1797                    if matches!(self.peek(), Token::Plus)
1798                        && matches!(self.peek_at(1), Token::LBrace)
1799                    {
1800                        self.advance(); // consume `+`
1801                        self.expect(&Token::LBrace)?;
1802                        // try_parse_hash_ref consumes the closing `}`
1803                        let pairs = self.try_parse_hash_ref()?;
1804                        let hashref_expr = Expr {
1805                            kind: ExprKind::HashRef(pairs),
1806                            line: stage_line,
1807                        };
1808                        let flatten_array_refs =
1809                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
1810                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
1811                        // Placeholder list — pipe_forward_apply replaces it with `result`.
1812                        let placeholder = Expr {
1813                            kind: ExprKind::Undef,
1814                            line: stage_line,
1815                        };
1816                        let map_node = Expr {
1817                            kind: ExprKind::MapExprComma {
1818                                expr: Box::new(hashref_expr),
1819                                list: Box::new(placeholder),
1820                                flatten_array_refs,
1821                                stream,
1822                            },
1823                            line: stage_line,
1824                        };
1825                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
1826                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
1827                    } else if func_name == "pmap_chunked" {
1828                        let chunk_size = self.parse_assign_expr()?;
1829                        let block = self.parse_block_or_bareword_block()?;
1830                        let placeholder = self.pipe_placeholder_list(stage_line);
1831                        let stage = Expr {
1832                            kind: ExprKind::PMapChunkedExpr {
1833                                chunk_size: Box::new(chunk_size),
1834                                block,
1835                                list: Box::new(placeholder),
1836                                progress: None,
1837                            },
1838                            line: stage_line,
1839                        };
1840                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1841                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
1842                    } else if func_name == "preduce_init" {
1843                        let init = self.parse_assign_expr()?;
1844                        let block = self.parse_block_or_bareword_block()?;
1845                        let placeholder = self.pipe_placeholder_list(stage_line);
1846                        let stage = Expr {
1847                            kind: ExprKind::PReduceInitExpr {
1848                                init: Box::new(init),
1849                                block,
1850                                list: Box::new(placeholder),
1851                                progress: None,
1852                            },
1853                            line: stage_line,
1854                        };
1855                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1856                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
1857                    } else if func_name == "pmap_reduce" {
1858                        let map_block = self.parse_block_or_bareword_block()?;
1859                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
1860                            self.parse_block()?
1861                        } else {
1862                            self.expect(&Token::Comma)?;
1863                            self.parse_block_or_bareword_cmp_block()?
1864                        };
1865                        let placeholder = self.pipe_placeholder_list(stage_line);
1866                        let stage = Expr {
1867                            kind: ExprKind::PMapReduceExpr {
1868                                map_block,
1869                                reduce_block,
1870                                list: Box::new(placeholder),
1871                                progress: None,
1872                            },
1873                            line: stage_line,
1874                        };
1875                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1876                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
1877                    } else if matches!(self.peek(), Token::LBrace) {
1878                        // Parse as a block-taking builtin
1879                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
1880                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
1881                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
1882                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1883                    } else if matches!(self.peek(), Token::LParen) {
1884                        // `name($_-bearing-args)` — parse explicit args, require at
1885                        // least one `$_` placeholder, then wrap as a `>{...}` block
1886                        // so the threaded value binds to `$_` at any position.
1887                        // Examples:
1888                        //   t 10 add2($_, 5) p      → add2(10, 5)
1889                        //   t 10 sub2(20, $_) p     → sub2(20, 10)
1890                        //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
1891                        // To pass the threaded value as a sole arg, use bare form:
1892                        //   t 10 add2 p   (not `add2()`)
1893                        self.advance(); // consume `(`
1894                        let mut call_args = Vec::new();
1895                        while !matches!(self.peek(), Token::RParen | Token::Eof) {
1896                            call_args.push(self.parse_assign_expr()?);
1897                            if !self.eat(&Token::Comma) {
1898                                break;
1899                            }
1900                        }
1901                        self.expect(&Token::RParen)?;
1902                        // If no `$_` placeholder, auto-inject threaded value as first arg.
1903                        // `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
1904                        if !call_args.iter().any(Self::expr_contains_topic_var) {
1905                            call_args.insert(
1906                                0,
1907                                Expr {
1908                                    kind: ExprKind::ScalarVar("_".to_string()),
1909                                    line: stage_line,
1910                                },
1911                            );
1912                        }
1913                        let call_expr = Expr {
1914                            kind: ExprKind::FuncCall {
1915                                name: func_name.clone(),
1916                                args: call_args,
1917                            },
1918                            line: stage_line,
1919                        };
1920                        let code_ref = Expr {
1921                            kind: ExprKind::CodeRef {
1922                                params: vec![],
1923                                body: vec![Statement {
1924                                    label: None,
1925                                    kind: StmtKind::Expression(call_expr),
1926                                    line: stage_line,
1927                                }],
1928                            },
1929                            line: stage_line,
1930                        };
1931                        result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1932                    } else {
1933                        // Bare function name — handle unary builtins specially
1934                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
1935                    }
1936                }
1937                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
1938                Token::Regex(ref pattern, ref flags, delim) => {
1939                    let pattern = pattern.clone();
1940                    let flags = flags.clone();
1941                    self.advance();
1942                    result =
1943                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
1944                }
1945                // Handle `/` that was lexed as Slash (division) because it followed a term.
1946                // In thread stage context, `/pattern/` should be a regex filter.
1947                Token::Slash => {
1948                    self.advance(); // consume opening /
1949
1950                    // Special case: if next token is Ident("m") or similar followed by Regex,
1951                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
1952                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
1953                    if let Token::Ident(ref ident_s) = self.peek().clone() {
1954                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
1955                            && matches!(self.peek_at(1), Token::Regex(..))
1956                        {
1957                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
1958                            self.advance(); // consume the ident
1959                                            // The Token::Regex after it was a misparsed `m/...` - we need to
1960                                            // extract what would have been the closing `/` situation.
1961                                            // Actually, the lexer consumed everything. Let's just use the ident
1962                                            // as the pattern and expect a closing slash.
1963                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
1964                                self.peek().clone()
1965                            {
1966                                // The misparsed regex ate our closing `/`.
1967                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
1968                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
1969                                // interprets as m// regex start, reads until next `/` (none) -> error.
1970                                // So we shouldn't reach here if there was an error.
1971                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
1972                                // This is getting complicated. Let me try a different approach.
1973                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
1974                                // Skip for now and fall through to manual parsing.
1975                                let _ = (misparsed_pattern, misparsed_flags);
1976                            }
1977                        }
1978                    }
1979
1980                    // Manually parse the regex pattern from tokens until we hit another Slash
1981                    let mut pattern = String::new();
1982                    loop {
1983                        match self.peek().clone() {
1984                            Token::Slash => {
1985                                self.advance(); // consume closing /
1986                                break;
1987                            }
1988                            Token::Eof | Token::Semicolon | Token::Newline => {
1989                                return Err(self
1990                                    .syntax_err("Unterminated regex in thread stage", stage_line));
1991                            }
1992                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
1993                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
1994                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
1995                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
1996                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
1997                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
1998                                // treat the previous ident as the whole pattern and this Regex as misparsed.
1999                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2000                                // This is a lexer bug workaround.
2001                                if pattern.is_empty()
2002                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2003                                {
2004                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2005                                    // and lexer misparsed. The Regex token is garbage.
2006                                    // Just use the ident as pattern and ignore this Regex.
2007                                    // But we already advanced past the ident...
2008                                    // This is messy. Let me try a cleaner approach.
2009                                    let _ = (inner_pattern, inner_flags, delim);
2010                                }
2011                                // For now, error out - this case is too complex
2012                                return Err(self.syntax_err(
2013                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2014                                    stage_line,
2015                                ));
2016                            }
2017                            Token::Ident(ref s) => {
2018                                pattern.push_str(s);
2019                                self.advance();
2020                            }
2021                            Token::Integer(n) => {
2022                                pattern.push_str(&n.to_string());
2023                                self.advance();
2024                            }
2025                            Token::ScalarVar(ref v) => {
2026                                pattern.push('$');
2027                                pattern.push_str(v);
2028                                self.advance();
2029                            }
2030                            Token::Dot => {
2031                                pattern.push('.');
2032                                self.advance();
2033                            }
2034                            Token::Star => {
2035                                pattern.push('*');
2036                                self.advance();
2037                            }
2038                            Token::Plus => {
2039                                pattern.push('+');
2040                                self.advance();
2041                            }
2042                            Token::Question => {
2043                                pattern.push('?');
2044                                self.advance();
2045                            }
2046                            Token::LParen => {
2047                                pattern.push('(');
2048                                self.advance();
2049                            }
2050                            Token::RParen => {
2051                                pattern.push(')');
2052                                self.advance();
2053                            }
2054                            Token::LBracket => {
2055                                pattern.push('[');
2056                                self.advance();
2057                            }
2058                            Token::RBracket => {
2059                                pattern.push(']');
2060                                self.advance();
2061                            }
2062                            Token::Backslash => {
2063                                pattern.push('\\');
2064                                self.advance();
2065                            }
2066                            Token::BitOr => {
2067                                pattern.push('|');
2068                                self.advance();
2069                            }
2070                            Token::Power => {
2071                                pattern.push_str("**");
2072                                self.advance();
2073                            }
2074                            Token::BitXor => {
2075                                pattern.push('^');
2076                                self.advance();
2077                            }
2078                            Token::Minus => {
2079                                pattern.push('-');
2080                                self.advance();
2081                            }
2082                            _ => {
2083                                return Err(self.syntax_err(
2084                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2085                                    stage_line,
2086                                ));
2087                            }
2088                        }
2089                    }
2090                    // Parse optional flags (sequence of letters after closing /)
2091                    // Be careful: single letters like 'e' could be regex flags OR thread
2092                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2093                    let mut flags = String::new();
2094                    if let Token::Ident(ref s) = self.peek().clone() {
2095                        let is_flag_only =
2096                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2097                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2098                        if is_flag_only && !followed_by_brace {
2099                            flags.push_str(s);
2100                            self.advance();
2101                        }
2102                    }
2103                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2104                }
2105                tok => {
2106                    return Err(self.syntax_err(
2107                        format!(
2108                            "thread: expected stage (ident, sub {{}}, s///, tr///, or /re/), got {:?}",
2109                            tok
2110                        ),
2111                        stage_line,
2112                    ));
2113                }
2114            };
2115        }
2116
2117        if pipe_rhs_wrap {
2118            // Wrap as `sub { …stages threaded from $_[0]… }` so the outer
2119            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2120            let body_line = result.line;
2121            return Ok(Expr {
2122                kind: ExprKind::CodeRef {
2123                    params: vec![],
2124                    body: vec![Statement {
2125                        label: None,
2126                        kind: StmtKind::Expression(result),
2127                        line: body_line,
2128                    }],
2129                },
2130                line: _line,
2131            });
2132        }
2133        Ok(result)
2134    }
2135
2136    /// Build a grep filter stage from a regex pattern for the thread macro.
2137    fn thread_regex_grep_stage(
2138        &self,
2139        list: Expr,
2140        pattern: String,
2141        flags: String,
2142        delim: char,
2143        line: usize,
2144    ) -> Expr {
2145        let topic = Expr {
2146            kind: ExprKind::ScalarVar("_".to_string()),
2147            line,
2148        };
2149        let match_expr = Expr {
2150            kind: ExprKind::Match {
2151                expr: Box::new(topic),
2152                pattern,
2153                flags,
2154                scalar_g: false,
2155                delim,
2156            },
2157            line,
2158        };
2159        let block = vec![Statement {
2160            label: None,
2161            kind: StmtKind::Expression(match_expr),
2162            line,
2163        }];
2164        Expr {
2165            kind: ExprKind::GrepExpr {
2166                block,
2167                list: Box::new(list),
2168                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2169            },
2170            line,
2171        }
2172    }
2173
2174    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2175    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2176    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2177    /// must appear in the args, otherwise the threaded value is silently dropped.
2178    ///
2179    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2180    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2181    /// per-variant walker that would need to be updated whenever new `ExprKind`
2182    /// variants are added (and would silently miss any it forgot to handle).
2183    /// Parse-time perf is non-critical and the AST is small at this scope.
2184    fn expr_contains_topic_var(e: &Expr) -> bool {
2185        format!("{:?}", e).contains("ScalarVar(\"_\")")
2186    }
2187
2188    /// Apply a bare function name in thread context, handling unary builtins specially.
2189    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2190        let kind = match name {
2191            // String functions
2192            "uc" => ExprKind::Uc(Box::new(arg)),
2193            "lc" => ExprKind::Lc(Box::new(arg)),
2194            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2195            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2196            "fc" => ExprKind::Fc(Box::new(arg)),
2197            "chomp" => ExprKind::Chomp(Box::new(arg)),
2198            "chop" => ExprKind::Chop(Box::new(arg)),
2199            "length" => ExprKind::Length(Box::new(arg)),
2200            "len" | "cnt" => ExprKind::FuncCall {
2201                name: "count".to_string(),
2202                args: vec![arg],
2203            },
2204            "quotemeta" | "qm" => ExprKind::FuncCall {
2205                name: "quotemeta".to_string(),
2206                args: vec![arg],
2207            },
2208            // Numeric functions
2209            "abs" => ExprKind::Abs(Box::new(arg)),
2210            "int" => ExprKind::Int(Box::new(arg)),
2211            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2212            "sin" => ExprKind::Sin(Box::new(arg)),
2213            "cos" => ExprKind::Cos(Box::new(arg)),
2214            "exp" => ExprKind::Exp(Box::new(arg)),
2215            "log" => ExprKind::Log(Box::new(arg)),
2216            "hex" => ExprKind::Hex(Box::new(arg)),
2217            "oct" => ExprKind::Oct(Box::new(arg)),
2218            "chr" => ExprKind::Chr(Box::new(arg)),
2219            "ord" => ExprKind::Ord(Box::new(arg)),
2220            // Type/ref functions
2221            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2222            "ref" => ExprKind::Ref(Box::new(arg)),
2223            "scalar" => ExprKind::ScalarContext(Box::new(arg)),
2224            // Array/hash functions
2225            "keys" => ExprKind::Keys(Box::new(arg)),
2226            "values" => ExprKind::Values(Box::new(arg)),
2227            "each" => ExprKind::Each(Box::new(arg)),
2228            "pop" => ExprKind::Pop(Box::new(arg)),
2229            "shift" => ExprKind::Shift(Box::new(arg)),
2230            "reverse" | "reversed" | "rv" => ExprKind::ReverseExpr(Box::new(arg)),
2231            "rev" => ExprKind::ScalarReverse(Box::new(arg)),
2232            "sort" | "so" => ExprKind::SortExpr {
2233                cmp: None,
2234                list: Box::new(arg),
2235            },
2236            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2237                name: "uniq".to_string(),
2238                args: vec![arg],
2239            },
2240            "trim" | "tm" => ExprKind::FuncCall {
2241                name: "trim".to_string(),
2242                args: vec![arg],
2243            },
2244            "flatten" | "fl" => ExprKind::FuncCall {
2245                name: "flatten".to_string(),
2246                args: vec![arg],
2247            },
2248            "compact" | "cpt" => ExprKind::FuncCall {
2249                name: "compact".to_string(),
2250                args: vec![arg],
2251            },
2252            "shuffle" | "shuf" => ExprKind::FuncCall {
2253                name: "shuffle".to_string(),
2254                args: vec![arg],
2255            },
2256            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2257                name: "frequencies".to_string(),
2258                args: vec![arg],
2259            },
2260            "dedup" | "dup" => ExprKind::FuncCall {
2261                name: "dedup".to_string(),
2262                args: vec![arg],
2263            },
2264            "enumerate" | "en" => ExprKind::FuncCall {
2265                name: "enumerate".to_string(),
2266                args: vec![arg],
2267            },
2268            "lines" | "ln" => ExprKind::FuncCall {
2269                name: "lines".to_string(),
2270                args: vec![arg],
2271            },
2272            "words" | "wd" => ExprKind::FuncCall {
2273                name: "words".to_string(),
2274                args: vec![arg],
2275            },
2276            "chars" | "ch" => ExprKind::FuncCall {
2277                name: "chars".to_string(),
2278                args: vec![arg],
2279            },
2280            "digits" | "dg" => ExprKind::FuncCall {
2281                name: "digits".to_string(),
2282                args: vec![arg],
2283            },
2284            "letters" | "lts" => ExprKind::FuncCall {
2285                name: "letters".to_string(),
2286                args: vec![arg],
2287            },
2288            "letters_uc" => ExprKind::FuncCall {
2289                name: "letters_uc".to_string(),
2290                args: vec![arg],
2291            },
2292            "letters_lc" => ExprKind::FuncCall {
2293                name: "letters_lc".to_string(),
2294                args: vec![arg],
2295            },
2296            "punctuation" | "punct" => ExprKind::FuncCall {
2297                name: "punctuation".to_string(),
2298                args: vec![arg],
2299            },
2300            "sentences" | "sents" => ExprKind::FuncCall {
2301                name: "sentences".to_string(),
2302                args: vec![arg],
2303            },
2304            "paragraphs" | "paras" => ExprKind::FuncCall {
2305                name: "paragraphs".to_string(),
2306                args: vec![arg],
2307            },
2308            "sections" | "sects" => ExprKind::FuncCall {
2309                name: "sections".to_string(),
2310                args: vec![arg],
2311            },
2312            "numbers" | "nums" => ExprKind::FuncCall {
2313                name: "numbers".to_string(),
2314                args: vec![arg],
2315            },
2316            "graphemes" | "grs" => ExprKind::FuncCall {
2317                name: "graphemes".to_string(),
2318                args: vec![arg],
2319            },
2320            "columns" | "cols" => ExprKind::FuncCall {
2321                name: "columns".to_string(),
2322                args: vec![arg],
2323            },
2324            // File functions
2325            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2326            "chdir" => ExprKind::Chdir(Box::new(arg)),
2327            "stat" => ExprKind::Stat(Box::new(arg)),
2328            "lstat" => ExprKind::Lstat(Box::new(arg)),
2329            "readlink" => ExprKind::Readlink(Box::new(arg)),
2330            "readdir" => ExprKind::Readdir(Box::new(arg)),
2331            "close" => ExprKind::Close(Box::new(arg)),
2332            "basename" | "bn" => ExprKind::FuncCall {
2333                name: "basename".to_string(),
2334                args: vec![arg],
2335            },
2336            "dirname" | "dn" => ExprKind::FuncCall {
2337                name: "dirname".to_string(),
2338                args: vec![arg],
2339            },
2340            "realpath" | "rp" => ExprKind::FuncCall {
2341                name: "realpath".to_string(),
2342                args: vec![arg],
2343            },
2344            "which" | "wh" => ExprKind::FuncCall {
2345                name: "which".to_string(),
2346                args: vec![arg],
2347            },
2348            // Other
2349            "eval" => ExprKind::Eval(Box::new(arg)),
2350            "require" => ExprKind::Require(Box::new(arg)),
2351            "study" => ExprKind::Study(Box::new(arg)),
2352            // Case conversion
2353            "snake_case" | "sc" => ExprKind::FuncCall {
2354                name: "snake_case".to_string(),
2355                args: vec![arg],
2356            },
2357            "camel_case" | "cc" => ExprKind::FuncCall {
2358                name: "camel_case".to_string(),
2359                args: vec![arg],
2360            },
2361            "kebab_case" | "kc" => ExprKind::FuncCall {
2362                name: "kebab_case".to_string(),
2363                args: vec![arg],
2364            },
2365            // Serialization
2366            "to_json" | "tj" => ExprKind::FuncCall {
2367                name: "to_json".to_string(),
2368                args: vec![arg],
2369            },
2370            "to_yaml" | "ty" => ExprKind::FuncCall {
2371                name: "to_yaml".to_string(),
2372                args: vec![arg],
2373            },
2374            "to_toml" | "tt" => ExprKind::FuncCall {
2375                name: "to_toml".to_string(),
2376                args: vec![arg],
2377            },
2378            "to_csv" | "tc" => ExprKind::FuncCall {
2379                name: "to_csv".to_string(),
2380                args: vec![arg],
2381            },
2382            "to_xml" | "tx" => ExprKind::FuncCall {
2383                name: "to_xml".to_string(),
2384                args: vec![arg],
2385            },
2386            "to_html" | "th" => ExprKind::FuncCall {
2387                name: "to_html".to_string(),
2388                args: vec![arg],
2389            },
2390            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2391                name: "to_markdown".to_string(),
2392                args: vec![arg],
2393            },
2394            "xopen" | "xo" => ExprKind::FuncCall {
2395                name: "xopen".to_string(),
2396                args: vec![arg],
2397            },
2398            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2399                name: "clip".to_string(),
2400                args: vec![arg],
2401            },
2402            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2403                name: "to_table".to_string(),
2404                args: vec![arg],
2405            },
2406            "sparkline" | "spark" => ExprKind::FuncCall {
2407                name: "sparkline".to_string(),
2408                args: vec![arg],
2409            },
2410            "bar_chart" | "bars" => ExprKind::FuncCall {
2411                name: "bar_chart".to_string(),
2412                args: vec![arg],
2413            },
2414            "flame" | "flamechart" => ExprKind::FuncCall {
2415                name: "flame".to_string(),
2416                args: vec![arg],
2417            },
2418            "ddump" | "dd" => ExprKind::FuncCall {
2419                name: "ddump".to_string(),
2420                args: vec![arg],
2421            },
2422            "stringify" | "str" => ExprKind::FuncCall {
2423                name: "stringify".to_string(),
2424                args: vec![arg],
2425            },
2426            "json_decode" | "jd" => ExprKind::FuncCall {
2427                name: "json_decode".to_string(),
2428                args: vec![arg],
2429            },
2430            "yaml_decode" | "yd" => ExprKind::FuncCall {
2431                name: "yaml_decode".to_string(),
2432                args: vec![arg],
2433            },
2434            "toml_decode" | "td" => ExprKind::FuncCall {
2435                name: "toml_decode".to_string(),
2436                args: vec![arg],
2437            },
2438            "xml_decode" | "xd" => ExprKind::FuncCall {
2439                name: "xml_decode".to_string(),
2440                args: vec![arg],
2441            },
2442            "json_encode" | "je" => ExprKind::FuncCall {
2443                name: "json_encode".to_string(),
2444                args: vec![arg],
2445            },
2446            "yaml_encode" | "ye" => ExprKind::FuncCall {
2447                name: "yaml_encode".to_string(),
2448                args: vec![arg],
2449            },
2450            "toml_encode" | "te" => ExprKind::FuncCall {
2451                name: "toml_encode".to_string(),
2452                args: vec![arg],
2453            },
2454            "xml_encode" | "xe" => ExprKind::FuncCall {
2455                name: "xml_encode".to_string(),
2456                args: vec![arg],
2457            },
2458            // Encoding
2459            "base64_encode" | "b64e" => ExprKind::FuncCall {
2460                name: "base64_encode".to_string(),
2461                args: vec![arg],
2462            },
2463            "base64_decode" | "b64d" => ExprKind::FuncCall {
2464                name: "base64_decode".to_string(),
2465                args: vec![arg],
2466            },
2467            "hex_encode" | "hxe" => ExprKind::FuncCall {
2468                name: "hex_encode".to_string(),
2469                args: vec![arg],
2470            },
2471            "hex_decode" | "hxd" => ExprKind::FuncCall {
2472                name: "hex_decode".to_string(),
2473                args: vec![arg],
2474            },
2475            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
2476                name: "url_encode".to_string(),
2477                args: vec![arg],
2478            },
2479            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
2480                name: "url_decode".to_string(),
2481                args: vec![arg],
2482            },
2483            "gzip" | "gz" => ExprKind::FuncCall {
2484                name: "gzip".to_string(),
2485                args: vec![arg],
2486            },
2487            "gunzip" | "ugz" => ExprKind::FuncCall {
2488                name: "gunzip".to_string(),
2489                args: vec![arg],
2490            },
2491            "zstd" | "zst" => ExprKind::FuncCall {
2492                name: "zstd".to_string(),
2493                args: vec![arg],
2494            },
2495            "zstd_decode" | "uzst" => ExprKind::FuncCall {
2496                name: "zstd_decode".to_string(),
2497                args: vec![arg],
2498            },
2499            // Crypto
2500            "sha256" | "s256" => ExprKind::FuncCall {
2501                name: "sha256".to_string(),
2502                args: vec![arg],
2503            },
2504            "sha1" | "s1" => ExprKind::FuncCall {
2505                name: "sha1".to_string(),
2506                args: vec![arg],
2507            },
2508            "md5" | "m5" => ExprKind::FuncCall {
2509                name: "md5".to_string(),
2510                args: vec![arg],
2511            },
2512            "uuid" | "uid" => ExprKind::FuncCall {
2513                name: "uuid".to_string(),
2514                args: vec![arg],
2515            },
2516            // Datetime
2517            "datetime_utc" | "utc" => ExprKind::FuncCall {
2518                name: "datetime_utc".to_string(),
2519                args: vec![arg],
2520            },
2521            // Output
2522            "p" | "say" => ExprKind::Say {
2523                handle: None,
2524                args: vec![arg],
2525            },
2526            "print" | "pr" => ExprKind::Print {
2527                handle: None,
2528                args: vec![arg],
2529            },
2530            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
2531            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
2532            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
2533                block: vec![Statement {
2534                    label: None,
2535                    kind: StmtKind::Expression(Expr {
2536                        kind: ExprKind::Say {
2537                            handle: None,
2538                            args: vec![Expr {
2539                                kind: ExprKind::ScalarVar("_".into()),
2540                                line,
2541                            }],
2542                        },
2543                        line,
2544                    }),
2545                    line,
2546                }],
2547                list: Box::new(arg),
2548            },
2549            // Default: generic function call
2550            _ => ExprKind::FuncCall {
2551                name: name.to_string(),
2552                args: vec![arg],
2553            },
2554        };
2555        Ok(Expr { kind, line })
2556    }
2557
2558    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
2559    /// In thread context, we only parse the block - the list comes from the piped result.
2560    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
2561        let block = self.parse_block()?;
2562        // Use a placeholder for the list - pipe_forward_apply will replace it
2563        let placeholder = self.pipe_placeholder_list(line);
2564
2565        match name {
2566            "map" | "flat_map" | "maps" | "flat_maps" => {
2567                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
2568                let stream = matches!(name, "maps" | "flat_maps");
2569                Ok(Expr {
2570                    kind: ExprKind::MapExpr {
2571                        block,
2572                        list: Box::new(placeholder),
2573                        flatten_array_refs,
2574                        stream,
2575                    },
2576                    line,
2577                })
2578            }
2579            "grep" | "greps" | "filter" | "f" | "find_all" | "gr" => {
2580                let keyword = match name {
2581                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
2582                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
2583                    "filter" | "f" => crate::ast::GrepBuiltinKeyword::Filter,
2584                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
2585                    _ => unreachable!(),
2586                };
2587                Ok(Expr {
2588                    kind: ExprKind::GrepExpr {
2589                        block,
2590                        list: Box::new(placeholder),
2591                        keyword,
2592                    },
2593                    line,
2594                })
2595            }
2596            "sort" | "so" => Ok(Expr {
2597                kind: ExprKind::SortExpr {
2598                    cmp: Some(SortComparator::Block(block)),
2599                    list: Box::new(placeholder),
2600                },
2601                line,
2602            }),
2603            "reduce" | "rd" => Ok(Expr {
2604                kind: ExprKind::ReduceExpr {
2605                    block,
2606                    list: Box::new(placeholder),
2607                },
2608                line,
2609            }),
2610            "fore" | "e" | "ep" => Ok(Expr {
2611                kind: ExprKind::ForEachExpr {
2612                    block,
2613                    list: Box::new(placeholder),
2614                },
2615                line,
2616            }),
2617            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
2618                kind: ExprKind::PMapExpr {
2619                    block,
2620                    list: Box::new(placeholder),
2621                    progress: None,
2622                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
2623                    on_cluster: None,
2624                    stream: name == "pmaps" || name == "pflat_maps",
2625                },
2626                line,
2627            }),
2628            "pgrep" | "pgreps" => Ok(Expr {
2629                kind: ExprKind::PGrepExpr {
2630                    block,
2631                    list: Box::new(placeholder),
2632                    progress: None,
2633                    stream: name == "pgreps",
2634                },
2635                line,
2636            }),
2637            "pfor" => Ok(Expr {
2638                kind: ExprKind::PForExpr {
2639                    block,
2640                    list: Box::new(placeholder),
2641                    progress: None,
2642                },
2643                line,
2644            }),
2645            "preduce" => Ok(Expr {
2646                kind: ExprKind::PReduceExpr {
2647                    block,
2648                    list: Box::new(placeholder),
2649                    progress: None,
2650                },
2651                line,
2652            }),
2653            "pcache" => Ok(Expr {
2654                kind: ExprKind::PcacheExpr {
2655                    block,
2656                    list: Box::new(placeholder),
2657                    progress: None,
2658                },
2659                line,
2660            }),
2661            "psort" => Ok(Expr {
2662                kind: ExprKind::PSortExpr {
2663                    cmp: Some(block),
2664                    list: Box::new(placeholder),
2665                    progress: None,
2666                },
2667                line,
2668            }),
2669            _ => {
2670                // Generic: parse block and treat as FuncCall with code ref arg
2671                let code_ref = Expr {
2672                    kind: ExprKind::CodeRef {
2673                        params: vec![],
2674                        body: block,
2675                    },
2676                    line,
2677                };
2678                Ok(Expr {
2679                    kind: ExprKind::FuncCall {
2680                        name: name.to_string(),
2681                        args: vec![code_ref],
2682                    },
2683                    line,
2684                })
2685            }
2686        }
2687    }
2688
2689    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
2690    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
2691        let line = self.peek_line();
2692        self.advance(); // tie
2693        let target = match self.peek().clone() {
2694            Token::HashVar(h) => {
2695                self.advance();
2696                TieTarget::Hash(h)
2697            }
2698            Token::ArrayVar(a) => {
2699                self.advance();
2700                TieTarget::Array(a)
2701            }
2702            Token::ScalarVar(s) => {
2703                self.advance();
2704                TieTarget::Scalar(s)
2705            }
2706            tok => {
2707                return Err(self.syntax_err(
2708                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
2709                    self.peek_line(),
2710                ));
2711            }
2712        };
2713        self.expect(&Token::Comma)?;
2714        let class = self.parse_assign_expr()?;
2715        let mut args = Vec::new();
2716        while self.eat(&Token::Comma) {
2717            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
2718                break;
2719            }
2720            args.push(self.parse_assign_expr()?);
2721        }
2722        self.eat(&Token::Semicolon);
2723        Ok(Statement {
2724            label: None,
2725            kind: StmtKind::Tie {
2726                target,
2727                class,
2728                args,
2729            },
2730            line,
2731        })
2732    }
2733
2734    /// `given (EXPR) { ... }`
2735    fn parse_given(&mut self) -> PerlResult<Statement> {
2736        let line = self.peek_line();
2737        self.advance();
2738        self.expect(&Token::LParen)?;
2739        let topic = self.parse_expression()?;
2740        self.expect(&Token::RParen)?;
2741        let body = self.parse_block()?;
2742        self.eat(&Token::Semicolon);
2743        Ok(Statement {
2744            label: None,
2745            kind: StmtKind::Given { topic, body },
2746            line,
2747        })
2748    }
2749
2750    /// `when (COND) { ... }` — only meaningful inside `given`
2751    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
2752        let line = self.peek_line();
2753        self.advance();
2754        self.expect(&Token::LParen)?;
2755        let cond = self.parse_expression()?;
2756        self.expect(&Token::RParen)?;
2757        let body = self.parse_block()?;
2758        self.eat(&Token::Semicolon);
2759        Ok(Statement {
2760            label: None,
2761            kind: StmtKind::When { cond, body },
2762            line,
2763        })
2764    }
2765
2766    /// `default { ... }` — only meaningful inside `given`
2767    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
2768        let line = self.peek_line();
2769        self.advance();
2770        let body = self.parse_block()?;
2771        self.eat(&Token::Semicolon);
2772        Ok(Statement {
2773            label: None,
2774            kind: StmtKind::DefaultCase { body },
2775            line,
2776        })
2777    }
2778
2779    /// `match (EXPR) { PATTERN => EXPR, ... }`
2780    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
2781        self.expect(&Token::LParen)?;
2782        let subject = self.parse_expression()?;
2783        self.expect(&Token::RParen)?;
2784        self.expect(&Token::LBrace)?;
2785        let mut arms = Vec::new();
2786        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2787            if self.eat(&Token::Semicolon) {
2788                continue;
2789            }
2790            let pattern = self.parse_match_pattern()?;
2791            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
2792                self.advance();
2793                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
2794                // separator (see [`Self::parse_comma_expr`]).
2795                Some(Box::new(self.parse_assign_expr()?))
2796            } else {
2797                None
2798            };
2799            self.expect(&Token::FatArrow)?;
2800            // Use assign-level parsing so commas separate arms, not `List` elements.
2801            let body = self.parse_assign_expr()?;
2802            arms.push(MatchArm {
2803                pattern,
2804                guard,
2805                body,
2806            });
2807            self.eat(&Token::Comma);
2808        }
2809        self.expect(&Token::RBrace)?;
2810        Ok(Expr {
2811            kind: ExprKind::AlgebraicMatch {
2812                subject: Box::new(subject),
2813                arms,
2814            },
2815            line,
2816        })
2817    }
2818
2819    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
2820        match self.peek().clone() {
2821            Token::Regex(pattern, flags, _delim) => {
2822                self.advance();
2823                Ok(MatchPattern::Regex { pattern, flags })
2824            }
2825            Token::Ident(ref s) if s == "_" => {
2826                self.advance();
2827                Ok(MatchPattern::Any)
2828            }
2829            Token::Ident(ref s) if s == "Some" => {
2830                self.advance();
2831                self.expect(&Token::LParen)?;
2832                let name = self.parse_scalar_var_name()?;
2833                self.expect(&Token::RParen)?;
2834                Ok(MatchPattern::OptionSome(name))
2835            }
2836            Token::LBracket => self.parse_match_array_pattern(),
2837            Token::LBrace => self.parse_match_hash_pattern(),
2838            Token::LParen => {
2839                self.advance();
2840                let e = self.parse_expression()?;
2841                self.expect(&Token::RParen)?;
2842                Ok(MatchPattern::Value(Box::new(e)))
2843            }
2844            _ => {
2845                let e = self.parse_assign_expr()?;
2846                Ok(MatchPattern::Value(Box::new(e)))
2847            }
2848        }
2849    }
2850
2851    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
2852    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
2853        let mut elems = Vec::new();
2854        if self.eat(&Token::RBracket) {
2855            return Ok(vec![]);
2856        }
2857        loop {
2858            if matches!(self.peek(), Token::Star) {
2859                self.advance();
2860                elems.push(MatchArrayElem::Rest);
2861                self.eat(&Token::Comma);
2862                if !matches!(self.peek(), Token::RBracket) {
2863                    return Err(self.syntax_err(
2864                        "`*` must be the last element in an array match pattern",
2865                        self.peek_line(),
2866                    ));
2867                }
2868                self.expect(&Token::RBracket)?;
2869                return Ok(elems);
2870            }
2871            if let Token::ArrayVar(name) = self.peek().clone() {
2872                self.advance();
2873                elems.push(MatchArrayElem::RestBind(name));
2874                self.eat(&Token::Comma);
2875                if !matches!(self.peek(), Token::RBracket) {
2876                    return Err(self.syntax_err(
2877                        "`@name` rest bind must be the last element in an array match pattern",
2878                        self.peek_line(),
2879                    ));
2880                }
2881                self.expect(&Token::RBracket)?;
2882                return Ok(elems);
2883            }
2884            if let Token::ScalarVar(name) = self.peek().clone() {
2885                self.advance();
2886                elems.push(MatchArrayElem::CaptureScalar(name));
2887                if self.eat(&Token::Comma) {
2888                    if matches!(self.peek(), Token::RBracket) {
2889                        break;
2890                    }
2891                    continue;
2892                }
2893                break;
2894            }
2895            let e = self.parse_assign_expr()?;
2896            elems.push(MatchArrayElem::Expr(e));
2897            if self.eat(&Token::Comma) {
2898                if matches!(self.peek(), Token::RBracket) {
2899                    break;
2900                }
2901                continue;
2902            }
2903            break;
2904        }
2905        self.expect(&Token::RBracket)?;
2906        Ok(elems)
2907    }
2908
2909    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
2910        self.expect(&Token::LBracket)?;
2911        let elems = self.parse_match_array_elems_until_rbracket()?;
2912        Ok(MatchPattern::Array(elems))
2913    }
2914
2915    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
2916        self.expect(&Token::LBrace)?;
2917        let mut pairs = Vec::new();
2918        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2919            if self.eat(&Token::Semicolon) {
2920                continue;
2921            }
2922            let key = self.parse_assign_expr()?;
2923            self.expect(&Token::FatArrow)?;
2924            match self.advance().0 {
2925                Token::Ident(ref s) if s == "_" => {
2926                    pairs.push(MatchHashPair::KeyOnly { key });
2927                }
2928                Token::ScalarVar(name) => {
2929                    pairs.push(MatchHashPair::Capture { key, name });
2930                }
2931                tok => {
2932                    return Err(self.syntax_err(
2933                        format!(
2934                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
2935                            tok
2936                        ),
2937                        self.peek_line(),
2938                    ));
2939                }
2940            }
2941            self.eat(&Token::Comma);
2942        }
2943        self.expect(&Token::RBrace)?;
2944        Ok(MatchPattern::Hash(pairs))
2945    }
2946
2947    /// `eval_timeout SECS { ... }`
2948    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
2949        let line = self.peek_line();
2950        self.advance();
2951        let timeout = self.parse_postfix()?;
2952        let body = self.parse_block_or_bareword_block_no_args()?;
2953        self.eat(&Token::Semicolon);
2954        Ok(Statement {
2955            label: None,
2956            kind: StmtKind::EvalTimeout { timeout, body },
2957            line,
2958        })
2959    }
2960
2961    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
2962        match &mut cond.kind {
2963            ExprKind::Match {
2964                flags, scalar_g, ..
2965            } if flags.contains('g') => {
2966                *scalar_g = true;
2967            }
2968            ExprKind::UnaryOp {
2969                op: UnaryOp::LogNot,
2970                expr,
2971            } => {
2972                if let ExprKind::Match {
2973                    flags, scalar_g, ..
2974                } = &mut expr.kind
2975                {
2976                    if flags.contains('g') {
2977                        *scalar_g = true;
2978                    }
2979                }
2980            }
2981            _ => {}
2982        }
2983    }
2984
2985    fn parse_if(&mut self) -> PerlResult<Statement> {
2986        let line = self.peek_line();
2987        self.advance(); // 'if'
2988        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
2989            if crate::compat_mode() {
2990                return Err(self.syntax_err(
2991                    "`if let` is a stryke extension (disabled by --compat)",
2992                    line,
2993                ));
2994            }
2995            return self.parse_if_let(line);
2996        }
2997        self.expect(&Token::LParen)?;
2998        let mut cond = self.parse_expression()?;
2999        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3000        self.expect(&Token::RParen)?;
3001        let body = self.parse_block()?;
3002
3003        let mut elsifs = Vec::new();
3004        let mut else_block = None;
3005
3006        loop {
3007            if let Token::Ident(ref kw) = self.peek().clone() {
3008                if kw == "elsif" {
3009                    self.advance();
3010                    self.expect(&Token::LParen)?;
3011                    let mut c = self.parse_expression()?;
3012                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3013                    self.expect(&Token::RParen)?;
3014                    let b = self.parse_block()?;
3015                    elsifs.push((c, b));
3016                    continue;
3017                }
3018                if kw == "else" {
3019                    self.advance();
3020                    else_block = Some(self.parse_block()?);
3021                }
3022            }
3023            break;
3024        }
3025
3026        Ok(Statement {
3027            label: None,
3028            kind: StmtKind::If {
3029                condition: cond,
3030                body,
3031                elsifs,
3032                else_block,
3033            },
3034            line,
3035        })
3036    }
3037
3038    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3039    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3040        self.advance(); // `let`
3041        let pattern = self.parse_match_pattern()?;
3042        self.expect(&Token::Assign)?;
3043        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3044        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3045        let rhs = self.parse_assign_expr();
3046        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3047        let rhs = rhs?;
3048        let then_block = self.parse_block()?;
3049        let else_block_opt = match self.peek().clone() {
3050            Token::Ident(ref kw) if kw == "else" => {
3051                self.advance();
3052                Some(self.parse_block()?)
3053            }
3054            Token::Ident(ref kw) if kw == "elsif" => {
3055                return Err(self.syntax_err(
3056                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3057                    self.peek_line(),
3058                ));
3059            }
3060            _ => None,
3061        };
3062        let then_expr = Self::expr_do_anon_block(then_block, line);
3063        let else_expr = if let Some(eb) = else_block_opt {
3064            Self::expr_do_anon_block(eb, line)
3065        } else {
3066            Expr {
3067                kind: ExprKind::Undef,
3068                line,
3069            }
3070        };
3071        let arms = vec![
3072            MatchArm {
3073                pattern,
3074                guard: None,
3075                body: then_expr,
3076            },
3077            MatchArm {
3078                pattern: MatchPattern::Any,
3079                guard: None,
3080                body: else_expr,
3081            },
3082        ];
3083        Ok(Statement {
3084            label: None,
3085            kind: StmtKind::Expression(Expr {
3086                kind: ExprKind::AlgebraicMatch {
3087                    subject: Box::new(rhs),
3088                    arms,
3089                },
3090                line,
3091            }),
3092            line,
3093        })
3094    }
3095
3096    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3097        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3098        Expr {
3099            kind: ExprKind::Do(Box::new(Expr {
3100                kind: ExprKind::CodeRef {
3101                    params: vec![],
3102                    body: block,
3103                },
3104                line: inner_line,
3105            })),
3106            line: outer_line,
3107        }
3108    }
3109
3110    fn parse_unless(&mut self) -> PerlResult<Statement> {
3111        let line = self.peek_line();
3112        self.advance(); // 'unless'
3113        self.expect(&Token::LParen)?;
3114        let mut cond = self.parse_expression()?;
3115        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3116        self.expect(&Token::RParen)?;
3117        let body = self.parse_block()?;
3118        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3119            if kw == "else" {
3120                self.advance();
3121                Some(self.parse_block()?)
3122            } else {
3123                None
3124            }
3125        } else {
3126            None
3127        };
3128        Ok(Statement {
3129            label: None,
3130            kind: StmtKind::Unless {
3131                condition: cond,
3132                body,
3133                else_block,
3134            },
3135            line,
3136        })
3137    }
3138
3139    fn parse_while(&mut self) -> PerlResult<Statement> {
3140        let line = self.peek_line();
3141        self.advance(); // 'while'
3142        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3143            if crate::compat_mode() {
3144                return Err(self.syntax_err(
3145                    "`while let` is a stryke extension (disabled by --compat)",
3146                    line,
3147                ));
3148            }
3149            return self.parse_while_let(line);
3150        }
3151        self.expect(&Token::LParen)?;
3152        let mut cond = self.parse_expression()?;
3153        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3154        self.expect(&Token::RParen)?;
3155        let body = self.parse_block()?;
3156        let continue_block = self.parse_optional_continue_block()?;
3157        Ok(Statement {
3158            label: None,
3159            kind: StmtKind::While {
3160                condition: cond,
3161                body,
3162                label: None,
3163                continue_block,
3164            },
3165            line,
3166        })
3167    }
3168
3169    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3170    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3171    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3172        self.advance(); // `let`
3173        let pattern = self.parse_match_pattern()?;
3174        self.expect(&Token::Assign)?;
3175        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3176        let rhs = self.parse_assign_expr();
3177        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3178        let rhs = rhs?;
3179        let mut user_body = self.parse_block()?;
3180        let continue_block = self.parse_optional_continue_block()?;
3181        user_body.push(Statement::new(
3182            StmtKind::Expression(Expr {
3183                kind: ExprKind::Integer(1),
3184                line,
3185            }),
3186            line,
3187        ));
3188        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3189        let match_expr = Expr {
3190            kind: ExprKind::AlgebraicMatch {
3191                subject: Box::new(rhs),
3192                arms: vec![
3193                    MatchArm {
3194                        pattern,
3195                        guard: None,
3196                        body: Self::expr_do_anon_block(user_body, line),
3197                    },
3198                    MatchArm {
3199                        pattern: MatchPattern::Any,
3200                        guard: None,
3201                        body: Expr {
3202                            kind: ExprKind::Integer(0),
3203                            line,
3204                        },
3205                    },
3206                ],
3207            },
3208            line,
3209        };
3210        let my_stmt = Statement::new(
3211            StmtKind::My(vec![VarDecl {
3212                sigil: Sigil::Scalar,
3213                name: tmp.clone(),
3214                initializer: Some(match_expr),
3215                frozen: false,
3216                type_annotation: None,
3217            }]),
3218            line,
3219        );
3220        let unless_last = Statement::new(
3221            StmtKind::Unless {
3222                condition: Expr {
3223                    kind: ExprKind::ScalarVar(tmp),
3224                    line,
3225                },
3226                body: vec![Statement::new(StmtKind::Last(None), line)],
3227                else_block: None,
3228            },
3229            line,
3230        );
3231        Ok(Statement::new(
3232            StmtKind::While {
3233                condition: Expr {
3234                    kind: ExprKind::Integer(1),
3235                    line,
3236                },
3237                body: vec![my_stmt, unless_last],
3238                label: None,
3239                continue_block,
3240            },
3241            line,
3242        ))
3243    }
3244
3245    fn parse_until(&mut self) -> PerlResult<Statement> {
3246        let line = self.peek_line();
3247        self.advance(); // 'until'
3248        self.expect(&Token::LParen)?;
3249        let mut cond = self.parse_expression()?;
3250        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3251        self.expect(&Token::RParen)?;
3252        let body = self.parse_block()?;
3253        let continue_block = self.parse_optional_continue_block()?;
3254        Ok(Statement {
3255            label: None,
3256            kind: StmtKind::Until {
3257                condition: cond,
3258                body,
3259                label: None,
3260                continue_block,
3261            },
3262            line,
3263        })
3264    }
3265
3266    /// `continue { ... }` after a loop body (optional).
3267    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3268        if let Token::Ident(ref kw) = self.peek().clone() {
3269            if kw == "continue" {
3270                self.advance();
3271                return Ok(Some(self.parse_block()?));
3272            }
3273        }
3274        Ok(None)
3275    }
3276
3277    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3278        let line = self.peek_line();
3279        self.advance(); // 'for'
3280
3281        // Peek to determine if C-style for or foreach
3282        // C-style: for (init; cond; step)
3283        // foreach-style: for $var (list) or for (list)
3284        match self.peek() {
3285            Token::LParen => {
3286                // Check if next after ( is a semicolon or an assignment — C-style
3287                // Or if it's a list — foreach-style
3288                // Heuristic: if the token after ( is 'my' or '$' followed by
3289                // content that contains ';', it's C-style.
3290                let saved = self.pos;
3291                self.advance(); // consume (
3292                                // Look for semicolon at paren depth 0
3293                let mut depth = 1;
3294                let mut has_semi = false;
3295                let mut scan = self.pos;
3296                while scan < self.tokens.len() {
3297                    match &self.tokens[scan].0 {
3298                        Token::LParen => depth += 1,
3299                        Token::RParen => {
3300                            depth -= 1;
3301                            if depth == 0 {
3302                                break;
3303                            }
3304                        }
3305                        Token::Semicolon if depth == 1 => {
3306                            has_semi = true;
3307                            break;
3308                        }
3309                        _ => {}
3310                    }
3311                    scan += 1;
3312                }
3313                self.pos = saved;
3314
3315                if has_semi {
3316                    self.parse_c_style_for(line)
3317                } else {
3318                    // foreach without explicit var — uses $_
3319                    self.expect(&Token::LParen)?;
3320                    let list = self.parse_expression()?;
3321                    self.expect(&Token::RParen)?;
3322                    let body = self.parse_block()?;
3323                    let continue_block = self.parse_optional_continue_block()?;
3324                    Ok(Statement {
3325                        label: None,
3326                        kind: StmtKind::Foreach {
3327                            var: "_".to_string(),
3328                            list,
3329                            body,
3330                            label: None,
3331                            continue_block,
3332                        },
3333                        line,
3334                    })
3335                }
3336            }
3337            Token::Ident(ref kw) if kw == "my" => {
3338                self.advance(); // 'my'
3339                let var = self.parse_scalar_var_name()?;
3340                self.expect(&Token::LParen)?;
3341                let list = self.parse_expression()?;
3342                self.expect(&Token::RParen)?;
3343                let body = self.parse_block()?;
3344                let continue_block = self.parse_optional_continue_block()?;
3345                Ok(Statement {
3346                    label: None,
3347                    kind: StmtKind::Foreach {
3348                        var,
3349                        list,
3350                        body,
3351                        label: None,
3352                        continue_block,
3353                    },
3354                    line,
3355                })
3356            }
3357            Token::ScalarVar(_) => {
3358                let var = self.parse_scalar_var_name()?;
3359                self.expect(&Token::LParen)?;
3360                let list = self.parse_expression()?;
3361                self.expect(&Token::RParen)?;
3362                let body = self.parse_block()?;
3363                let continue_block = self.parse_optional_continue_block()?;
3364                Ok(Statement {
3365                    label: None,
3366                    kind: StmtKind::Foreach {
3367                        var,
3368                        list,
3369                        body,
3370                        label: None,
3371                        continue_block,
3372                    },
3373                    line,
3374                })
3375            }
3376            _ => self.parse_c_style_for(line),
3377        }
3378    }
3379
3380    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
3381        self.expect(&Token::LParen)?;
3382        let init = if self.eat(&Token::Semicolon) {
3383            None
3384        } else {
3385            let s = self.parse_statement()?;
3386            self.eat(&Token::Semicolon);
3387            Some(Box::new(s))
3388        };
3389        let mut condition = if matches!(self.peek(), Token::Semicolon) {
3390            None
3391        } else {
3392            Some(self.parse_expression()?)
3393        };
3394        if let Some(ref mut c) = condition {
3395            Self::mark_match_scalar_g_for_boolean_condition(c);
3396        }
3397        self.expect(&Token::Semicolon)?;
3398        let step = if matches!(self.peek(), Token::RParen) {
3399            None
3400        } else {
3401            Some(self.parse_expression()?)
3402        };
3403        self.expect(&Token::RParen)?;
3404        let body = self.parse_block()?;
3405        let continue_block = self.parse_optional_continue_block()?;
3406        Ok(Statement {
3407            label: None,
3408            kind: StmtKind::For {
3409                init,
3410                condition,
3411                step,
3412                body,
3413                label: None,
3414                continue_block,
3415            },
3416            line,
3417        })
3418    }
3419
3420    fn parse_foreach(&mut self) -> PerlResult<Statement> {
3421        let line = self.peek_line();
3422        self.advance(); // 'foreach'
3423        let var = match self.peek() {
3424            Token::Ident(ref kw) if kw == "my" => {
3425                self.advance();
3426                self.parse_scalar_var_name()?
3427            }
3428            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
3429            _ => "_".to_string(),
3430        };
3431        self.expect(&Token::LParen)?;
3432        let list = self.parse_expression()?;
3433        self.expect(&Token::RParen)?;
3434        let body = self.parse_block()?;
3435        let continue_block = self.parse_optional_continue_block()?;
3436        Ok(Statement {
3437            label: None,
3438            kind: StmtKind::Foreach {
3439                var,
3440                list,
3441                body,
3442                label: None,
3443                continue_block,
3444            },
3445            line,
3446        })
3447    }
3448
3449    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
3450        match self.advance() {
3451            (Token::ScalarVar(name), _) => Ok(name),
3452            (tok, line) => {
3453                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
3454            }
3455        }
3456    }
3457
3458    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
3459    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
3460        let mut s = String::new();
3461        loop {
3462            match self.peek().clone() {
3463                Token::RParen => {
3464                    self.advance();
3465                    break;
3466                }
3467                Token::Eof => {
3468                    return Err(self.syntax_err(
3469                        "Unterminated sub prototype (expected ')' before end of input)",
3470                        self.peek_line(),
3471                    ));
3472                }
3473                Token::ScalarVar(v) if v == ")" => {
3474                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
3475                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
3476                    self.advance();
3477                    s.push('$');
3478                    if matches!(self.peek(), Token::LBrace) {
3479                        break;
3480                    }
3481                }
3482                Token::Ident(i) => {
3483                    let i = i.clone();
3484                    self.advance();
3485                    s.push_str(&i);
3486                }
3487                Token::Semicolon => {
3488                    self.advance();
3489                    s.push(';');
3490                }
3491                Token::LParen => {
3492                    self.advance();
3493                    s.push('(');
3494                }
3495                Token::LBracket => {
3496                    self.advance();
3497                    s.push('[');
3498                }
3499                Token::RBracket => {
3500                    self.advance();
3501                    s.push(']');
3502                }
3503                Token::Backslash => {
3504                    self.advance();
3505                    s.push('\\');
3506                }
3507                Token::Comma => {
3508                    self.advance();
3509                    s.push(',');
3510                }
3511                Token::ScalarVar(v) => {
3512                    let v = v.clone();
3513                    self.advance();
3514                    s.push('$');
3515                    s.push_str(&v);
3516                }
3517                Token::ArrayVar(v) => {
3518                    let v = v.clone();
3519                    self.advance();
3520                    s.push('@');
3521                    s.push_str(&v);
3522                }
3523                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
3524                Token::ArrayAt => {
3525                    self.advance();
3526                    s.push('@');
3527                }
3528                Token::HashVar(v) => {
3529                    let v = v.clone();
3530                    self.advance();
3531                    s.push('%');
3532                    s.push_str(&v);
3533                }
3534                Token::HashPercent => {
3535                    self.advance();
3536                    s.push('%');
3537                }
3538                Token::Plus => {
3539                    self.advance();
3540                    s.push('+');
3541                }
3542                Token::Minus => {
3543                    self.advance();
3544                    s.push('-');
3545                }
3546                Token::BitAnd => {
3547                    self.advance();
3548                    s.push('&');
3549                }
3550                tok => {
3551                    return Err(self.syntax_err(
3552                        format!("Unexpected token in sub prototype: {:?}", tok),
3553                        self.peek_line(),
3554                    ));
3555                }
3556            }
3557        }
3558        Ok(s)
3559    }
3560
3561    fn sub_signature_list_starts_here(&self) -> bool {
3562        match self.peek() {
3563            Token::LBrace | Token::LBracket => true,
3564            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
3565            Token::ArrayVar(_) | Token::HashVar(_) => true,
3566            _ => false,
3567        }
3568    }
3569
3570    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
3571        let (tok, line) = self.advance();
3572        match tok {
3573            Token::Ident(i) => Ok(i),
3574            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
3575            tok => Err(self.syntax_err(
3576                format!(
3577                    "sub signature: expected hash key (identifier or string), got {:?}",
3578                    tok
3579                ),
3580                line,
3581            )),
3582        }
3583    }
3584
3585    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
3586        let mut params = Vec::new();
3587        loop {
3588            if matches!(self.peek(), Token::RParen) {
3589                break;
3590            }
3591            match self.peek().clone() {
3592                Token::ScalarVar(name) => {
3593                    if name == "$$" || name == ")" {
3594                        return Err(self.syntax_err(
3595                            format!(
3596                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
3597                            ),
3598                            self.peek_line(),
3599                        ));
3600                    }
3601                    self.advance();
3602                    let ty = if self.eat(&Token::Colon) {
3603                        match self.peek() {
3604                            Token::Ident(ref tname) => {
3605                                let tname = tname.clone();
3606                                self.advance();
3607                                Some(match tname.as_str() {
3608                                    "Int" => PerlTypeName::Int,
3609                                    "Str" => PerlTypeName::Str,
3610                                    "Float" => PerlTypeName::Float,
3611                                    "Bool" => PerlTypeName::Bool,
3612                                    "Array" => PerlTypeName::Array,
3613                                    "Hash" => PerlTypeName::Hash,
3614                                    "Ref" => PerlTypeName::Ref,
3615                                    "Any" => PerlTypeName::Any,
3616                                    _ => PerlTypeName::Struct(tname),
3617                                })
3618                            }
3619                            _ => {
3620                                return Err(self.syntax_err(
3621                                    "expected type name after `:` in sub signature",
3622                                    self.peek_line(),
3623                                ));
3624                            }
3625                        }
3626                    } else {
3627                        None
3628                    };
3629                    params.push(SubSigParam::Scalar(name, ty));
3630                }
3631                Token::ArrayVar(name) => {
3632                    self.advance();
3633                    params.push(SubSigParam::Array(name));
3634                }
3635                Token::HashVar(name) => {
3636                    self.advance();
3637                    params.push(SubSigParam::Hash(name));
3638                }
3639                Token::LBracket => {
3640                    self.advance();
3641                    let elems = self.parse_match_array_elems_until_rbracket()?;
3642                    params.push(SubSigParam::ArrayDestruct(elems));
3643                }
3644                Token::LBrace => {
3645                    self.advance();
3646                    let mut pairs = Vec::new();
3647                    loop {
3648                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
3649                            break;
3650                        }
3651                        if self.eat(&Token::Comma) {
3652                            continue;
3653                        }
3654                        let key = self.parse_sub_signature_hash_key()?;
3655                        self.expect(&Token::FatArrow)?;
3656                        let bind = self.parse_scalar_var_name()?;
3657                        pairs.push((key, bind));
3658                        self.eat(&Token::Comma);
3659                    }
3660                    self.expect(&Token::RBrace)?;
3661                    params.push(SubSigParam::HashDestruct(pairs));
3662                }
3663                tok => {
3664                    return Err(self.syntax_err(
3665                        format!(
3666                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
3667                            tok
3668                        ),
3669                        self.peek_line(),
3670                    ));
3671                }
3672            }
3673            match self.peek() {
3674                Token::Comma => {
3675                    self.advance();
3676                    if matches!(self.peek(), Token::RParen) {
3677                        return Err(self.syntax_err(
3678                            "trailing `,` before `)` in sub signature",
3679                            self.peek_line(),
3680                        ));
3681                    }
3682                }
3683                Token::RParen => break,
3684                _ => {
3685                    return Err(self.syntax_err(
3686                        format!(
3687                            "expected `,` or `)` after sub signature parameter, got {:?}",
3688                            self.peek()
3689                        ),
3690                        self.peek_line(),
3691                    ));
3692                }
3693            }
3694        }
3695        Ok(params)
3696    }
3697
3698    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
3699    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
3700        if !matches!(self.peek(), Token::LParen) {
3701            return Ok((vec![], None));
3702        }
3703        self.advance();
3704        if matches!(self.peek(), Token::RParen) {
3705            self.advance();
3706            return Ok((vec![], Some(String::new())));
3707        }
3708        if self.sub_signature_list_starts_here() {
3709            let params = self.parse_sub_signature_param_list()?;
3710            self.expect(&Token::RParen)?;
3711            return Ok((params, None));
3712        }
3713        let proto = self.parse_legacy_sub_prototype_tail()?;
3714        Ok((vec![], Some(proto)))
3715    }
3716
3717    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
3718    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
3719        while self.eat(&Token::Colon) {
3720            match self.advance() {
3721                (Token::Ident(_), _) => {}
3722                (tok, line) => {
3723                    return Err(self.syntax_err(
3724                        format!("Expected attribute name after `:`, got {:?}", tok),
3725                        line,
3726                    ));
3727                }
3728            }
3729            if self.eat(&Token::LParen) {
3730                let mut depth = 1usize;
3731                while depth > 0 {
3732                    match self.advance().0 {
3733                        Token::LParen => depth += 1,
3734                        Token::RParen => {
3735                            depth -= 1;
3736                        }
3737                        Token::Eof => {
3738                            return Err(self.syntax_err(
3739                                "Unterminated sub attribute argument list",
3740                                self.peek_line(),
3741                            ));
3742                        }
3743                        _ => {}
3744                    }
3745                }
3746            }
3747        }
3748        Ok(())
3749    }
3750
3751    fn parse_sub_decl(&mut self) -> PerlResult<Statement> {
3752        let line = self.peek_line();
3753        self.advance(); // 'sub'
3754        match self.peek().clone() {
3755            Token::Ident(_) => {
3756                let name = self.parse_package_qualified_identifier()?;
3757                self.declared_subs.insert(name.clone());
3758                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
3759                self.parse_sub_attributes()?;
3760                let body = self.parse_block()?;
3761                Ok(Statement {
3762                    label: None,
3763                    kind: StmtKind::SubDecl {
3764                        name,
3765                        params,
3766                        body,
3767                        prototype,
3768                    },
3769                    line,
3770                })
3771            }
3772            Token::LParen | Token::LBrace | Token::Colon => {
3773                // Statement-level anonymous sub: `sub { }`, `sub () { }`, `sub :lvalue { }`
3774                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
3775                self.parse_sub_attributes()?;
3776                let body = self.parse_block()?;
3777                Ok(Statement {
3778                    label: None,
3779                    kind: StmtKind::Expression(Expr {
3780                        kind: ExprKind::CodeRef { params, body },
3781                        line,
3782                    }),
3783                    line,
3784                })
3785            }
3786            tok => Err(self.syntax_err(
3787                format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
3788                self.peek_line(),
3789            )),
3790        }
3791    }
3792
3793    /// `struct Name { field => Type, ... ; fn method { } }`
3794    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
3795        let line = self.peek_line();
3796        self.advance(); // struct
3797        let name = match self.advance() {
3798            (Token::Ident(n), _) => n,
3799            (tok, err_line) => {
3800                return Err(
3801                    self.syntax_err(format!("Expected struct name, got {:?}", tok), err_line)
3802                )
3803            }
3804        };
3805        self.expect(&Token::LBrace)?;
3806        let mut fields = Vec::new();
3807        let mut methods = Vec::new();
3808        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3809            // Check for method definition: `fn name { }` or `sub name { }`
3810            let is_method = match self.peek() {
3811                Token::Ident(s) => s == "fn" || s == "sub",
3812                _ => false,
3813            };
3814            if is_method {
3815                self.advance(); // fn/sub
3816                let method_name = match self.advance() {
3817                    (Token::Ident(n), _) => n,
3818                    (tok, err_line) => {
3819                        return Err(self
3820                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
3821                    }
3822                };
3823                // Parse optional signature: `($self, $arg: Type, ...)`
3824                let params = if self.eat(&Token::LParen) {
3825                    let p = self.parse_sub_signature_param_list()?;
3826                    self.expect(&Token::RParen)?;
3827                    p
3828                } else {
3829                    Vec::new()
3830                };
3831                // parse_block handles its own { } delimiters
3832                let body = self.parse_block()?;
3833                methods.push(crate::ast::StructMethod {
3834                    name: method_name,
3835                    params,
3836                    body,
3837                });
3838                // Optional trailing comma/semicolon after method
3839                self.eat(&Token::Comma);
3840                self.eat(&Token::Semicolon);
3841                continue;
3842            }
3843
3844            let field_name = match self.advance() {
3845                (Token::Ident(n), _) => n,
3846                (tok, err_line) => {
3847                    return Err(
3848                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
3849                    )
3850                }
3851            };
3852            // Support both `field => Type` and bare `field` (implies Any type)
3853            let ty = if self.eat(&Token::FatArrow) {
3854                self.parse_type_name()?
3855            } else {
3856                crate::ast::PerlTypeName::Any
3857            };
3858            let default = if self.eat(&Token::Assign) {
3859                // Use parse_ternary to avoid consuming commas (next field separator)
3860                Some(self.parse_ternary()?)
3861            } else {
3862                None
3863            };
3864            fields.push(StructField {
3865                name: field_name,
3866                ty,
3867                default,
3868            });
3869            if !self.eat(&Token::Comma) {
3870                // Also allow semicolons as field separators
3871                self.eat(&Token::Semicolon);
3872            }
3873        }
3874        self.expect(&Token::RBrace)?;
3875        self.eat(&Token::Semicolon);
3876        Ok(Statement {
3877            label: None,
3878            kind: StmtKind::StructDecl {
3879                def: StructDef {
3880                    name,
3881                    fields,
3882                    methods,
3883                },
3884            },
3885            line,
3886        })
3887    }
3888
3889    /// `enum Name { Variant1, Variant2 => Type, ... }`
3890    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
3891        let line = self.peek_line();
3892        self.advance(); // enum
3893        let name = match self.advance() {
3894            (Token::Ident(n), _) => n,
3895            (tok, err_line) => {
3896                return Err(self.syntax_err(format!("Expected enum name, got {:?}", tok), err_line))
3897            }
3898        };
3899        self.expect(&Token::LBrace)?;
3900        let mut variants = Vec::new();
3901        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3902            let variant_name = match self.advance() {
3903                (Token::Ident(n), _) => n,
3904                (tok, err_line) => {
3905                    return Err(
3906                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
3907                    )
3908                }
3909            };
3910            let ty = if self.eat(&Token::FatArrow) {
3911                Some(self.parse_type_name()?)
3912            } else {
3913                None
3914            };
3915            variants.push(EnumVariant {
3916                name: variant_name,
3917                ty,
3918            });
3919            if !self.eat(&Token::Comma) {
3920                self.eat(&Token::Semicolon);
3921            }
3922        }
3923        self.expect(&Token::RBrace)?;
3924        self.eat(&Token::Semicolon);
3925        Ok(Statement {
3926            label: None,
3927            kind: StmtKind::EnumDecl {
3928                def: EnumDef { name, variants },
3929            },
3930            line,
3931        })
3932    }
3933
3934    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
3935    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
3936        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
3937        let line = self.peek_line();
3938        self.advance(); // class
3939        let name = match self.advance() {
3940            (Token::Ident(n), _) => n,
3941            (tok, err_line) => {
3942                return Err(self.syntax_err(format!("Expected class name, got {:?}", tok), err_line))
3943            }
3944        };
3945
3946        // Parse `extends Parent1, Parent2`
3947        let mut extends = Vec::new();
3948        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
3949            self.advance(); // extends
3950            loop {
3951                match self.advance() {
3952                    (Token::Ident(parent), _) => extends.push(parent),
3953                    (tok, err_line) => {
3954                        return Err(self.syntax_err(
3955                            format!("Expected parent class name after `extends`, got {:?}", tok),
3956                            err_line,
3957                        ))
3958                    }
3959                }
3960                if !self.eat(&Token::Comma) {
3961                    break;
3962                }
3963            }
3964        }
3965
3966        // Parse `impl Trait1, Trait2`
3967        let mut implements = Vec::new();
3968        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
3969            self.advance(); // impl
3970            loop {
3971                match self.advance() {
3972                    (Token::Ident(trait_name), _) => implements.push(trait_name),
3973                    (tok, err_line) => {
3974                        return Err(self.syntax_err(
3975                            format!("Expected trait name after `impl`, got {:?}", tok),
3976                            err_line,
3977                        ))
3978                    }
3979                }
3980                if !self.eat(&Token::Comma) {
3981                    break;
3982                }
3983            }
3984        }
3985
3986        self.expect(&Token::LBrace)?;
3987        let mut fields = Vec::new();
3988        let mut methods = Vec::new();
3989        let mut static_fields = Vec::new();
3990
3991        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3992            // Check for visibility modifier
3993            let visibility = match self.peek() {
3994                Token::Ident(ref s) if s == "pub" => {
3995                    self.advance();
3996                    Visibility::Public
3997                }
3998                Token::Ident(ref s) if s == "priv" => {
3999                    self.advance();
4000                    Visibility::Private
4001                }
4002                Token::Ident(ref s) if s == "prot" => {
4003                    self.advance();
4004                    Visibility::Protected
4005                }
4006                _ => Visibility::Public, // default public
4007            };
4008
4009            // Check for static field: `static name: Type = default`
4010            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
4011                self.advance(); // static
4012
4013                // Could be a static method (`static fn`) or static field
4014                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4015                    // static fn is same as fn Self.name — handled below but not here
4016                    return Err(self.syntax_err(
4017                        "use `fn Self.name` for static methods, not `static fn`",
4018                        self.peek_line(),
4019                    ));
4020                }
4021
4022                let field_name = match self.advance() {
4023                    (Token::Ident(n), _) => n,
4024                    (tok, err_line) => {
4025                        return Err(self.syntax_err(
4026                            format!("Expected static field name, got {:?}", tok),
4027                            err_line,
4028                        ))
4029                    }
4030                };
4031
4032                let ty = if self.eat(&Token::Colon) {
4033                    self.parse_type_name()?
4034                } else {
4035                    crate::ast::PerlTypeName::Any
4036                };
4037
4038                let default = if self.eat(&Token::Assign) {
4039                    Some(self.parse_ternary()?)
4040                } else {
4041                    None
4042                };
4043
4044                static_fields.push(ClassStaticField {
4045                    name: field_name,
4046                    ty,
4047                    visibility,
4048                    default,
4049                });
4050
4051                if !self.eat(&Token::Comma) {
4052                    self.eat(&Token::Semicolon);
4053                }
4054                continue;
4055            }
4056
4057            // Check for `final` modifier before fn
4058            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
4059            if method_is_final {
4060                self.advance(); // final
4061            }
4062
4063            // Check for method: `fn name` or `fn Self.name` (static)
4064            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
4065            if is_method {
4066                self.advance(); // fn/sub
4067
4068                // Check for static method: `fn Self.name`
4069                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
4070                if is_static {
4071                    self.advance(); // Self
4072                    self.expect(&Token::Dot)?;
4073                }
4074
4075                let method_name = match self.advance() {
4076                    (Token::Ident(n), _) => n,
4077                    (tok, err_line) => {
4078                        return Err(self
4079                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4080                    }
4081                };
4082
4083                // Parse optional signature
4084                let params = if self.eat(&Token::LParen) {
4085                    let p = self.parse_sub_signature_param_list()?;
4086                    self.expect(&Token::RParen)?;
4087                    p
4088                } else {
4089                    Vec::new()
4090                };
4091
4092                // Body is optional (abstract method in trait has no body)
4093                let body = if matches!(self.peek(), Token::LBrace) {
4094                    Some(self.parse_block()?)
4095                } else {
4096                    None
4097                };
4098
4099                methods.push(ClassMethod {
4100                    name: method_name,
4101                    params,
4102                    body,
4103                    visibility,
4104                    is_static,
4105                    is_final: method_is_final,
4106                });
4107                self.eat(&Token::Comma);
4108                self.eat(&Token::Semicolon);
4109                continue;
4110            } else if method_is_final {
4111                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
4112            }
4113
4114            // Parse field: `name: Type = default`
4115            let field_name = match self.advance() {
4116                (Token::Ident(n), _) => n,
4117                (tok, err_line) => {
4118                    return Err(
4119                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4120                    )
4121                }
4122            };
4123
4124            // Type after colon: `name: Type`
4125            let ty = if self.eat(&Token::Colon) {
4126                self.parse_type_name()?
4127            } else {
4128                crate::ast::PerlTypeName::Any
4129            };
4130
4131            // Default value after `=`
4132            let default = if self.eat(&Token::Assign) {
4133                Some(self.parse_ternary()?)
4134            } else {
4135                None
4136            };
4137
4138            fields.push(ClassField {
4139                name: field_name,
4140                ty,
4141                visibility,
4142                default,
4143            });
4144
4145            if !self.eat(&Token::Comma) {
4146                self.eat(&Token::Semicolon);
4147            }
4148        }
4149
4150        self.expect(&Token::RBrace)?;
4151        self.eat(&Token::Semicolon);
4152
4153        Ok(Statement {
4154            label: None,
4155            kind: StmtKind::ClassDecl {
4156                def: ClassDef {
4157                    name,
4158                    is_abstract,
4159                    is_final,
4160                    extends,
4161                    implements,
4162                    fields,
4163                    methods,
4164                    static_fields,
4165                },
4166            },
4167            line,
4168        })
4169    }
4170
4171    /// `trait Name { fn required; fn with_default { } }`
4172    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
4173        use crate::ast::{ClassMethod, TraitDef, Visibility};
4174        let line = self.peek_line();
4175        self.advance(); // trait
4176        let name = match self.advance() {
4177            (Token::Ident(n), _) => n,
4178            (tok, err_line) => {
4179                return Err(self.syntax_err(format!("Expected trait name, got {:?}", tok), err_line))
4180            }
4181        };
4182
4183        self.expect(&Token::LBrace)?;
4184        let mut methods = Vec::new();
4185
4186        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4187            // Optional visibility
4188            let visibility = match self.peek() {
4189                Token::Ident(ref s) if s == "pub" => {
4190                    self.advance();
4191                    Visibility::Public
4192                }
4193                Token::Ident(ref s) if s == "priv" => {
4194                    self.advance();
4195                    Visibility::Private
4196                }
4197                Token::Ident(ref s) if s == "prot" => {
4198                    self.advance();
4199                    Visibility::Protected
4200                }
4201                _ => Visibility::Public,
4202            };
4203
4204            // Expect `fn` or `sub`
4205            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4206                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
4207            }
4208            self.advance(); // fn/sub
4209
4210            let method_name = match self.advance() {
4211                (Token::Ident(n), _) => n,
4212                (tok, err_line) => {
4213                    return Err(
4214                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
4215                    )
4216                }
4217            };
4218
4219            // Optional signature
4220            let params = if self.eat(&Token::LParen) {
4221                let p = self.parse_sub_signature_param_list()?;
4222                self.expect(&Token::RParen)?;
4223                p
4224            } else {
4225                Vec::new()
4226            };
4227
4228            // Body is optional (no body = abstract/required method)
4229            let body = if matches!(self.peek(), Token::LBrace) {
4230                Some(self.parse_block()?)
4231            } else {
4232                None
4233            };
4234
4235            methods.push(ClassMethod {
4236                name: method_name,
4237                params,
4238                body,
4239                visibility,
4240                is_static: false,
4241                is_final: false,
4242            });
4243
4244            self.eat(&Token::Comma);
4245            self.eat(&Token::Semicolon);
4246        }
4247
4248        self.expect(&Token::RBrace)?;
4249        self.eat(&Token::Semicolon);
4250
4251        Ok(Statement {
4252            label: None,
4253            kind: StmtKind::TraitDecl {
4254                def: TraitDef { name, methods },
4255            },
4256            line,
4257        })
4258    }
4259
4260    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
4261        match &target.kind {
4262            ExprKind::ScalarVar(name) => Some(VarDecl {
4263                sigil: Sigil::Scalar,
4264                name: name.clone(),
4265                initializer: None,
4266                frozen: false,
4267                type_annotation: None,
4268            }),
4269            ExprKind::ArrayVar(name) => Some(VarDecl {
4270                sigil: Sigil::Array,
4271                name: name.clone(),
4272                initializer: None,
4273                frozen: false,
4274                type_annotation: None,
4275            }),
4276            ExprKind::HashVar(name) => Some(VarDecl {
4277                sigil: Sigil::Hash,
4278                name: name.clone(),
4279                initializer: None,
4280                frozen: false,
4281                type_annotation: None,
4282            }),
4283            ExprKind::Typeglob(name) => Some(VarDecl {
4284                sigil: Sigil::Typeglob,
4285                name: name.clone(),
4286                initializer: None,
4287                frozen: false,
4288                type_annotation: None,
4289            }),
4290            _ => None,
4291        }
4292    }
4293
4294    fn parse_decl_array_destructure(
4295        &mut self,
4296        keyword: &str,
4297        line: usize,
4298    ) -> PerlResult<Statement> {
4299        self.expect(&Token::LBracket)?;
4300        let elems = self.parse_match_array_elems_until_rbracket()?;
4301        self.expect(&Token::Assign)?;
4302        self.suppress_scalar_hash_brace += 1;
4303        let rhs = self.parse_expression()?;
4304        self.suppress_scalar_hash_brace -= 1;
4305        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
4306        self.parse_stmt_postfix_modifier(stmt)
4307    }
4308
4309    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
4310        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
4311            unreachable!("parse_match_hash_pattern returns Hash");
4312        };
4313        self.expect(&Token::Assign)?;
4314        self.suppress_scalar_hash_brace += 1;
4315        let rhs = self.parse_expression()?;
4316        self.suppress_scalar_hash_brace -= 1;
4317        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
4318        self.parse_stmt_postfix_modifier(stmt)
4319    }
4320
4321    fn desugar_array_destructure(
4322        &mut self,
4323        keyword: &str,
4324        line: usize,
4325        elems: Vec<MatchArrayElem>,
4326        rhs: Expr,
4327    ) -> PerlResult<Statement> {
4328        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4329        let mut stmts: Vec<Statement> = Vec::new();
4330        stmts.push(destructure_stmt_from_var_decls(
4331            keyword,
4332            vec![VarDecl {
4333                sigil: Sigil::Scalar,
4334                name: tmp.clone(),
4335                initializer: Some(rhs),
4336                frozen: false,
4337                type_annotation: None,
4338            }],
4339            line,
4340        ));
4341
4342        let has_rest = elems
4343            .iter()
4344            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
4345        let fixed_slots = elems
4346            .iter()
4347            .filter(|e| {
4348                matches!(
4349                    e,
4350                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
4351                )
4352            })
4353            .count();
4354        if !has_rest {
4355            let cond = Expr {
4356                kind: ExprKind::BinOp {
4357                    left: Box::new(destructure_expr_array_len(&tmp, line)),
4358                    op: BinOp::NumEq,
4359                    right: Box::new(Expr {
4360                        kind: ExprKind::Integer(fixed_slots as i64),
4361                        line,
4362                    }),
4363                },
4364                line,
4365            };
4366            stmts.push(destructure_stmt_unless_die(
4367                line,
4368                cond,
4369                "array destructure: length mismatch",
4370            ));
4371        }
4372
4373        let mut idx: i64 = 0;
4374        for elem in elems {
4375            match elem {
4376                MatchArrayElem::Rest => break,
4377                MatchArrayElem::RestBind(name) => {
4378                    let list_source = Expr {
4379                        kind: ExprKind::Deref {
4380                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4381                            kind: Sigil::Array,
4382                        },
4383                        line,
4384                    };
4385                    let last_ix = Expr {
4386                        kind: ExprKind::BinOp {
4387                            left: Box::new(destructure_expr_array_len(&tmp, line)),
4388                            op: BinOp::Sub,
4389                            right: Box::new(Expr {
4390                                kind: ExprKind::Integer(1),
4391                                line,
4392                            }),
4393                        },
4394                        line,
4395                    };
4396                    let range = Expr {
4397                        kind: ExprKind::Range {
4398                            from: Box::new(Expr {
4399                                kind: ExprKind::Integer(idx),
4400                                line,
4401                            }),
4402                            to: Box::new(last_ix),
4403                            exclusive: false,
4404                        },
4405                        line,
4406                    };
4407                    let slice = Expr {
4408                        kind: ExprKind::AnonymousListSlice {
4409                            source: Box::new(list_source),
4410                            indices: vec![range],
4411                        },
4412                        line,
4413                    };
4414                    stmts.push(destructure_stmt_from_var_decls(
4415                        keyword,
4416                        vec![VarDecl {
4417                            sigil: Sigil::Array,
4418                            name,
4419                            initializer: Some(slice),
4420                            frozen: false,
4421                            type_annotation: None,
4422                        }],
4423                        line,
4424                    ));
4425                    break;
4426                }
4427                MatchArrayElem::CaptureScalar(name) => {
4428                    let arrow = Expr {
4429                        kind: ExprKind::ArrowDeref {
4430                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4431                            index: Box::new(Expr {
4432                                kind: ExprKind::Integer(idx),
4433                                line,
4434                            }),
4435                            kind: DerefKind::Array,
4436                        },
4437                        line,
4438                    };
4439                    stmts.push(destructure_stmt_from_var_decls(
4440                        keyword,
4441                        vec![VarDecl {
4442                            sigil: Sigil::Scalar,
4443                            name,
4444                            initializer: Some(arrow),
4445                            frozen: false,
4446                            type_annotation: None,
4447                        }],
4448                        line,
4449                    ));
4450                    idx += 1;
4451                }
4452                MatchArrayElem::Expr(e) => {
4453                    let elem_subj = Expr {
4454                        kind: ExprKind::ArrowDeref {
4455                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4456                            index: Box::new(Expr {
4457                                kind: ExprKind::Integer(idx),
4458                                line,
4459                            }),
4460                            kind: DerefKind::Array,
4461                        },
4462                        line,
4463                    };
4464                    let match_expr = Expr {
4465                        kind: ExprKind::AlgebraicMatch {
4466                            subject: Box::new(elem_subj),
4467                            arms: vec![
4468                                MatchArm {
4469                                    pattern: MatchPattern::Value(Box::new(e.clone())),
4470                                    guard: None,
4471                                    body: Expr {
4472                                        kind: ExprKind::Integer(0),
4473                                        line,
4474                                    },
4475                                },
4476                                MatchArm {
4477                                    pattern: MatchPattern::Any,
4478                                    guard: None,
4479                                    body: Expr {
4480                                        kind: ExprKind::Die(vec![Expr {
4481                                            kind: ExprKind::String(
4482                                                "array destructure: element pattern mismatch"
4483                                                    .to_string(),
4484                                            ),
4485                                            line,
4486                                        }]),
4487                                        line,
4488                                    },
4489                                },
4490                            ],
4491                        },
4492                        line,
4493                    };
4494                    stmts.push(Statement {
4495                        label: None,
4496                        kind: StmtKind::Expression(match_expr),
4497                        line,
4498                    });
4499                    idx += 1;
4500                }
4501            }
4502        }
4503
4504        Ok(Statement {
4505            label: None,
4506            kind: StmtKind::StmtGroup(stmts),
4507            line,
4508        })
4509    }
4510
4511    fn desugar_hash_destructure(
4512        &mut self,
4513        keyword: &str,
4514        line: usize,
4515        pairs: Vec<MatchHashPair>,
4516        rhs: Expr,
4517    ) -> PerlResult<Statement> {
4518        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4519        let mut stmts: Vec<Statement> = Vec::new();
4520        stmts.push(destructure_stmt_from_var_decls(
4521            keyword,
4522            vec![VarDecl {
4523                sigil: Sigil::Scalar,
4524                name: tmp.clone(),
4525                initializer: Some(rhs),
4526                frozen: false,
4527                type_annotation: None,
4528            }],
4529            line,
4530        ));
4531
4532        for pair in pairs {
4533            match pair {
4534                MatchHashPair::KeyOnly { key } => {
4535                    let exists_op = Expr {
4536                        kind: ExprKind::Exists(Box::new(Expr {
4537                            kind: ExprKind::ArrowDeref {
4538                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4539                                index: Box::new(key),
4540                                kind: DerefKind::Hash,
4541                            },
4542                            line,
4543                        })),
4544                        line,
4545                    };
4546                    stmts.push(destructure_stmt_unless_die(
4547                        line,
4548                        exists_op,
4549                        "hash destructure: missing required key",
4550                    ));
4551                }
4552                MatchHashPair::Capture { key, name } => {
4553                    let init = Expr {
4554                        kind: ExprKind::ArrowDeref {
4555                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4556                            index: Box::new(key),
4557                            kind: DerefKind::Hash,
4558                        },
4559                        line,
4560                    };
4561                    stmts.push(destructure_stmt_from_var_decls(
4562                        keyword,
4563                        vec![VarDecl {
4564                            sigil: Sigil::Scalar,
4565                            name,
4566                            initializer: Some(init),
4567                            frozen: false,
4568                            type_annotation: None,
4569                        }],
4570                        line,
4571                    ));
4572                }
4573            }
4574        }
4575
4576        Ok(Statement {
4577            label: None,
4578            kind: StmtKind::StmtGroup(stmts),
4579            line,
4580        })
4581    }
4582
4583    fn parse_my_our_local(
4584        &mut self,
4585        keyword: &str,
4586        allow_type_annotation: bool,
4587    ) -> PerlResult<Statement> {
4588        let line = self.peek_line();
4589        self.advance(); // 'my'/'our'/'local'
4590
4591        if keyword == "local"
4592            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
4593        {
4594            let target = self.parse_postfix()?;
4595            let mut initializer: Option<Expr> = None;
4596            if self.eat(&Token::Assign) {
4597                initializer = Some(self.parse_expression()?);
4598            } else if matches!(
4599                self.peek(),
4600                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
4601            ) {
4602                if matches!(&target.kind, ExprKind::Typeglob(_)) {
4603                    return Err(self.syntax_err(
4604                        "compound assignment on typeglob declaration is not supported",
4605                        self.peek_line(),
4606                    ));
4607                }
4608                let op = match self.peek().clone() {
4609                    Token::OrAssign => BinOp::LogOr,
4610                    Token::DefinedOrAssign => BinOp::DefinedOr,
4611                    Token::AndAssign => BinOp::LogAnd,
4612                    _ => unreachable!(),
4613                };
4614                self.advance();
4615                let rhs = self.parse_assign_expr()?;
4616                let tgt_line = target.line;
4617                initializer = Some(Expr {
4618                    kind: ExprKind::CompoundAssign {
4619                        target: Box::new(target.clone()),
4620                        op,
4621                        value: Box::new(rhs),
4622                    },
4623                    line: tgt_line,
4624                });
4625            }
4626
4627            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
4628                decl.initializer = initializer;
4629                StmtKind::Local(vec![decl])
4630            } else {
4631                StmtKind::LocalExpr {
4632                    target,
4633                    initializer,
4634                }
4635            };
4636            let stmt = Statement {
4637                label: None,
4638                kind,
4639                line,
4640            };
4641            return self.parse_stmt_postfix_modifier(stmt);
4642        }
4643
4644        if matches!(self.peek(), Token::LBracket) {
4645            return self.parse_decl_array_destructure(keyword, line);
4646        }
4647        if matches!(self.peek(), Token::LBrace) {
4648            return self.parse_decl_hash_destructure(keyword, line);
4649        }
4650
4651        let mut decls = Vec::new();
4652
4653        if self.eat(&Token::LParen) {
4654            // my ($a, @b, %c)
4655            while !matches!(self.peek(), Token::RParen | Token::Eof) {
4656                let decl = self.parse_var_decl(allow_type_annotation)?;
4657                decls.push(decl);
4658                if !self.eat(&Token::Comma) {
4659                    break;
4660                }
4661            }
4662            self.expect(&Token::RParen)?;
4663        } else {
4664            decls.push(self.parse_var_decl(allow_type_annotation)?);
4665        }
4666
4667        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
4668        if self.eat(&Token::Assign) {
4669            if keyword == "our" && decls.len() == 1 {
4670                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
4671                    self.advance();
4672                    decls.push(self.parse_var_decl(allow_type_annotation)?);
4673                    if !self.eat(&Token::Assign) {
4674                        return Err(self.syntax_err(
4675                            "expected `=` after `our` in chained our-declaration",
4676                            self.peek_line(),
4677                        ));
4678                    }
4679                }
4680            }
4681            let val = self.parse_expression()?;
4682            if decls.len() == 1 {
4683                decls[0].initializer = Some(val);
4684            } else {
4685                for decl in &mut decls {
4686                    decl.initializer = Some(val.clone());
4687                }
4688            }
4689        } else if decls.len() == 1 {
4690            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
4691            let op = match self.peek().clone() {
4692                Token::OrAssign => Some(BinOp::LogOr),
4693                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
4694                Token::AndAssign => Some(BinOp::LogAnd),
4695                _ => None,
4696            };
4697            if let Some(op) = op {
4698                let d = &decls[0];
4699                if matches!(d.sigil, Sigil::Typeglob) {
4700                    return Err(self.syntax_err(
4701                        "compound assignment on typeglob declaration is not supported",
4702                        self.peek_line(),
4703                    ));
4704                }
4705                self.advance();
4706                let rhs = self.parse_assign_expr()?;
4707                let target = Expr {
4708                    kind: match d.sigil {
4709                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
4710                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
4711                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
4712                        Sigil::Typeglob => unreachable!(),
4713                    },
4714                    line,
4715                };
4716                decls[0].initializer = Some(Expr {
4717                    kind: ExprKind::CompoundAssign {
4718                        target: Box::new(target),
4719                        op,
4720                        value: Box::new(rhs),
4721                    },
4722                    line,
4723                });
4724            }
4725        }
4726
4727        let kind = match keyword {
4728            "my" => StmtKind::My(decls),
4729            "mysync" => StmtKind::MySync(decls),
4730            "our" => StmtKind::Our(decls),
4731            "local" => StmtKind::Local(decls),
4732            "state" => StmtKind::State(decls),
4733            _ => unreachable!(),
4734        };
4735        let stmt = Statement {
4736            label: None,
4737            kind,
4738            line,
4739        };
4740        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
4741        self.parse_stmt_postfix_modifier(stmt)
4742    }
4743
4744    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
4745        let mut decl = match self.advance() {
4746            (Token::ScalarVar(name), _) => VarDecl {
4747                sigil: Sigil::Scalar,
4748                name,
4749                initializer: None,
4750                frozen: false,
4751                type_annotation: None,
4752            },
4753            (Token::ArrayVar(name), _) => VarDecl {
4754                sigil: Sigil::Array,
4755                name,
4756                initializer: None,
4757                frozen: false,
4758                type_annotation: None,
4759            },
4760            (Token::HashVar(name), _) => VarDecl {
4761                sigil: Sigil::Hash,
4762                name,
4763                initializer: None,
4764                frozen: false,
4765                type_annotation: None,
4766            },
4767            (Token::Star, _line) => {
4768                let name = match self.advance() {
4769                    (Token::Ident(n), _) => n,
4770                    (tok, l) => {
4771                        return Err(self
4772                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
4773                    }
4774                };
4775                VarDecl {
4776                    sigil: Sigil::Typeglob,
4777                    name,
4778                    initializer: None,
4779                    frozen: false,
4780                    type_annotation: None,
4781                }
4782            }
4783            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
4784            // slot in a list assignment. The interpreter treats `undef`-named
4785            // scalar decls as throwaway: declared into a unique sink so the
4786            // distribute-to-decls loop advances past the slot.
4787            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
4788                sigil: Sigil::Scalar,
4789                // Synthesize a name that user code cannot reference. Each
4790                // sink slot in a list-assign gets its own unique name so the
4791                // declarations don't collide.
4792                name: format!("__undef_sink_{}", self.pos),
4793                initializer: None,
4794                frozen: false,
4795                type_annotation: None,
4796            },
4797            (tok, line) => {
4798                return Err(self.syntax_err(
4799                    format!("Expected variable in declaration, got {:?}", tok),
4800                    line,
4801                ));
4802            }
4803        };
4804        if allow_type_annotation && self.eat(&Token::Colon) {
4805            let ty = self.parse_type_name()?;
4806            if decl.sigil != Sigil::Scalar {
4807                return Err(self.syntax_err(
4808                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
4809                    self.peek_line(),
4810                ));
4811            }
4812            decl.type_annotation = Some(ty);
4813        }
4814        Ok(decl)
4815    }
4816
4817    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
4818        match self.advance() {
4819            (Token::Ident(name), _) => match name.as_str() {
4820                "Int" => Ok(PerlTypeName::Int),
4821                "Str" => Ok(PerlTypeName::Str),
4822                "Float" => Ok(PerlTypeName::Float),
4823                "Bool" => Ok(PerlTypeName::Bool),
4824                "Array" => Ok(PerlTypeName::Array),
4825                "Hash" => Ok(PerlTypeName::Hash),
4826                "Ref" => Ok(PerlTypeName::Ref),
4827                "Any" => Ok(PerlTypeName::Any),
4828                _ => Ok(PerlTypeName::Struct(name)),
4829            },
4830            (tok, err_line) => Err(self.syntax_err(
4831                format!("Expected type name after `:`, got {:?}", tok),
4832                err_line,
4833            )),
4834        }
4835    }
4836
4837    fn parse_package(&mut self) -> PerlResult<Statement> {
4838        let line = self.peek_line();
4839        self.advance(); // 'package'
4840        let name = match self.advance() {
4841            (Token::Ident(n), _) => n,
4842            (tok, line) => {
4843                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
4844            }
4845        };
4846        // Handle Foo::Bar
4847        let mut full_name = name;
4848        while self.eat(&Token::PackageSep) {
4849            if let (Token::Ident(part), _) = self.advance() {
4850                full_name = format!("{}::{}", full_name, part);
4851            }
4852        }
4853        self.eat(&Token::Semicolon);
4854        Ok(Statement {
4855            label: None,
4856            kind: StmtKind::Package { name: full_name },
4857            line,
4858        })
4859    }
4860
4861    fn parse_use(&mut self) -> PerlResult<Statement> {
4862        let line = self.peek_line();
4863        self.advance(); // 'use'
4864        let (tok, tok_line) = self.advance();
4865        match tok {
4866            Token::Float(v) => {
4867                self.eat(&Token::Semicolon);
4868                Ok(Statement {
4869                    label: None,
4870                    kind: StmtKind::UsePerlVersion { version: v },
4871                    line,
4872                })
4873            }
4874            Token::Integer(n) => {
4875                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
4876                    self.eat(&Token::Semicolon);
4877                    Ok(Statement {
4878                        label: None,
4879                        kind: StmtKind::UsePerlVersion { version: n as f64 },
4880                        line,
4881                    })
4882                } else {
4883                    Err(self.syntax_err(
4884                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
4885                        line,
4886                    ))
4887                }
4888            }
4889            Token::Ident(n) => {
4890                let mut full_name = n;
4891                while self.eat(&Token::PackageSep) {
4892                    if let (Token::Ident(part), _) = self.advance() {
4893                        full_name = format!("{}::{}", full_name, part);
4894                    }
4895                }
4896                if full_name == "overload" {
4897                    let mut pairs = Vec::new();
4898                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
4899                        loop {
4900                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
4901                            {
4902                                break;
4903                            }
4904                            let key_e = this.parse_assign_expr()?;
4905                            this.expect(&Token::FatArrow)?;
4906                            let val_e = this.parse_assign_expr()?;
4907                            let key = this.expr_to_overload_key(&key_e)?;
4908                            let val = this.expr_to_overload_sub(&val_e)?;
4909                            pairs.push((key, val));
4910                            if !this.eat(&Token::Comma) {
4911                                break;
4912                            }
4913                        }
4914                        Ok(())
4915                    };
4916                    if self.eat(&Token::LParen) {
4917                        // `use overload ();` — common in JSON::PP and other modules.
4918                        parse_overload_pairs(self)?;
4919                        self.expect(&Token::RParen)?;
4920                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
4921                        parse_overload_pairs(self)?;
4922                    }
4923                    self.eat(&Token::Semicolon);
4924                    return Ok(Statement {
4925                        label: None,
4926                        kind: StmtKind::UseOverload { pairs },
4927                        line,
4928                    });
4929                }
4930                let mut imports = Vec::new();
4931                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
4932                    && !self.next_is_new_statement_start(tok_line)
4933                {
4934                    loop {
4935                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
4936                            break;
4937                        }
4938                        imports.push(self.parse_expression()?);
4939                        if !self.eat(&Token::Comma) {
4940                            break;
4941                        }
4942                    }
4943                }
4944                self.eat(&Token::Semicolon);
4945                Ok(Statement {
4946                    label: None,
4947                    kind: StmtKind::Use {
4948                        module: full_name,
4949                        imports,
4950                    },
4951                    line,
4952                })
4953            }
4954            other => Err(self.syntax_err(
4955                format!("Expected module name or version after use, got {:?}", other),
4956                tok_line,
4957            )),
4958        }
4959    }
4960
4961    fn parse_no(&mut self) -> PerlResult<Statement> {
4962        let line = self.peek_line();
4963        self.advance(); // 'no'
4964        let module = match self.advance() {
4965            (Token::Ident(n), tok_line) => (n, tok_line),
4966            (tok, line) => {
4967                return Err(self.syntax_err(
4968                    format!("Expected module name after no, got {:?}", tok),
4969                    line,
4970                ))
4971            }
4972        };
4973        let (module_name, tok_line) = module;
4974        let mut full_name = module_name;
4975        while self.eat(&Token::PackageSep) {
4976            if let (Token::Ident(part), _) = self.advance() {
4977                full_name = format!("{}::{}", full_name, part);
4978            }
4979        }
4980        let mut imports = Vec::new();
4981        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
4982            && !self.next_is_new_statement_start(tok_line)
4983        {
4984            loop {
4985                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
4986                    break;
4987                }
4988                imports.push(self.parse_expression()?);
4989                if !self.eat(&Token::Comma) {
4990                    break;
4991                }
4992            }
4993        }
4994        self.eat(&Token::Semicolon);
4995        Ok(Statement {
4996            label: None,
4997            kind: StmtKind::No {
4998                module: full_name,
4999                imports,
5000            },
5001            line,
5002        })
5003    }
5004
5005    fn parse_return(&mut self) -> PerlResult<Statement> {
5006        let line = self.peek_line();
5007        self.advance(); // 'return'
5008        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
5009            None
5010        } else {
5011            // Only parse up to the assign level to avoid consuming postfix if/unless
5012            Some(self.parse_assign_expr()?)
5013        };
5014        // Check for postfix modifiers on return
5015        let stmt = Statement {
5016            label: None,
5017            kind: StmtKind::Return(val),
5018            line,
5019        };
5020        if let Token::Ident(ref kw) = self.peek().clone() {
5021            match kw.as_str() {
5022                "if" => {
5023                    self.advance();
5024                    let cond = self.parse_expression()?;
5025                    self.eat(&Token::Semicolon);
5026                    return Ok(Statement {
5027                        label: None,
5028                        kind: StmtKind::If {
5029                            condition: cond,
5030                            body: vec![stmt],
5031                            elsifs: vec![],
5032                            else_block: None,
5033                        },
5034                        line,
5035                    });
5036                }
5037                "unless" => {
5038                    self.advance();
5039                    let cond = self.parse_expression()?;
5040                    self.eat(&Token::Semicolon);
5041                    return Ok(Statement {
5042                        label: None,
5043                        kind: StmtKind::Unless {
5044                            condition: cond,
5045                            body: vec![stmt],
5046                            else_block: None,
5047                        },
5048                        line,
5049                    });
5050                }
5051                _ => {}
5052            }
5053        }
5054        self.eat(&Token::Semicolon);
5055        Ok(stmt)
5056    }
5057
5058    // ── Expressions (Pratt / precedence climbing) ──
5059
5060    fn parse_expression(&mut self) -> PerlResult<Expr> {
5061        self.parse_comma_expr()
5062    }
5063
5064    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
5065        let expr = self.parse_assign_expr()?;
5066        let mut exprs = vec![expr];
5067        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5068            if matches!(
5069                self.peek(),
5070                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
5071            ) {
5072                break; // trailing comma
5073            }
5074            exprs.push(self.parse_assign_expr()?);
5075        }
5076        if exprs.len() == 1 {
5077            return Ok(exprs.pop().unwrap());
5078        }
5079        let line = exprs[0].line;
5080        Ok(Expr {
5081            kind: ExprKind::List(exprs),
5082            line,
5083        })
5084    }
5085
5086    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
5087        let expr = self.parse_ternary()?;
5088        let line = expr.line;
5089
5090        match self.peek().clone() {
5091            Token::Assign => {
5092                self.advance();
5093                let right = self.parse_assign_expr()?;
5094                Ok(Expr {
5095                    kind: ExprKind::Assign {
5096                        target: Box::new(expr),
5097                        value: Box::new(right),
5098                    },
5099                    line,
5100                })
5101            }
5102            Token::PlusAssign => {
5103                self.advance();
5104                let r = self.parse_assign_expr()?;
5105                Ok(Expr {
5106                    kind: ExprKind::CompoundAssign {
5107                        target: Box::new(expr),
5108                        op: BinOp::Add,
5109                        value: Box::new(r),
5110                    },
5111                    line,
5112                })
5113            }
5114            Token::MinusAssign => {
5115                self.advance();
5116                let r = self.parse_assign_expr()?;
5117                Ok(Expr {
5118                    kind: ExprKind::CompoundAssign {
5119                        target: Box::new(expr),
5120                        op: BinOp::Sub,
5121                        value: Box::new(r),
5122                    },
5123                    line,
5124                })
5125            }
5126            Token::MulAssign => {
5127                self.advance();
5128                let r = self.parse_assign_expr()?;
5129                Ok(Expr {
5130                    kind: ExprKind::CompoundAssign {
5131                        target: Box::new(expr),
5132                        op: BinOp::Mul,
5133                        value: Box::new(r),
5134                    },
5135                    line,
5136                })
5137            }
5138            Token::DivAssign => {
5139                self.advance();
5140                let r = self.parse_assign_expr()?;
5141                Ok(Expr {
5142                    kind: ExprKind::CompoundAssign {
5143                        target: Box::new(expr),
5144                        op: BinOp::Div,
5145                        value: Box::new(r),
5146                    },
5147                    line,
5148                })
5149            }
5150            Token::ModAssign => {
5151                self.advance();
5152                let r = self.parse_assign_expr()?;
5153                Ok(Expr {
5154                    kind: ExprKind::CompoundAssign {
5155                        target: Box::new(expr),
5156                        op: BinOp::Mod,
5157                        value: Box::new(r),
5158                    },
5159                    line,
5160                })
5161            }
5162            Token::PowAssign => {
5163                self.advance();
5164                let r = self.parse_assign_expr()?;
5165                Ok(Expr {
5166                    kind: ExprKind::CompoundAssign {
5167                        target: Box::new(expr),
5168                        op: BinOp::Pow,
5169                        value: Box::new(r),
5170                    },
5171                    line,
5172                })
5173            }
5174            Token::DotAssign => {
5175                self.advance();
5176                let r = self.parse_assign_expr()?;
5177                Ok(Expr {
5178                    kind: ExprKind::CompoundAssign {
5179                        target: Box::new(expr),
5180                        op: BinOp::Concat,
5181                        value: Box::new(r),
5182                    },
5183                    line,
5184                })
5185            }
5186            Token::BitAndAssign => {
5187                self.advance();
5188                let r = self.parse_assign_expr()?;
5189                Ok(Expr {
5190                    kind: ExprKind::CompoundAssign {
5191                        target: Box::new(expr),
5192                        op: BinOp::BitAnd,
5193                        value: Box::new(r),
5194                    },
5195                    line,
5196                })
5197            }
5198            Token::BitOrAssign => {
5199                self.advance();
5200                let r = self.parse_assign_expr()?;
5201                Ok(Expr {
5202                    kind: ExprKind::CompoundAssign {
5203                        target: Box::new(expr),
5204                        op: BinOp::BitOr,
5205                        value: Box::new(r),
5206                    },
5207                    line,
5208                })
5209            }
5210            Token::XorAssign => {
5211                self.advance();
5212                let r = self.parse_assign_expr()?;
5213                Ok(Expr {
5214                    kind: ExprKind::CompoundAssign {
5215                        target: Box::new(expr),
5216                        op: BinOp::BitXor,
5217                        value: Box::new(r),
5218                    },
5219                    line,
5220                })
5221            }
5222            Token::ShiftLeftAssign => {
5223                self.advance();
5224                let r = self.parse_assign_expr()?;
5225                Ok(Expr {
5226                    kind: ExprKind::CompoundAssign {
5227                        target: Box::new(expr),
5228                        op: BinOp::ShiftLeft,
5229                        value: Box::new(r),
5230                    },
5231                    line,
5232                })
5233            }
5234            Token::ShiftRightAssign => {
5235                self.advance();
5236                let r = self.parse_assign_expr()?;
5237                Ok(Expr {
5238                    kind: ExprKind::CompoundAssign {
5239                        target: Box::new(expr),
5240                        op: BinOp::ShiftRight,
5241                        value: Box::new(r),
5242                    },
5243                    line,
5244                })
5245            }
5246            Token::OrAssign => {
5247                self.advance();
5248                let r = self.parse_assign_expr()?;
5249                Ok(Expr {
5250                    kind: ExprKind::CompoundAssign {
5251                        target: Box::new(expr),
5252                        op: BinOp::LogOr,
5253                        value: Box::new(r),
5254                    },
5255                    line,
5256                })
5257            }
5258            Token::DefinedOrAssign => {
5259                self.advance();
5260                let r = self.parse_assign_expr()?;
5261                Ok(Expr {
5262                    kind: ExprKind::CompoundAssign {
5263                        target: Box::new(expr),
5264                        op: BinOp::DefinedOr,
5265                        value: Box::new(r),
5266                    },
5267                    line,
5268                })
5269            }
5270            Token::AndAssign => {
5271                self.advance();
5272                let r = self.parse_assign_expr()?;
5273                Ok(Expr {
5274                    kind: ExprKind::CompoundAssign {
5275                        target: Box::new(expr),
5276                        op: BinOp::LogAnd,
5277                        value: Box::new(r),
5278                    },
5279                    line,
5280                })
5281            }
5282            _ => Ok(expr),
5283        }
5284    }
5285
5286    fn parse_ternary(&mut self) -> PerlResult<Expr> {
5287        let expr = self.parse_pipe_forward()?;
5288        if self.eat(&Token::Question) {
5289            let line = expr.line;
5290            let then_expr = self.parse_assign_expr()?;
5291            self.expect(&Token::Colon)?;
5292            let else_expr = self.parse_assign_expr()?;
5293            return Ok(Expr {
5294                kind: ExprKind::Ternary {
5295                    condition: Box::new(expr),
5296                    then_expr: Box::new(then_expr),
5297                    else_expr: Box::new(else_expr),
5298                },
5299                line,
5300            });
5301        }
5302        Ok(expr)
5303    }
5304
5305    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
5306    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
5307    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
5308    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
5309    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
5310    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
5311        let mut left = self.parse_or_word()?;
5312        // Inside a paren-less arg list, `|>` is a hard terminator for the
5313        // enclosing call — leave it for the outer `parse_pipe_forward` loop
5314        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
5315        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
5316        // outer `|>` via its first-arg `parse_assign_expr`.
5317        if self.no_pipe_forward_depth > 0 {
5318            return Ok(left);
5319        }
5320        while matches!(self.peek(), Token::PipeForward) {
5321            if crate::compat_mode() {
5322                return Err(self.syntax_err(
5323                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
5324                    left.line,
5325                ));
5326            }
5327            let line = left.line;
5328            self.advance();
5329            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
5330            // `join`, …) accept a placeholder in place of their list operand.
5331            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
5332            let right_result = self.parse_or_word();
5333            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
5334            let right = right_result?;
5335            left = self.pipe_forward_apply(left, right, line)?;
5336        }
5337        Ok(left)
5338    }
5339
5340    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
5341    /// its **first** argument (Elixir / R / proposed-JS convention).
5342    ///
5343    /// The strategy depends on the shape of `rhs`:
5344    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
5345    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
5346    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
5347    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
5348    ///   matching the `(data, filter)` signature the builtin expects.
5349    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
5350    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
5351    ///   `lhs` (these parse a single default `$_` when called without an arg, so
5352    ///   piping overrides that default; first-arg and last-arg are identical).
5353    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
5354    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
5355    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
5356    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
5357    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
5358    ///   as the sole argument.
5359    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
5360    ///   since silently calling a non-callable at runtime would be worse.
5361    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
5362        let Expr { kind, line: rline } = rhs;
5363        let new_kind = match kind {
5364            // ── Generic / user-defined calls ───────────────────────────────────
5365            ExprKind::FuncCall { name, mut args } => {
5366                match name.as_str() {
5367                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
5368                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
5369                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
5370                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "letters"
5371                    | "letters_uc" | "letters_lc" | "punctuation" | "numbers" | "graphemes"
5372                    | "columns" | "sentences" | "paragraphs" | "sections" | "trim" | "avg"
5373                    | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml" | "to_html"
5374                    | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline" | "bar_chart"
5375                    | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed" | "cb"
5376                    | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
5377                        if args.is_empty() {
5378                            args.push(lhs);
5379                        } else {
5380                            args[0] = lhs;
5381                        }
5382                    }
5383                    "chunked" | "windowed" => {
5384                        if args.is_empty() {
5385                            return Err(self.syntax_err(
5386                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
5387                                line,
5388                            ));
5389                        }
5390                        args.insert(0, lhs);
5391                    }
5392                    "List::Util::reduce" | "List::Util::fold" => {
5393                        args.push(lhs);
5394                    }
5395                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
5396                        // data |> grep_v "pattern" → grep_v("pattern", data...)
5397                        // data |> pluck "key" → pluck("key", data...)
5398                        // data |> tee "file" → tee("file", data...)
5399                        // data |> nth N → nth(N, data...)
5400                        // data |> chunk N → chunk(N, data...)
5401                        args.push(lhs);
5402                    }
5403                    "enumerate" | "dedup" => {
5404                        // data |> enumerate → enumerate(data)
5405                        // data |> dedup → dedup(data)
5406                        args.insert(0, lhs);
5407                    }
5408                    "clamp" => {
5409                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
5410                        args.push(lhs);
5411                    }
5412                    "pfirst" | "pany" | "any" | "all" | "none" | "first" | "take_while"
5413                    | "drop_while" | "skip_while" | "reject" | "tap" | "peek" | "group_by"
5414                    | "chunk_by" | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
5415                        if args.len() < 2 {
5416                            return Err(self.syntax_err(
5417                                format!(
5418                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
5419                                ),
5420                                line,
5421                            ));
5422                        }
5423                        args[1] = lhs;
5424                    }
5425                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail" => {
5426                        if args.is_empty() {
5427                            return Err(self.syntax_err(
5428                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
5429                                line,
5430                            ));
5431                        }
5432                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
5433                        args.insert(0, lhs);
5434                    }
5435                    _ => {
5436                        args.insert(0, lhs);
5437                    }
5438                }
5439                ExprKind::FuncCall { name, args }
5440            }
5441            ExprKind::MethodCall {
5442                object,
5443                method,
5444                mut args,
5445                super_call,
5446            } => {
5447                args.insert(0, lhs);
5448                ExprKind::MethodCall {
5449                    object,
5450                    method,
5451                    args,
5452                    super_call,
5453                }
5454            }
5455            ExprKind::IndirectCall {
5456                target,
5457                mut args,
5458                ampersand,
5459                pass_caller_arglist: _,
5460            } => {
5461                args.insert(0, lhs);
5462                ExprKind::IndirectCall {
5463                    target,
5464                    args,
5465                    ampersand,
5466                    // Prepending an explicit first arg means this is no longer
5467                    // "pass the caller's @_" — that form is only bare `&$cr`.
5468                    pass_caller_arglist: false,
5469                }
5470            }
5471
5472            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
5473            ExprKind::Print { handle, mut args } => {
5474                args.insert(0, lhs);
5475                ExprKind::Print { handle, args }
5476            }
5477            ExprKind::Say { handle, mut args } => {
5478                args.insert(0, lhs);
5479                ExprKind::Say { handle, args }
5480            }
5481            ExprKind::Printf { handle, mut args } => {
5482                args.insert(0, lhs);
5483                ExprKind::Printf { handle, args }
5484            }
5485            ExprKind::Die(mut args) => {
5486                args.insert(0, lhs);
5487                ExprKind::Die(args)
5488            }
5489            ExprKind::Warn(mut args) => {
5490                args.insert(0, lhs);
5491                ExprKind::Warn(args)
5492            }
5493
5494            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
5495            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
5496            //   but piping the format string is the rarer case. Prepending
5497            //   to the values list gives `sprintf(format, lhs, ...args)` for
5498            //   the common `$n |> sprintf "count=%d"` case.
5499            ExprKind::Sprintf { format, mut args } => {
5500                args.insert(0, lhs);
5501                ExprKind::Sprintf { format, args }
5502            }
5503
5504            // ── System / exec / globbing / filesystem variadics ────────────────
5505            ExprKind::System(mut args) => {
5506                args.insert(0, lhs);
5507                ExprKind::System(args)
5508            }
5509            ExprKind::Exec(mut args) => {
5510                args.insert(0, lhs);
5511                ExprKind::Exec(args)
5512            }
5513            ExprKind::Unlink(mut args) => {
5514                args.insert(0, lhs);
5515                ExprKind::Unlink(args)
5516            }
5517            ExprKind::Chmod(mut args) => {
5518                args.insert(0, lhs);
5519                ExprKind::Chmod(args)
5520            }
5521            ExprKind::Chown(mut args) => {
5522                args.insert(0, lhs);
5523                ExprKind::Chown(args)
5524            }
5525            ExprKind::Glob(mut args) => {
5526                args.insert(0, lhs);
5527                ExprKind::Glob(args)
5528            }
5529            ExprKind::Files(mut args) => {
5530                args.insert(0, lhs);
5531                ExprKind::Files(args)
5532            }
5533            ExprKind::Filesf(mut args) => {
5534                args.insert(0, lhs);
5535                ExprKind::Filesf(args)
5536            }
5537            ExprKind::FilesfRecursive(mut args) => {
5538                args.insert(0, lhs);
5539                ExprKind::FilesfRecursive(args)
5540            }
5541            ExprKind::Dirs(mut args) => {
5542                args.insert(0, lhs);
5543                ExprKind::Dirs(args)
5544            }
5545            ExprKind::DirsRecursive(mut args) => {
5546                args.insert(0, lhs);
5547                ExprKind::DirsRecursive(args)
5548            }
5549            ExprKind::SymLinks(mut args) => {
5550                args.insert(0, lhs);
5551                ExprKind::SymLinks(args)
5552            }
5553            ExprKind::Sockets(mut args) => {
5554                args.insert(0, lhs);
5555                ExprKind::Sockets(args)
5556            }
5557            ExprKind::Pipes(mut args) => {
5558                args.insert(0, lhs);
5559                ExprKind::Pipes(args)
5560            }
5561            ExprKind::BlockDevices(mut args) => {
5562                args.insert(0, lhs);
5563                ExprKind::BlockDevices(args)
5564            }
5565            ExprKind::CharDevices(mut args) => {
5566                args.insert(0, lhs);
5567                ExprKind::CharDevices(args)
5568            }
5569            ExprKind::GlobPar { mut args, progress } => {
5570                args.insert(0, lhs);
5571                ExprKind::GlobPar { args, progress }
5572            }
5573            ExprKind::ParSed { mut args, progress } => {
5574                args.insert(0, lhs);
5575                ExprKind::ParSed { args, progress }
5576            }
5577
5578            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
5579            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
5580            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
5581            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
5582            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
5583            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
5584            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
5585            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
5586            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
5587            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
5588            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
5589            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
5590            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
5591            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
5592            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
5593            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
5594            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
5595            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
5596            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
5597            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
5598            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
5599            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
5600            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
5601            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
5602            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
5603            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
5604            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
5605            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
5606            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
5607            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
5608            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
5609            ExprKind::ScalarReverse(_) => ExprKind::ScalarReverse(Box::new(lhs)),
5610            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
5611            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
5612            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
5613            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
5614            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
5615            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
5616            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
5617            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
5618            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
5619            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
5620            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
5621            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
5622            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
5623            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
5624            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
5625            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
5626            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
5627            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
5628            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
5629            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
5630
5631            // ── Higher-order / list-taking forms: replace the `list` slot ──────
5632            ExprKind::MapExpr {
5633                block,
5634                list: _,
5635                flatten_array_refs,
5636                stream,
5637            } => ExprKind::MapExpr {
5638                block,
5639                list: Box::new(lhs),
5640                flatten_array_refs,
5641                stream,
5642            },
5643            ExprKind::MapExprComma {
5644                expr,
5645                list: _,
5646                flatten_array_refs,
5647                stream,
5648            } => ExprKind::MapExprComma {
5649                expr,
5650                list: Box::new(lhs),
5651                flatten_array_refs,
5652                stream,
5653            },
5654            ExprKind::GrepExpr {
5655                block,
5656                list: _,
5657                keyword,
5658            } => ExprKind::GrepExpr {
5659                block,
5660                list: Box::new(lhs),
5661                keyword,
5662            },
5663            ExprKind::GrepExprComma {
5664                expr,
5665                list: _,
5666                keyword,
5667            } => ExprKind::GrepExprComma {
5668                expr,
5669                list: Box::new(lhs),
5670                keyword,
5671            },
5672            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
5673                block,
5674                list: Box::new(lhs),
5675            },
5676            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
5677                cmp,
5678                list: Box::new(lhs),
5679            },
5680            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
5681                separator,
5682                list: Box::new(lhs),
5683            },
5684            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
5685                block,
5686                list: Box::new(lhs),
5687            },
5688            ExprKind::PMapExpr {
5689                block,
5690                list: _,
5691                progress,
5692                flat_outputs,
5693                on_cluster,
5694                stream,
5695            } => ExprKind::PMapExpr {
5696                block,
5697                list: Box::new(lhs),
5698                progress,
5699                flat_outputs,
5700                on_cluster,
5701                stream,
5702            },
5703            ExprKind::PMapChunkedExpr {
5704                chunk_size,
5705                block,
5706                list: _,
5707                progress,
5708            } => ExprKind::PMapChunkedExpr {
5709                chunk_size,
5710                block,
5711                list: Box::new(lhs),
5712                progress,
5713            },
5714            ExprKind::PGrepExpr {
5715                block,
5716                list: _,
5717                progress,
5718                stream,
5719            } => ExprKind::PGrepExpr {
5720                block,
5721                list: Box::new(lhs),
5722                progress,
5723                stream,
5724            },
5725            ExprKind::PForExpr {
5726                block,
5727                list: _,
5728                progress,
5729            } => ExprKind::PForExpr {
5730                block,
5731                list: Box::new(lhs),
5732                progress,
5733            },
5734            ExprKind::PSortExpr {
5735                cmp,
5736                list: _,
5737                progress,
5738            } => ExprKind::PSortExpr {
5739                cmp,
5740                list: Box::new(lhs),
5741                progress,
5742            },
5743            ExprKind::PReduceExpr {
5744                block,
5745                list: _,
5746                progress,
5747            } => ExprKind::PReduceExpr {
5748                block,
5749                list: Box::new(lhs),
5750                progress,
5751            },
5752            ExprKind::PcacheExpr {
5753                block,
5754                list: _,
5755                progress,
5756            } => ExprKind::PcacheExpr {
5757                block,
5758                list: Box::new(lhs),
5759                progress,
5760            },
5761            ExprKind::PReduceInitExpr {
5762                init,
5763                block,
5764                list: _,
5765                progress,
5766            } => ExprKind::PReduceInitExpr {
5767                init,
5768                block,
5769                list: Box::new(lhs),
5770                progress,
5771            },
5772            ExprKind::PMapReduceExpr {
5773                map_block,
5774                reduce_block,
5775                list: _,
5776                progress,
5777            } => ExprKind::PMapReduceExpr {
5778                map_block,
5779                reduce_block,
5780                list: Box::new(lhs),
5781                progress,
5782            },
5783
5784            // ── Push / unshift: first arg is the array, so pipe the LHS
5785            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
5786            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
5787            //     directly for that.
5788            ExprKind::Push { array, mut values } => {
5789                values.insert(0, lhs);
5790                ExprKind::Push { array, values }
5791            }
5792            ExprKind::Unshift { array, mut values } => {
5793                values.insert(0, lhs);
5794                ExprKind::Unshift { array, values }
5795            }
5796
5797            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
5798            ExprKind::SplitExpr {
5799                pattern,
5800                string: _,
5801                limit,
5802            } => ExprKind::SplitExpr {
5803                pattern,
5804                string: Box::new(lhs),
5805                limit,
5806            },
5807
5808            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
5809            //    Auto-inject `r` flag so the substitution returns the modified
5810            //    string instead of the match count (non-destructive / Perl /r).
5811            ExprKind::Substitution {
5812                pattern,
5813                replacement,
5814                mut flags,
5815                expr: _,
5816                delim,
5817            } => {
5818                if !flags.contains('r') {
5819                    flags.push('r');
5820                }
5821                ExprKind::Substitution {
5822                    expr: Box::new(lhs),
5823                    pattern,
5824                    replacement,
5825                    flags,
5826                    delim,
5827                }
5828            }
5829            ExprKind::Transliterate {
5830                from,
5831                to,
5832                mut flags,
5833                expr: _,
5834                delim,
5835            } => {
5836                if !flags.contains('r') {
5837                    flags.push('r');
5838                }
5839                ExprKind::Transliterate {
5840                    expr: Box::new(lhs),
5841                    from,
5842                    to,
5843                    flags,
5844                    delim,
5845                }
5846            }
5847            ExprKind::Match {
5848                pattern,
5849                flags,
5850                scalar_g,
5851                expr: _,
5852                delim,
5853            } => ExprKind::Match {
5854                expr: Box::new(lhs),
5855                pattern,
5856                flags,
5857                scalar_g,
5858                delim,
5859            },
5860            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
5861            ExprKind::Regex(pattern, flags) => ExprKind::Match {
5862                expr: Box::new(lhs),
5863                pattern,
5864                flags,
5865                scalar_g: false,
5866                delim: '/',
5867            },
5868
5869            // ── Bareword function name → plain unary call ──────────────────────
5870            ExprKind::Bareword(name) => match name.as_str() {
5871                "rv" | "reverse" | "reversed" => ExprKind::ReverseExpr(Box::new(lhs)),
5872                "rev" => ExprKind::ScalarReverse(Box::new(lhs)),
5873                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
5874                    name: "uniq".to_string(),
5875                    args: vec![lhs],
5876                },
5877                "fl" | "flatten" => ExprKind::FuncCall {
5878                    name: "flatten".to_string(),
5879                    args: vec![lhs],
5880                },
5881                _ => ExprKind::FuncCall {
5882                    name,
5883                    args: vec![lhs],
5884                },
5885            },
5886
5887            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
5888            kind @ (ExprKind::ScalarVar(_)
5889            | ExprKind::ArrayElement { .. }
5890            | ExprKind::HashElement { .. }
5891            | ExprKind::Deref { .. }
5892            | ExprKind::ArrowDeref { .. }
5893            | ExprKind::CodeRef { .. }
5894            | ExprKind::SubroutineRef(_)
5895            | ExprKind::SubroutineCodeRef(_)
5896            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
5897                target: Box::new(Expr { kind, line: rline }),
5898                args: vec![lhs],
5899                ampersand: false,
5900                pass_caller_arglist: false,
5901            },
5902
5903            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
5904            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
5905            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> sub { ... }`.
5906            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
5907                ExprKind::IndirectCall {
5908                    target: inner,
5909                    args: vec![lhs],
5910                    ampersand: false,
5911                    pass_caller_arglist: false,
5912                }
5913            }
5914
5915            other => {
5916                return Err(self.syntax_err(
5917                    format!(
5918                        "right-hand side of `|>` must be a call, builtin, or coderef \
5919                         expression (got {})",
5920                        Self::expr_kind_name(&other)
5921                    ),
5922                    line,
5923                ));
5924            }
5925        };
5926        Ok(Expr {
5927            kind: new_kind,
5928            line,
5929        })
5930    }
5931
5932    /// Short label for an `ExprKind` (used in `|>` error messages).
5933    fn expr_kind_name(kind: &ExprKind) -> &'static str {
5934        match kind {
5935            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
5936            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
5937            ExprKind::BinOp { .. } => "binary expression",
5938            ExprKind::UnaryOp { .. } => "unary expression",
5939            ExprKind::Ternary { .. } => "ternary expression",
5940            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
5941            ExprKind::List(_) => "list expression",
5942            ExprKind::Range { .. } => "range expression",
5943            _ => "expression",
5944        }
5945    }
5946
5947    // or / not (lowest precedence word operators)
5948    fn parse_or_word(&mut self) -> PerlResult<Expr> {
5949        let mut left = self.parse_and_word()?;
5950        while matches!(self.peek(), Token::LogOrWord) {
5951            let line = left.line;
5952            self.advance();
5953            let right = self.parse_and_word()?;
5954            left = Expr {
5955                kind: ExprKind::BinOp {
5956                    left: Box::new(left),
5957                    op: BinOp::LogOrWord,
5958                    right: Box::new(right),
5959                },
5960                line,
5961            };
5962        }
5963        Ok(left)
5964    }
5965
5966    fn parse_and_word(&mut self) -> PerlResult<Expr> {
5967        let mut left = self.parse_not_word()?;
5968        while matches!(self.peek(), Token::LogAndWord) {
5969            let line = left.line;
5970            self.advance();
5971            let right = self.parse_not_word()?;
5972            left = Expr {
5973                kind: ExprKind::BinOp {
5974                    left: Box::new(left),
5975                    op: BinOp::LogAndWord,
5976                    right: Box::new(right),
5977                },
5978                line,
5979            };
5980        }
5981        Ok(left)
5982    }
5983
5984    fn parse_not_word(&mut self) -> PerlResult<Expr> {
5985        if matches!(self.peek(), Token::LogNotWord) {
5986            let line = self.peek_line();
5987            self.advance();
5988            let expr = self.parse_not_word()?;
5989            return Ok(Expr {
5990                kind: ExprKind::UnaryOp {
5991                    op: UnaryOp::LogNotWord,
5992                    expr: Box::new(expr),
5993                },
5994                line,
5995            });
5996        }
5997        self.parse_range()
5998    }
5999
6000    fn parse_log_or(&mut self) -> PerlResult<Expr> {
6001        let mut left = self.parse_log_and()?;
6002        loop {
6003            let op = match self.peek() {
6004                Token::LogOr => BinOp::LogOr,
6005                Token::DefinedOr => BinOp::DefinedOr,
6006                _ => break,
6007            };
6008            let line = left.line;
6009            self.advance();
6010            let right = self.parse_log_and()?;
6011            left = Expr {
6012                kind: ExprKind::BinOp {
6013                    left: Box::new(left),
6014                    op,
6015                    right: Box::new(right),
6016                },
6017                line,
6018            };
6019        }
6020        Ok(left)
6021    }
6022
6023    fn parse_log_and(&mut self) -> PerlResult<Expr> {
6024        let mut left = self.parse_bit_or()?;
6025        while matches!(self.peek(), Token::LogAnd) {
6026            let line = left.line;
6027            self.advance();
6028            let right = self.parse_bit_or()?;
6029            left = Expr {
6030                kind: ExprKind::BinOp {
6031                    left: Box::new(left),
6032                    op: BinOp::LogAnd,
6033                    right: Box::new(right),
6034                },
6035                line,
6036            };
6037        }
6038        Ok(left)
6039    }
6040
6041    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
6042        let mut left = self.parse_bit_xor()?;
6043        while matches!(self.peek(), Token::BitOr) {
6044            let line = left.line;
6045            self.advance();
6046            let right = self.parse_bit_xor()?;
6047            left = Expr {
6048                kind: ExprKind::BinOp {
6049                    left: Box::new(left),
6050                    op: BinOp::BitOr,
6051                    right: Box::new(right),
6052                },
6053                line,
6054            };
6055        }
6056        Ok(left)
6057    }
6058
6059    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
6060        let mut left = self.parse_bit_and()?;
6061        while matches!(self.peek(), Token::BitXor) {
6062            let line = left.line;
6063            self.advance();
6064            let right = self.parse_bit_and()?;
6065            left = Expr {
6066                kind: ExprKind::BinOp {
6067                    left: Box::new(left),
6068                    op: BinOp::BitXor,
6069                    right: Box::new(right),
6070                },
6071                line,
6072            };
6073        }
6074        Ok(left)
6075    }
6076
6077    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
6078        let mut left = self.parse_equality()?;
6079        while matches!(self.peek(), Token::BitAnd) {
6080            let line = left.line;
6081            self.advance();
6082            let right = self.parse_equality()?;
6083            left = Expr {
6084                kind: ExprKind::BinOp {
6085                    left: Box::new(left),
6086                    op: BinOp::BitAnd,
6087                    right: Box::new(right),
6088                },
6089                line,
6090            };
6091        }
6092        Ok(left)
6093    }
6094
6095    fn parse_equality(&mut self) -> PerlResult<Expr> {
6096        let mut left = self.parse_comparison()?;
6097        loop {
6098            let op = match self.peek() {
6099                Token::NumEq => BinOp::NumEq,
6100                Token::NumNe => BinOp::NumNe,
6101                Token::StrEq => BinOp::StrEq,
6102                Token::StrNe => BinOp::StrNe,
6103                Token::Spaceship => BinOp::Spaceship,
6104                Token::StrCmp => BinOp::StrCmp,
6105                _ => break,
6106            };
6107            let line = left.line;
6108            self.advance();
6109            let right = self.parse_comparison()?;
6110            left = Expr {
6111                kind: ExprKind::BinOp {
6112                    left: Box::new(left),
6113                    op,
6114                    right: Box::new(right),
6115                },
6116                line,
6117            };
6118        }
6119        Ok(left)
6120    }
6121
6122    fn parse_comparison(&mut self) -> PerlResult<Expr> {
6123        let mut left = self.parse_shift()?;
6124        loop {
6125            let op = match self.peek() {
6126                Token::NumLt => BinOp::NumLt,
6127                Token::NumGt => BinOp::NumGt,
6128                Token::NumLe => BinOp::NumLe,
6129                Token::NumGe => BinOp::NumGe,
6130                Token::StrLt => BinOp::StrLt,
6131                Token::StrGt => BinOp::StrGt,
6132                Token::StrLe => BinOp::StrLe,
6133                Token::StrGe => BinOp::StrGe,
6134                _ => break,
6135            };
6136            let line = left.line;
6137            self.advance();
6138            let right = self.parse_shift()?;
6139            left = Expr {
6140                kind: ExprKind::BinOp {
6141                    left: Box::new(left),
6142                    op,
6143                    right: Box::new(right),
6144                },
6145                line,
6146            };
6147        }
6148        Ok(left)
6149    }
6150
6151    fn parse_shift(&mut self) -> PerlResult<Expr> {
6152        let mut left = self.parse_addition()?;
6153        loop {
6154            let op = match self.peek() {
6155                Token::ShiftLeft => BinOp::ShiftLeft,
6156                Token::ShiftRight => BinOp::ShiftRight,
6157                _ => break,
6158            };
6159            let line = left.line;
6160            self.advance();
6161            let right = self.parse_addition()?;
6162            left = Expr {
6163                kind: ExprKind::BinOp {
6164                    left: Box::new(left),
6165                    op,
6166                    right: Box::new(right),
6167                },
6168                line,
6169            };
6170        }
6171        Ok(left)
6172    }
6173
6174    fn parse_addition(&mut self) -> PerlResult<Expr> {
6175        let mut left = self.parse_multiplication()?;
6176        loop {
6177            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
6178            // the next statement, not a binary operator continuing this expression.
6179            let op = match self.peek() {
6180                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
6181                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
6182                Token::Dot => BinOp::Concat,
6183                _ => break,
6184            };
6185            let line = left.line;
6186            self.advance();
6187            let right = self.parse_multiplication()?;
6188            left = Expr {
6189                kind: ExprKind::BinOp {
6190                    left: Box::new(left),
6191                    op,
6192                    right: Box::new(right),
6193                },
6194                line,
6195            };
6196        }
6197        Ok(left)
6198    }
6199
6200    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
6201        let mut left = self.parse_regex_bind()?;
6202        loop {
6203            let op = match self.peek() {
6204                Token::Star => BinOp::Mul,
6205                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
6206                // Implicit semicolon: `%` on a new line is a hash dereference or hash
6207                // sigil for the next statement, not modulo operator on this expression.
6208                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
6209                Token::X => {
6210                    let line = left.line;
6211                    self.advance();
6212                    let right = self.parse_regex_bind()?;
6213                    left = Expr {
6214                        kind: ExprKind::Repeat {
6215                            expr: Box::new(left),
6216                            count: Box::new(right),
6217                        },
6218                        line,
6219                    };
6220                    continue;
6221                }
6222                _ => break,
6223            };
6224            let line = left.line;
6225            self.advance();
6226            let right = self.parse_regex_bind()?;
6227            left = Expr {
6228                kind: ExprKind::BinOp {
6229                    left: Box::new(left),
6230                    op,
6231                    right: Box::new(right),
6232                },
6233                line,
6234            };
6235        }
6236        Ok(left)
6237    }
6238
6239    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
6240        let left = self.parse_unary()?;
6241        match self.peek() {
6242            Token::BindMatch => {
6243                let line = left.line;
6244                self.advance();
6245                match self.peek().clone() {
6246                    Token::Regex(pattern, flags, delim) => {
6247                        self.advance();
6248                        Ok(Expr {
6249                            kind: ExprKind::Match {
6250                                expr: Box::new(left),
6251                                pattern,
6252                                flags,
6253                                scalar_g: false,
6254                                delim,
6255                            },
6256                            line,
6257                        })
6258                    }
6259                    Token::Ident(ref s) if s.starts_with('\x00') => {
6260                        let (Token::Ident(encoded), _) = self.advance() else {
6261                            unreachable!()
6262                        };
6263                        let parts: Vec<&str> = encoded.split('\x00').collect();
6264                        if parts.len() >= 4 && parts[1] == "s" {
6265                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6266                            Ok(Expr {
6267                                kind: ExprKind::Substitution {
6268                                    expr: Box::new(left),
6269                                    pattern: parts[2].to_string(),
6270                                    replacement: parts[3].to_string(),
6271                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6272                                    delim,
6273                                },
6274                                line,
6275                            })
6276                        } else if parts.len() >= 4 && parts[1] == "tr" {
6277                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6278                            Ok(Expr {
6279                                kind: ExprKind::Transliterate {
6280                                    expr: Box::new(left),
6281                                    from: parts[2].to_string(),
6282                                    to: parts[3].to_string(),
6283                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6284                                    delim,
6285                                },
6286                                line,
6287                            })
6288                        } else {
6289                            Err(self.syntax_err("Invalid regex binding", line))
6290                        }
6291                    }
6292                    _ => {
6293                        let rhs = self.parse_unary()?;
6294                        Ok(Expr {
6295                            kind: ExprKind::BinOp {
6296                                left: Box::new(left),
6297                                op: BinOp::BindMatch,
6298                                right: Box::new(rhs),
6299                            },
6300                            line,
6301                        })
6302                    }
6303                }
6304            }
6305            Token::BindNotMatch => {
6306                let line = left.line;
6307                self.advance();
6308                match self.peek().clone() {
6309                    Token::Regex(pattern, flags, delim) => {
6310                        self.advance();
6311                        Ok(Expr {
6312                            kind: ExprKind::UnaryOp {
6313                                op: UnaryOp::LogNot,
6314                                expr: Box::new(Expr {
6315                                    kind: ExprKind::Match {
6316                                        expr: Box::new(left),
6317                                        pattern,
6318                                        flags,
6319                                        scalar_g: false,
6320                                        delim,
6321                                    },
6322                                    line,
6323                                }),
6324                            },
6325                            line,
6326                        })
6327                    }
6328                    Token::Ident(ref s) if s.starts_with('\x00') => {
6329                        let (Token::Ident(encoded), _) = self.advance() else {
6330                            unreachable!()
6331                        };
6332                        let parts: Vec<&str> = encoded.split('\x00').collect();
6333                        if parts.len() >= 4 && parts[1] == "s" {
6334                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6335                            Ok(Expr {
6336                                kind: ExprKind::UnaryOp {
6337                                    op: UnaryOp::LogNot,
6338                                    expr: Box::new(Expr {
6339                                        kind: ExprKind::Substitution {
6340                                            expr: Box::new(left),
6341                                            pattern: parts[2].to_string(),
6342                                            replacement: parts[3].to_string(),
6343                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6344                                            delim,
6345                                        },
6346                                        line,
6347                                    }),
6348                                },
6349                                line,
6350                            })
6351                        } else if parts.len() >= 4 && parts[1] == "tr" {
6352                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6353                            Ok(Expr {
6354                                kind: ExprKind::UnaryOp {
6355                                    op: UnaryOp::LogNot,
6356                                    expr: Box::new(Expr {
6357                                        kind: ExprKind::Transliterate {
6358                                            expr: Box::new(left),
6359                                            from: parts[2].to_string(),
6360                                            to: parts[3].to_string(),
6361                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6362                                            delim,
6363                                        },
6364                                        line,
6365                                    }),
6366                                },
6367                                line,
6368                            })
6369                        } else {
6370                            Err(self.syntax_err("Invalid regex binding after !~", line))
6371                        }
6372                    }
6373                    _ => {
6374                        let rhs = self.parse_unary()?;
6375                        Ok(Expr {
6376                            kind: ExprKind::BinOp {
6377                                left: Box::new(left),
6378                                op: BinOp::BindNotMatch,
6379                                right: Box::new(rhs),
6380                            },
6381                            line,
6382                        })
6383                    }
6384                }
6385            }
6386            _ => Ok(left),
6387        }
6388    }
6389
6390    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
6391    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
6392    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
6393        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
6394        let result = self.parse_range();
6395        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
6396        result
6397    }
6398
6399    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
6400    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
6401    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
6402    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
6403    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
6404    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
6405    fn parse_range(&mut self) -> PerlResult<Expr> {
6406        let left = self.parse_log_or()?;
6407        let line = left.line;
6408        let exclusive = if self.eat(&Token::RangeExclusive) {
6409            true
6410        } else if self.eat(&Token::Range) {
6411            false
6412        } else {
6413            return Ok(left);
6414        };
6415        let right = self.parse_log_or()?;
6416        Ok(Expr {
6417            kind: ExprKind::Range {
6418                from: Box::new(left),
6419                to: Box::new(right),
6420                exclusive,
6421            },
6422            line,
6423        })
6424    }
6425
6426    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
6427    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
6428        let mut name = match self.advance() {
6429            (Token::Ident(n), _) => n,
6430            (tok, l) => {
6431                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
6432            }
6433        };
6434        while self.eat(&Token::PackageSep) {
6435            match self.advance() {
6436                (Token::Ident(part), _) => {
6437                    name.push_str("::");
6438                    name.push_str(&part);
6439                }
6440                (tok, l) => {
6441                    return Err(self
6442                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
6443                }
6444            }
6445        }
6446        Ok(name)
6447    }
6448
6449    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
6450    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
6451        self.parse_package_qualified_identifier()
6452    }
6453
6454    fn parse_unary(&mut self) -> PerlResult<Expr> {
6455        let line = self.peek_line();
6456        match self.peek().clone() {
6457            Token::Minus => {
6458                self.advance();
6459                let expr = self.parse_power()?;
6460                Ok(Expr {
6461                    kind: ExprKind::UnaryOp {
6462                        op: UnaryOp::Negate,
6463                        expr: Box::new(expr),
6464                    },
6465                    line,
6466                })
6467            }
6468            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
6469            // and for scalar context; treat as a no-op on the parsed operand.
6470            Token::Plus => {
6471                self.advance();
6472                self.parse_unary()
6473            }
6474            Token::LogNot => {
6475                self.advance();
6476                let expr = self.parse_unary()?;
6477                Ok(Expr {
6478                    kind: ExprKind::UnaryOp {
6479                        op: UnaryOp::LogNot,
6480                        expr: Box::new(expr),
6481                    },
6482                    line,
6483                })
6484            }
6485            Token::BitNot => {
6486                self.advance();
6487                let expr = self.parse_unary()?;
6488                Ok(Expr {
6489                    kind: ExprKind::UnaryOp {
6490                        op: UnaryOp::BitNot,
6491                        expr: Box::new(expr),
6492                    },
6493                    line,
6494                })
6495            }
6496            Token::Increment => {
6497                self.advance();
6498                let expr = self.parse_postfix()?;
6499                Ok(Expr {
6500                    kind: ExprKind::UnaryOp {
6501                        op: UnaryOp::PreIncrement,
6502                        expr: Box::new(expr),
6503                    },
6504                    line,
6505                })
6506            }
6507            Token::Decrement => {
6508                self.advance();
6509                let expr = self.parse_postfix()?;
6510                Ok(Expr {
6511                    kind: ExprKind::UnaryOp {
6512                        op: UnaryOp::PreDecrement,
6513                        expr: Box::new(expr),
6514                    },
6515                    line,
6516                })
6517            }
6518            Token::BitAnd => {
6519                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
6520                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
6521                self.advance();
6522                if matches!(self.peek(), Token::LBrace) {
6523                    self.advance();
6524                    let inner = self.parse_expression()?;
6525                    self.expect(&Token::RBrace)?;
6526                    return Ok(Expr {
6527                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
6528                        line,
6529                    });
6530                }
6531                if matches!(self.peek(), Token::Ident(_)) {
6532                    let name = self.parse_qualified_subroutine_name()?;
6533                    return Ok(Expr {
6534                        kind: ExprKind::SubroutineRef(name),
6535                        line,
6536                    });
6537                }
6538                let target = self.parse_primary()?;
6539                if matches!(self.peek(), Token::LParen) {
6540                    self.advance();
6541                    let args = self.parse_arg_list()?;
6542                    self.expect(&Token::RParen)?;
6543                    return Ok(Expr {
6544                        kind: ExprKind::IndirectCall {
6545                            target: Box::new(target),
6546                            args,
6547                            ampersand: true,
6548                            pass_caller_arglist: false,
6549                        },
6550                        line,
6551                    });
6552                }
6553                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
6554                Ok(Expr {
6555                    kind: ExprKind::IndirectCall {
6556                        target: Box::new(target),
6557                        args: vec![],
6558                        ampersand: true,
6559                        pass_caller_arglist: true,
6560                    },
6561                    line,
6562                })
6563            }
6564            Token::Backslash => {
6565                self.advance();
6566                let expr = self.parse_unary()?;
6567                if let ExprKind::SubroutineRef(name) = expr.kind {
6568                    return Ok(Expr {
6569                        kind: ExprKind::SubroutineCodeRef(name),
6570                        line,
6571                    });
6572                }
6573                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
6574                    return Ok(expr);
6575                }
6576                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
6577                Ok(Expr {
6578                    kind: ExprKind::ScalarRef(Box::new(expr)),
6579                    line,
6580                })
6581            }
6582            Token::FileTest(op) => {
6583                self.advance();
6584                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
6585                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
6586                    Expr {
6587                        kind: ExprKind::ScalarVar("_".into()),
6588                        line: self.peek_line(),
6589                    }
6590                } else {
6591                    self.parse_unary()?
6592                };
6593                Ok(Expr {
6594                    kind: ExprKind::FileTest {
6595                        op,
6596                        expr: Box::new(expr),
6597                    },
6598                    line,
6599                })
6600            }
6601            _ => self.parse_power(),
6602        }
6603    }
6604
6605    fn parse_power(&mut self) -> PerlResult<Expr> {
6606        let left = self.parse_postfix()?;
6607        if matches!(self.peek(), Token::Power) {
6608            let line = left.line;
6609            self.advance();
6610            let right = self.parse_unary()?; // right-associative
6611            return Ok(Expr {
6612                kind: ExprKind::BinOp {
6613                    left: Box::new(left),
6614                    op: BinOp::Pow,
6615                    right: Box::new(right),
6616                },
6617                line,
6618            });
6619        }
6620        Ok(left)
6621    }
6622
6623    fn parse_postfix(&mut self) -> PerlResult<Expr> {
6624        let mut expr = self.parse_primary()?;
6625        loop {
6626            match self.peek().clone() {
6627                Token::Increment => {
6628                    // Implicit semicolon: `++` on a new line is a prefix operator
6629                    // on the next statement, not postfix on the previous expression.
6630                    if self.peek_line() > self.prev_line() {
6631                        break;
6632                    }
6633                    let line = expr.line;
6634                    self.advance();
6635                    expr = Expr {
6636                        kind: ExprKind::PostfixOp {
6637                            expr: Box::new(expr),
6638                            op: PostfixOp::Increment,
6639                        },
6640                        line,
6641                    };
6642                }
6643                Token::Decrement => {
6644                    // Implicit semicolon: `--` on a new line is a prefix operator
6645                    // on the next statement, not postfix on the previous expression.
6646                    if self.peek_line() > self.prev_line() {
6647                        break;
6648                    }
6649                    let line = expr.line;
6650                    self.advance();
6651                    expr = Expr {
6652                        kind: ExprKind::PostfixOp {
6653                            expr: Box::new(expr),
6654                            op: PostfixOp::Decrement,
6655                        },
6656                        line,
6657                    };
6658                }
6659                Token::LParen => {
6660                    if self.suppress_indirect_paren_call > 0 {
6661                        break;
6662                    }
6663                    // Implicit semicolon: `(` on a new line after an expression
6664                    // is a new statement, not a postfix code-ref call.
6665                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
6666                    if self.peek_line() > self.prev_line() {
6667                        break;
6668                    }
6669                    let line = expr.line;
6670                    self.advance();
6671                    let args = self.parse_arg_list()?;
6672                    self.expect(&Token::RParen)?;
6673                    expr = Expr {
6674                        kind: ExprKind::IndirectCall {
6675                            target: Box::new(expr),
6676                            args,
6677                            ampersand: false,
6678                            pass_caller_arglist: false,
6679                        },
6680                        line,
6681                    };
6682                }
6683                Token::Arrow => {
6684                    let line = expr.line;
6685                    self.advance();
6686                    match self.peek().clone() {
6687                        Token::LBracket => {
6688                            self.advance();
6689                            let index = self.parse_expression()?;
6690                            self.expect(&Token::RBracket)?;
6691                            expr = Expr {
6692                                kind: ExprKind::ArrowDeref {
6693                                    expr: Box::new(expr),
6694                                    index: Box::new(index),
6695                                    kind: DerefKind::Array,
6696                                },
6697                                line,
6698                            };
6699                        }
6700                        Token::LBrace => {
6701                            self.advance();
6702                            let key = self.parse_hash_subscript_key()?;
6703                            self.expect(&Token::RBrace)?;
6704                            expr = Expr {
6705                                kind: ExprKind::ArrowDeref {
6706                                    expr: Box::new(expr),
6707                                    index: Box::new(key),
6708                                    kind: DerefKind::Hash,
6709                                },
6710                                line,
6711                            };
6712                        }
6713                        Token::LParen => {
6714                            self.advance();
6715                            let args = self.parse_arg_list()?;
6716                            self.expect(&Token::RParen)?;
6717                            expr = Expr {
6718                                kind: ExprKind::ArrowDeref {
6719                                    expr: Box::new(expr),
6720                                    index: Box::new(Expr {
6721                                        kind: ExprKind::List(args),
6722                                        line,
6723                                    }),
6724                                    kind: DerefKind::Call,
6725                                },
6726                                line,
6727                            };
6728                        }
6729                        Token::Ident(method) => {
6730                            self.advance();
6731                            if method == "SUPER" {
6732                                self.expect(&Token::PackageSep)?;
6733                                let real_method = match self.advance() {
6734                                    (Token::Ident(n), _) => n,
6735                                    (tok, l) => {
6736                                        return Err(self.syntax_err(
6737                                            format!(
6738                                                "Expected method name after SUPER::, got {:?}",
6739                                                tok
6740                                            ),
6741                                            l,
6742                                        ));
6743                                    }
6744                                };
6745                                let args = if self.eat(&Token::LParen) {
6746                                    let a = self.parse_arg_list()?;
6747                                    self.expect(&Token::RParen)?;
6748                                    a
6749                                } else {
6750                                    self.parse_method_arg_list_no_paren()?
6751                                };
6752                                expr = Expr {
6753                                    kind: ExprKind::MethodCall {
6754                                        object: Box::new(expr),
6755                                        method: real_method,
6756                                        args,
6757                                        super_call: true,
6758                                    },
6759                                    line,
6760                                };
6761                            } else {
6762                                let mut method_name = method;
6763                                while self.eat(&Token::PackageSep) {
6764                                    match self.advance() {
6765                                        (Token::Ident(part), _) => {
6766                                            method_name.push_str("::");
6767                                            method_name.push_str(&part);
6768                                        }
6769                                        (tok, l) => {
6770                                            return Err(self.syntax_err(
6771                                                format!(
6772                                                    "Expected identifier after :: in method name, got {:?}",
6773                                                    tok
6774                                                ),
6775                                                l,
6776                                            ));
6777                                        }
6778                                    }
6779                                }
6780                                let args = if self.eat(&Token::LParen) {
6781                                    let a = self.parse_arg_list()?;
6782                                    self.expect(&Token::RParen)?;
6783                                    a
6784                                } else {
6785                                    self.parse_method_arg_list_no_paren()?
6786                                };
6787                                expr = Expr {
6788                                    kind: ExprKind::MethodCall {
6789                                        object: Box::new(expr),
6790                                        method: method_name,
6791                                        args,
6792                                        super_call: false,
6793                                    },
6794                                    line,
6795                                };
6796                            }
6797                        }
6798                        // Postfix dereference (Perl 5.20+, default 5.24+):
6799                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
6800                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
6801                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
6802                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
6803                        Token::ArrayAt => {
6804                            self.advance(); // consume `@`
6805                            match self.peek().clone() {
6806                                Token::Star => {
6807                                    self.advance();
6808                                    expr = Expr {
6809                                        kind: ExprKind::Deref {
6810                                            expr: Box::new(expr),
6811                                            kind: Sigil::Array,
6812                                        },
6813                                        line,
6814                                    };
6815                                }
6816                                Token::LBracket => {
6817                                    self.advance();
6818                                    let mut indices = Vec::new();
6819                                    while !matches!(self.peek(), Token::RBracket | Token::Eof) {
6820                                        indices.push(self.parse_assign_expr()?);
6821                                        if !self.eat(&Token::Comma) {
6822                                            break;
6823                                        }
6824                                    }
6825                                    self.expect(&Token::RBracket)?;
6826                                    let source = Expr {
6827                                        kind: ExprKind::Deref {
6828                                            expr: Box::new(expr),
6829                                            kind: Sigil::Array,
6830                                        },
6831                                        line,
6832                                    };
6833                                    expr = Expr {
6834                                        kind: ExprKind::AnonymousListSlice {
6835                                            source: Box::new(source),
6836                                            indices,
6837                                        },
6838                                        line,
6839                                    };
6840                                }
6841                                Token::LBrace => {
6842                                    self.advance();
6843                                    let mut keys = Vec::new();
6844                                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
6845                                        keys.push(self.parse_assign_expr()?);
6846                                        if !self.eat(&Token::Comma) {
6847                                            break;
6848                                        }
6849                                    }
6850                                    self.expect(&Token::RBrace)?;
6851                                    expr = Expr {
6852                                        kind: ExprKind::HashSliceDeref {
6853                                            container: Box::new(expr),
6854                                            keys,
6855                                        },
6856                                        line,
6857                                    };
6858                                }
6859                                tok => {
6860                                    return Err(self.syntax_err(
6861                                        format!(
6862                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
6863                                            tok
6864                                        ),
6865                                        line,
6866                                    ));
6867                                }
6868                            }
6869                        }
6870                        Token::HashPercent => {
6871                            self.advance(); // consume `%`
6872                            match self.peek().clone() {
6873                                Token::Star => {
6874                                    self.advance();
6875                                    expr = Expr {
6876                                        kind: ExprKind::Deref {
6877                                            expr: Box::new(expr),
6878                                            kind: Sigil::Hash,
6879                                        },
6880                                        line,
6881                                    };
6882                                }
6883                                tok => {
6884                                    return Err(self.syntax_err(
6885                                        format!("Expected `*` after `->%`, got {:?}", tok),
6886                                        line,
6887                                    ));
6888                                }
6889                            }
6890                        }
6891                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
6892                        Token::X => {
6893                            self.advance();
6894                            let args = if self.eat(&Token::LParen) {
6895                                let a = self.parse_arg_list()?;
6896                                self.expect(&Token::RParen)?;
6897                                a
6898                            } else {
6899                                self.parse_method_arg_list_no_paren()?
6900                            };
6901                            expr = Expr {
6902                                kind: ExprKind::MethodCall {
6903                                    object: Box::new(expr),
6904                                    method: "x".to_string(),
6905                                    args,
6906                                    super_call: false,
6907                                },
6908                                line,
6909                            };
6910                        }
6911                        _ => break,
6912                    }
6913                }
6914                Token::LBracket => {
6915                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
6916                    let line = expr.line;
6917                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
6918                        if let ExprKind::ScalarVar(ref name) = expr.kind {
6919                            let name = name.clone();
6920                            self.advance();
6921                            let index = self.parse_expression()?;
6922                            self.expect(&Token::RBracket)?;
6923                            expr = Expr {
6924                                kind: ExprKind::ArrayElement {
6925                                    array: name,
6926                                    index: Box::new(index),
6927                                },
6928                                line,
6929                            };
6930                        }
6931                    } else if postfix_lbracket_is_arrow_container(&expr) {
6932                        self.advance();
6933                        let indices = self.parse_arg_list()?;
6934                        self.expect(&Token::RBracket)?;
6935                        expr = Expr {
6936                            kind: ExprKind::ArrowDeref {
6937                                expr: Box::new(expr),
6938                                index: Box::new(Expr {
6939                                    kind: ExprKind::List(indices),
6940                                    line,
6941                                }),
6942                                kind: DerefKind::Array,
6943                            },
6944                            line,
6945                        };
6946                    } else {
6947                        self.advance();
6948                        let indices = self.parse_arg_list()?;
6949                        self.expect(&Token::RBracket)?;
6950                        expr = Expr {
6951                            kind: ExprKind::AnonymousListSlice {
6952                                source: Box::new(expr),
6953                                indices,
6954                            },
6955                            line,
6956                        };
6957                    }
6958                }
6959                Token::LBrace => {
6960                    if self.suppress_scalar_hash_brace > 0 {
6961                        break;
6962                    }
6963                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
6964                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
6965                    let line = expr.line;
6966                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
6967                    let is_chainable_hash_subscript = is_scalar_named_hash
6968                        || matches!(
6969                            expr.kind,
6970                            ExprKind::HashElement { .. }
6971                                | ExprKind::ArrayElement { .. }
6972                                | ExprKind::ArrowDeref { .. }
6973                                | ExprKind::Deref {
6974                                    kind: Sigil::Scalar,
6975                                    ..
6976                                }
6977                        );
6978                    if !is_chainable_hash_subscript {
6979                        break;
6980                    }
6981                    self.advance();
6982                    let key = self.parse_hash_subscript_key()?;
6983                    self.expect(&Token::RBrace)?;
6984                    expr = if is_scalar_named_hash {
6985                        if let ExprKind::ScalarVar(ref name) = expr.kind {
6986                            let name = name.clone();
6987                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
6988                            if name == "_" {
6989                                Expr {
6990                                    kind: ExprKind::ArrowDeref {
6991                                        expr: Box::new(Expr {
6992                                            kind: ExprKind::ScalarVar("_".into()),
6993                                            line,
6994                                        }),
6995                                        index: Box::new(key),
6996                                        kind: DerefKind::Hash,
6997                                    },
6998                                    line,
6999                                }
7000                            } else {
7001                                Expr {
7002                                    kind: ExprKind::HashElement {
7003                                        hash: name,
7004                                        key: Box::new(key),
7005                                    },
7006                                    line,
7007                                }
7008                            }
7009                        } else {
7010                            unreachable!("is_scalar_named_hash implies ScalarVar");
7011                        }
7012                    } else {
7013                        Expr {
7014                            kind: ExprKind::ArrowDeref {
7015                                expr: Box::new(expr),
7016                                index: Box::new(key),
7017                                kind: DerefKind::Hash,
7018                            },
7019                            line,
7020                        }
7021                    };
7022                }
7023                _ => break,
7024            }
7025        }
7026        Ok(expr)
7027    }
7028
7029    fn parse_primary(&mut self) -> PerlResult<Expr> {
7030        let line = self.peek_line();
7031        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
7032        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
7033        // assigned value(s); has the side effect of declaring the variable in
7034        // the current scope.  See `ExprKind::MyExpr`.
7035        if let Token::Ident(ref kw) = self.peek().clone() {
7036            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
7037                let kw_owned = kw.clone();
7038                // Parse exactly like the statement form via `parse_my_our_local`,
7039                // then unwrap the resulting `StmtKind::*` back into a list of
7040                // `VarDecl`s for the expression node.  This re-uses the full
7041                // syntax (typed sigs, list destructuring, type annotations).
7042                let saved_pos = self.pos;
7043                let stmt = self.parse_my_our_local(&kw_owned, false)?;
7044                let decls = match stmt.kind {
7045                    StmtKind::My(d)
7046                    | StmtKind::Our(d)
7047                    | StmtKind::State(d)
7048                    | StmtKind::Local(d) => d,
7049                    _ => {
7050                        // `local *FOO = …` / non-decl forms — fall back to the
7051                        // statement parser (already advanced); restore position
7052                        // and let the surrounding code handle it as a statement
7053                        // by erroring loudly here.
7054                        self.pos = saved_pos;
7055                        return Err(self.syntax_err(
7056                            "`my`/`our`/`local` in expression must declare variables",
7057                            line,
7058                        ));
7059                    }
7060                };
7061                return Ok(Expr {
7062                    kind: ExprKind::MyExpr {
7063                        keyword: kw_owned,
7064                        decls,
7065                    },
7066                    line,
7067                });
7068            }
7069        }
7070        match self.peek().clone() {
7071            Token::Integer(n) => {
7072                self.advance();
7073                Ok(Expr {
7074                    kind: ExprKind::Integer(n),
7075                    line,
7076                })
7077            }
7078            Token::Float(f) => {
7079                self.advance();
7080                Ok(Expr {
7081                    kind: ExprKind::Float(f),
7082                    line,
7083                })
7084            }
7085            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
7086            // Valid in any expression position; evaluates the block and yields its last value.
7087            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
7088            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
7089            // instead pipe-applied as a coderef — that path is never reached from here.
7090            Token::ArrowBrace => {
7091                self.advance();
7092                let mut stmts = Vec::new();
7093                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7094                    if self.eat(&Token::Semicolon) {
7095                        continue;
7096                    }
7097                    stmts.push(self.parse_statement()?);
7098                }
7099                self.expect(&Token::RBrace)?;
7100                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
7101                let inner = Expr {
7102                    kind: ExprKind::CodeRef {
7103                        params: vec![],
7104                        body: stmts,
7105                    },
7106                    line: inner_line,
7107                };
7108                Ok(Expr {
7109                    kind: ExprKind::Do(Box::new(inner)),
7110                    line,
7111                })
7112            }
7113            Token::Star => {
7114                self.advance();
7115                if matches!(self.peek(), Token::LBrace) {
7116                    self.advance();
7117                    let inner = self.parse_expression()?;
7118                    self.expect(&Token::RBrace)?;
7119                    return Ok(Expr {
7120                        kind: ExprKind::Deref {
7121                            expr: Box::new(inner),
7122                            kind: Sigil::Typeglob,
7123                        },
7124                        line,
7125                    });
7126                }
7127                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
7128                if matches!(
7129                    self.peek(),
7130                    Token::ScalarVar(_)
7131                        | Token::ArrayVar(_)
7132                        | Token::HashVar(_)
7133                        | Token::DerefScalarVar(_)
7134                        | Token::HashPercent
7135                ) {
7136                    let inner = self.parse_postfix()?;
7137                    return Ok(Expr {
7138                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
7139                        line,
7140                    });
7141                }
7142                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
7143                let mut full_name = match self.advance() {
7144                    (Token::Ident(n), _) => n,
7145                    (Token::X, _) => "x".to_string(),
7146                    (tok, l) => {
7147                        return Err(self
7148                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
7149                    }
7150                };
7151                while self.eat(&Token::PackageSep) {
7152                    match self.advance() {
7153                        (Token::Ident(part), _) => {
7154                            full_name = format!("{}::{}", full_name, part);
7155                        }
7156                        (Token::X, _) => {
7157                            full_name = format!("{}::x", full_name);
7158                        }
7159                        (tok, l) => {
7160                            return Err(self.syntax_err(
7161                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
7162                                l,
7163                            ));
7164                        }
7165                    }
7166                }
7167                Ok(Expr {
7168                    kind: ExprKind::Typeglob(full_name),
7169                    line,
7170                })
7171            }
7172            Token::SingleString(s) => {
7173                self.advance();
7174                Ok(Expr {
7175                    kind: ExprKind::String(s),
7176                    line,
7177                })
7178            }
7179            Token::DoubleString(s) => {
7180                self.advance();
7181                self.parse_interpolated_string(&s, line)
7182            }
7183            Token::BacktickString(s) => {
7184                self.advance();
7185                let inner = self.parse_interpolated_string(&s, line)?;
7186                Ok(Expr {
7187                    kind: ExprKind::Qx(Box::new(inner)),
7188                    line,
7189                })
7190            }
7191            Token::HereDoc(_, body, interpolate) => {
7192                self.advance();
7193                if interpolate {
7194                    self.parse_interpolated_string(&body, line)
7195                } else {
7196                    Ok(Expr {
7197                        kind: ExprKind::String(body),
7198                        line,
7199                    })
7200                }
7201            }
7202            Token::Regex(pattern, flags, _delim) => {
7203                self.advance();
7204                Ok(Expr {
7205                    kind: ExprKind::Regex(pattern, flags),
7206                    line,
7207                })
7208            }
7209            Token::QW(words) => {
7210                self.advance();
7211                Ok(Expr {
7212                    kind: ExprKind::QW(words),
7213                    line,
7214                })
7215            }
7216            Token::DerefScalarVar(name) => {
7217                self.advance();
7218                Ok(Expr {
7219                    kind: ExprKind::Deref {
7220                        expr: Box::new(Expr {
7221                            kind: ExprKind::ScalarVar(name),
7222                            line,
7223                        }),
7224                        kind: Sigil::Scalar,
7225                    },
7226                    line,
7227                })
7228            }
7229            Token::ScalarVar(name) => {
7230                self.advance();
7231                Ok(Expr {
7232                    kind: ExprKind::ScalarVar(name),
7233                    line,
7234                })
7235            }
7236            Token::ArrayVar(name) => {
7237                self.advance();
7238                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
7239                match self.peek() {
7240                    Token::LBracket => {
7241                        self.advance();
7242                        let indices = self.parse_arg_list()?;
7243                        self.expect(&Token::RBracket)?;
7244                        Ok(Expr {
7245                            kind: ExprKind::ArraySlice {
7246                                array: name,
7247                                indices,
7248                            },
7249                            line,
7250                        })
7251                    }
7252                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
7253                        self.advance();
7254                        let keys = self.parse_arg_list()?;
7255                        self.expect(&Token::RBrace)?;
7256                        Ok(Expr {
7257                            kind: ExprKind::HashSlice { hash: name, keys },
7258                            line,
7259                        })
7260                    }
7261                    _ => Ok(Expr {
7262                        kind: ExprKind::ArrayVar(name),
7263                        line,
7264                    }),
7265                }
7266            }
7267            Token::HashVar(name) => {
7268                self.advance();
7269                Ok(Expr {
7270                    kind: ExprKind::HashVar(name),
7271                    line,
7272                })
7273            }
7274            Token::HashPercent => {
7275                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
7276                self.advance();
7277                if matches!(self.peek(), Token::ScalarVar(_)) {
7278                    let n = match self.advance() {
7279                        (Token::ScalarVar(n), _) => n,
7280                        (tok, l) => {
7281                            return Err(self.syntax_err(
7282                                format!("Expected scalar variable after %%, got {:?}", tok),
7283                                l,
7284                            ));
7285                        }
7286                    };
7287                    return Ok(Expr {
7288                        kind: ExprKind::Deref {
7289                            expr: Box::new(Expr {
7290                                kind: ExprKind::ScalarVar(n),
7291                                line,
7292                            }),
7293                            kind: Sigil::Hash,
7294                        },
7295                        line,
7296                    });
7297                }
7298                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
7299                // anonymous hashref inline, using `[...]` as the delimiter to avoid
7300                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
7301                // Real Perl errors on `%[...]` syntactically, so no compat risk.
7302                if matches!(self.peek(), Token::LBracket) {
7303                    self.advance();
7304                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7305                    self.expect(&Token::RBracket)?;
7306                    let href = Expr {
7307                        kind: ExprKind::HashRef(pairs),
7308                        line,
7309                    };
7310                    return Ok(Expr {
7311                        kind: ExprKind::Deref {
7312                            expr: Box::new(href),
7313                            kind: Sigil::Hash,
7314                        },
7315                        line,
7316                    });
7317                }
7318                self.expect(&Token::LBrace)?;
7319                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
7320                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
7321                // heuristic is famously unreliable — when the first non-whitespace
7322                // token is an ident/string followed by `=>`, treat the whole thing
7323                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
7324                let looks_like_pair = matches!(
7325                    self.peek(),
7326                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7327                ) && matches!(self.peek_at(1), Token::FatArrow);
7328                let inner = if looks_like_pair {
7329                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7330                    Expr {
7331                        kind: ExprKind::HashRef(pairs),
7332                        line,
7333                    }
7334                } else {
7335                    self.parse_expression()?
7336                };
7337                self.expect(&Token::RBrace)?;
7338                Ok(Expr {
7339                    kind: ExprKind::Deref {
7340                        expr: Box::new(inner),
7341                        kind: Sigil::Hash,
7342                    },
7343                    line,
7344                })
7345            }
7346            Token::ArrayAt => {
7347                self.advance();
7348                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
7349                if matches!(self.peek(), Token::LBrace) {
7350                    self.advance();
7351                    let inner = self.parse_expression()?;
7352                    self.expect(&Token::RBrace)?;
7353                    return Ok(Expr {
7354                        kind: ExprKind::Deref {
7355                            expr: Box::new(inner),
7356                            kind: Sigil::Array,
7357                        },
7358                        line,
7359                    });
7360                }
7361                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
7362                // anonymous arrayref inline. Real Perl rejects `@[...]` at
7363                // the parser level, so this extension has no compat risk.
7364                if matches!(self.peek(), Token::LBracket) {
7365                    self.advance();
7366                    let mut elems = Vec::new();
7367                    if !matches!(self.peek(), Token::RBracket) {
7368                        elems.push(self.parse_assign_expr()?);
7369                        while self.eat(&Token::Comma) {
7370                            if matches!(self.peek(), Token::RBracket) {
7371                                break;
7372                            }
7373                            elems.push(self.parse_assign_expr()?);
7374                        }
7375                    }
7376                    self.expect(&Token::RBracket)?;
7377                    let aref = Expr {
7378                        kind: ExprKind::ArrayRef(elems),
7379                        line,
7380                    };
7381                    return Ok(Expr {
7382                        kind: ExprKind::Deref {
7383                            expr: Box::new(aref),
7384                            kind: Sigil::Array,
7385                        },
7386                        line,
7387                    });
7388                }
7389                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
7390                let container = match self.peek().clone() {
7391                    Token::ScalarVar(n) => {
7392                        self.advance();
7393                        Expr {
7394                            kind: ExprKind::ScalarVar(n),
7395                            line,
7396                        }
7397                    }
7398                    _ => {
7399                        return Err(self.syntax_err(
7400                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
7401                            line,
7402                        ));
7403                    }
7404                };
7405                if matches!(self.peek(), Token::LBrace) {
7406                    self.advance();
7407                    let keys = self.parse_arg_list()?;
7408                    self.expect(&Token::RBrace)?;
7409                    return Ok(Expr {
7410                        kind: ExprKind::HashSliceDeref {
7411                            container: Box::new(container),
7412                            keys,
7413                        },
7414                        line,
7415                    });
7416                }
7417                Ok(Expr {
7418                    kind: ExprKind::Deref {
7419                        expr: Box::new(container),
7420                        kind: Sigil::Array,
7421                    },
7422                    line,
7423                })
7424            }
7425            Token::LParen => {
7426                self.advance();
7427                if matches!(self.peek(), Token::RParen) {
7428                    self.advance();
7429                    return Ok(Expr {
7430                        kind: ExprKind::List(vec![]),
7431                        line,
7432                    });
7433                }
7434                let expr = self.parse_expression()?;
7435                self.expect(&Token::RParen)?;
7436                Ok(expr)
7437            }
7438            Token::LBracket => {
7439                self.advance();
7440                let elems = self.parse_arg_list()?;
7441                self.expect(&Token::RBracket)?;
7442                Ok(Expr {
7443                    kind: ExprKind::ArrayRef(elems),
7444                    line,
7445                })
7446            }
7447            Token::LBrace => {
7448                // Could be hash ref or block — disambiguate
7449                self.advance();
7450                // Try to parse as hash ref: { key => val, ... }
7451                let saved = self.pos;
7452                match self.try_parse_hash_ref() {
7453                    Ok(pairs) => Ok(Expr {
7454                        kind: ExprKind::HashRef(pairs),
7455                        line,
7456                    }),
7457                    Err(_) => {
7458                        self.pos = saved;
7459                        // Parse as block, wrap in code ref
7460                        let mut stmts = Vec::new();
7461                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7462                            if self.eat(&Token::Semicolon) {
7463                                continue;
7464                            }
7465                            stmts.push(self.parse_statement()?);
7466                        }
7467                        self.expect(&Token::RBrace)?;
7468                        Ok(Expr {
7469                            kind: ExprKind::CodeRef {
7470                                params: vec![],
7471                                body: stmts,
7472                            },
7473                            line,
7474                        })
7475                    }
7476                }
7477            }
7478            Token::Diamond => {
7479                self.advance();
7480                Ok(Expr {
7481                    kind: ExprKind::ReadLine(None),
7482                    line,
7483                })
7484            }
7485            Token::ReadLine(handle) => {
7486                self.advance();
7487                Ok(Expr {
7488                    kind: ExprKind::ReadLine(Some(handle)),
7489                    line,
7490                })
7491            }
7492
7493            // Named functions / builtins
7494            Token::ThreadArrow => {
7495                self.advance();
7496                self.parse_thread_macro(line)
7497            }
7498            Token::Ident(ref name) => {
7499                let name = name.clone();
7500                // Handle s///
7501                if name.starts_with('\x00') {
7502                    self.advance();
7503                    let parts: Vec<&str> = name.split('\x00').collect();
7504                    if parts.len() >= 4 && parts[1] == "s" {
7505                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7506                        return Ok(Expr {
7507                            kind: ExprKind::Substitution {
7508                                expr: Box::new(Expr {
7509                                    kind: ExprKind::ScalarVar("_".into()),
7510                                    line,
7511                                }),
7512                                pattern: parts[2].to_string(),
7513                                replacement: parts[3].to_string(),
7514                                flags: parts.get(4).unwrap_or(&"").to_string(),
7515                                delim,
7516                            },
7517                            line,
7518                        });
7519                    }
7520                    if parts.len() >= 4 && parts[1] == "tr" {
7521                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7522                        return Ok(Expr {
7523                            kind: ExprKind::Transliterate {
7524                                expr: Box::new(Expr {
7525                                    kind: ExprKind::ScalarVar("_".into()),
7526                                    line,
7527                                }),
7528                                from: parts[2].to_string(),
7529                                to: parts[3].to_string(),
7530                                flags: parts.get(4).unwrap_or(&"").to_string(),
7531                                delim,
7532                            },
7533                            line,
7534                        });
7535                    }
7536                    return Err(self.syntax_err("Unexpected encoded token", line));
7537                }
7538                self.parse_named_expr(name)
7539            }
7540
7541            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
7542            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
7543            Token::Percent => {
7544                self.advance();
7545                match self.peek().clone() {
7546                    Token::Ident(name) => {
7547                        self.advance();
7548                        Ok(Expr {
7549                            kind: ExprKind::HashVar(name),
7550                            line,
7551                        })
7552                    }
7553                    Token::ScalarVar(n) => {
7554                        self.advance();
7555                        Ok(Expr {
7556                            kind: ExprKind::Deref {
7557                                expr: Box::new(Expr {
7558                                    kind: ExprKind::ScalarVar(n),
7559                                    line,
7560                                }),
7561                                kind: Sigil::Hash,
7562                            },
7563                            line,
7564                        })
7565                    }
7566                    Token::LBrace => {
7567                        self.advance();
7568                        let looks_like_pair = matches!(
7569                            self.peek(),
7570                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7571                        ) && matches!(self.peek_at(1), Token::FatArrow);
7572                        let inner = if looks_like_pair {
7573                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7574                            Expr {
7575                                kind: ExprKind::HashRef(pairs),
7576                                line,
7577                            }
7578                        } else {
7579                            self.parse_expression()?
7580                        };
7581                        self.expect(&Token::RBrace)?;
7582                        Ok(Expr {
7583                            kind: ExprKind::Deref {
7584                                expr: Box::new(inner),
7585                                kind: Sigil::Hash,
7586                            },
7587                            line,
7588                        })
7589                    }
7590                    Token::LBracket => {
7591                        self.advance();
7592                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7593                        self.expect(&Token::RBracket)?;
7594                        let href = Expr {
7595                            kind: ExprKind::HashRef(pairs),
7596                            line,
7597                        };
7598                        Ok(Expr {
7599                            kind: ExprKind::Deref {
7600                                expr: Box::new(href),
7601                                kind: Sigil::Hash,
7602                            },
7603                            line,
7604                        })
7605                    }
7606                    tok => Err(self.syntax_err(
7607                        format!(
7608                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
7609                            tok
7610                        ),
7611                        line,
7612                    )),
7613                }
7614            }
7615
7616            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
7617        }
7618    }
7619
7620    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
7621        let line = self.peek_line();
7622        self.advance(); // consume the ident
7623        while self.eat(&Token::PackageSep) {
7624            match self.advance() {
7625                (Token::Ident(part), _) => {
7626                    name = format!("{}::{}", name, part);
7627                }
7628                (tok, err_line) => {
7629                    return Err(self.syntax_err(
7630                        format!("Expected identifier after `::`, got {:?}", tok),
7631                        err_line,
7632                    ));
7633                }
7634            }
7635        }
7636
7637        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
7638        // before `=>` is treated as a string key, matching Perl 5 semantics.
7639        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
7640        if matches!(self.peek(), Token::FatArrow) {
7641            return Ok(Expr {
7642                kind: ExprKind::String(name),
7643                line,
7644            });
7645        }
7646
7647        if crate::compat_mode() {
7648            if let Some(ext) = Self::stryke_extension_name(&name) {
7649                if !self.declared_subs.contains(&name) {
7650                    return Err(self.syntax_err(
7651                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
7652                        line,
7653                    ));
7654                }
7655            }
7656        }
7657
7658        match name.as_str() {
7659            "__FILE__" => Ok(Expr {
7660                kind: ExprKind::MagicConst(MagicConstKind::File),
7661                line,
7662            }),
7663            "__LINE__" => Ok(Expr {
7664                kind: ExprKind::MagicConst(MagicConstKind::Line),
7665                line,
7666            }),
7667            "__SUB__" => Ok(Expr {
7668                kind: ExprKind::MagicConst(MagicConstKind::Sub),
7669                line,
7670            }),
7671            "stdin" => Ok(Expr {
7672                kind: ExprKind::FuncCall {
7673                    name: "stdin".into(),
7674                    args: vec![],
7675                },
7676                line,
7677            }),
7678            "range" => {
7679                let args = self.parse_builtin_args()?;
7680                Ok(Expr {
7681                    kind: ExprKind::FuncCall {
7682                        name: "range".into(),
7683                        args,
7684                    },
7685                    line,
7686                })
7687            }
7688            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
7689            "say" | "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
7690            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
7691            "die" => {
7692                let args = self.parse_list_until_terminator()?;
7693                Ok(Expr {
7694                    kind: ExprKind::Die(args),
7695                    line,
7696                })
7697            }
7698            "warn" => {
7699                let args = self.parse_list_until_terminator()?;
7700                Ok(Expr {
7701                    kind: ExprKind::Warn(args),
7702                    line,
7703                })
7704            }
7705            // `croak` / `confess` — `Carp` builtins available without `use Carp`
7706            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
7707            // `die` — TODO: croak should report caller's file/line, confess
7708            // should append a full stack trace.
7709            "croak" | "confess" => {
7710                let args = self.parse_list_until_terminator()?;
7711                Ok(Expr {
7712                    kind: ExprKind::Die(args),
7713                    line,
7714                })
7715            }
7716            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
7717            "carp" | "cluck" => {
7718                let args = self.parse_list_until_terminator()?;
7719                Ok(Expr {
7720                    kind: ExprKind::Warn(args),
7721                    line,
7722                })
7723            }
7724            "chomp" => {
7725                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7726                    return Ok(e);
7727                }
7728                let a = self.parse_one_arg_or_default()?;
7729                Ok(Expr {
7730                    kind: ExprKind::Chomp(Box::new(a)),
7731                    line,
7732                })
7733            }
7734            "chop" => {
7735                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7736                    return Ok(e);
7737                }
7738                let a = self.parse_one_arg_or_default()?;
7739                Ok(Expr {
7740                    kind: ExprKind::Chop(Box::new(a)),
7741                    line,
7742                })
7743            }
7744            "length" => {
7745                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7746                    return Ok(e);
7747                }
7748                let a = self.parse_one_arg_or_default()?;
7749                Ok(Expr {
7750                    kind: ExprKind::Length(Box::new(a)),
7751                    line,
7752                })
7753            }
7754            "defined" => {
7755                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7756                    return Ok(e);
7757                }
7758                let a = self.parse_one_arg_or_default()?;
7759                Ok(Expr {
7760                    kind: ExprKind::Defined(Box::new(a)),
7761                    line,
7762                })
7763            }
7764            "ref" => {
7765                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7766                    return Ok(e);
7767                }
7768                let a = self.parse_one_arg_or_default()?;
7769                Ok(Expr {
7770                    kind: ExprKind::Ref(Box::new(a)),
7771                    line,
7772                })
7773            }
7774            "undef" => {
7775                // `undef $var` sets `$var` to undef — but a variable on a new line
7776                // is a separate statement (implicit semicolon), not an argument.
7777                if self.peek_line() == self.prev_line()
7778                    && matches!(
7779                        self.peek(),
7780                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
7781                    )
7782                {
7783                    let target = self.parse_primary()?;
7784                    return Ok(Expr {
7785                        kind: ExprKind::Assign {
7786                            target: Box::new(target),
7787                            value: Box::new(Expr {
7788                                kind: ExprKind::Undef,
7789                                line,
7790                            }),
7791                        },
7792                        line,
7793                    });
7794                }
7795                Ok(Expr {
7796                    kind: ExprKind::Undef,
7797                    line,
7798                })
7799            }
7800            "scalar" => {
7801                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7802                    return Ok(e);
7803                }
7804                let a = self.parse_one_arg_or_default()?;
7805                Ok(Expr {
7806                    kind: ExprKind::ScalarContext(Box::new(a)),
7807                    line,
7808                })
7809            }
7810            "abs" => {
7811                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7812                    return Ok(e);
7813                }
7814                let a = self.parse_one_arg_or_default()?;
7815                Ok(Expr {
7816                    kind: ExprKind::Abs(Box::new(a)),
7817                    line,
7818                })
7819            }
7820            // stryke unary numeric extensions — treat like `abs` so a bare
7821            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
7822            // call with implicit `$_` rather than falling through to the
7823            // generic `Bareword` arm (which stringifies to `"inc"`).
7824            "inc" | "dec" => {
7825                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7826                    return Ok(e);
7827                }
7828                let a = self.parse_one_arg_or_default()?;
7829                Ok(Expr {
7830                    kind: ExprKind::FuncCall {
7831                        name,
7832                        args: vec![a],
7833                    },
7834                    line,
7835                })
7836            }
7837            "int" => {
7838                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7839                    return Ok(e);
7840                }
7841                let a = self.parse_one_arg_or_default()?;
7842                Ok(Expr {
7843                    kind: ExprKind::Int(Box::new(a)),
7844                    line,
7845                })
7846            }
7847            "sqrt" => {
7848                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7849                    return Ok(e);
7850                }
7851                let a = self.parse_one_arg_or_default()?;
7852                Ok(Expr {
7853                    kind: ExprKind::Sqrt(Box::new(a)),
7854                    line,
7855                })
7856            }
7857            "sin" => {
7858                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7859                    return Ok(e);
7860                }
7861                let a = self.parse_one_arg_or_default()?;
7862                Ok(Expr {
7863                    kind: ExprKind::Sin(Box::new(a)),
7864                    line,
7865                })
7866            }
7867            "cos" => {
7868                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7869                    return Ok(e);
7870                }
7871                let a = self.parse_one_arg_or_default()?;
7872                Ok(Expr {
7873                    kind: ExprKind::Cos(Box::new(a)),
7874                    line,
7875                })
7876            }
7877            "atan2" => {
7878                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7879                    return Ok(e);
7880                }
7881                let args = self.parse_builtin_args()?;
7882                if args.len() != 2 {
7883                    return Err(self.syntax_err("atan2 requires two arguments", line));
7884                }
7885                Ok(Expr {
7886                    kind: ExprKind::Atan2 {
7887                        y: Box::new(args[0].clone()),
7888                        x: Box::new(args[1].clone()),
7889                    },
7890                    line,
7891                })
7892            }
7893            "exp" => {
7894                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7895                    return Ok(e);
7896                }
7897                let a = self.parse_one_arg_or_default()?;
7898                Ok(Expr {
7899                    kind: ExprKind::Exp(Box::new(a)),
7900                    line,
7901                })
7902            }
7903            "log" => {
7904                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
7905                    return Ok(e);
7906                }
7907                let a = self.parse_one_arg_or_default()?;
7908                Ok(Expr {
7909                    kind: ExprKind::Log(Box::new(a)),
7910                    line,
7911                })
7912            }
7913            "input" => {
7914                let args = if matches!(
7915                    self.peek(),
7916                    Token::Semicolon
7917                        | Token::RBrace
7918                        | Token::RParen
7919                        | Token::Eof
7920                        | Token::Comma
7921                        | Token::PipeForward
7922                ) {
7923                    vec![]
7924                } else if matches!(self.peek(), Token::LParen) {
7925                    self.advance();
7926                    if matches!(self.peek(), Token::RParen) {
7927                        self.advance();
7928                        vec![]
7929                    } else {
7930                        let a = self.parse_expression()?;
7931                        self.expect(&Token::RParen)?;
7932                        vec![a]
7933                    }
7934                } else {
7935                    let a = self.parse_one_arg()?;
7936                    vec![a]
7937                };
7938                Ok(Expr {
7939                    kind: ExprKind::FuncCall {
7940                        name: "input".to_string(),
7941                        args,
7942                    },
7943                    line,
7944                })
7945            }
7946            "rand" => {
7947                if matches!(
7948                    self.peek(),
7949                    Token::Semicolon
7950                        | Token::RBrace
7951                        | Token::RParen
7952                        | Token::Eof
7953                        | Token::Comma
7954                        | Token::PipeForward
7955                ) {
7956                    Ok(Expr {
7957                        kind: ExprKind::Rand(None),
7958                        line,
7959                    })
7960                } else if matches!(self.peek(), Token::LParen) {
7961                    self.advance();
7962                    if matches!(self.peek(), Token::RParen) {
7963                        self.advance();
7964                        Ok(Expr {
7965                            kind: ExprKind::Rand(None),
7966                            line,
7967                        })
7968                    } else {
7969                        let a = self.parse_expression()?;
7970                        self.expect(&Token::RParen)?;
7971                        Ok(Expr {
7972                            kind: ExprKind::Rand(Some(Box::new(a))),
7973                            line,
7974                        })
7975                    }
7976                } else {
7977                    let a = self.parse_one_arg()?;
7978                    Ok(Expr {
7979                        kind: ExprKind::Rand(Some(Box::new(a))),
7980                        line,
7981                    })
7982                }
7983            }
7984            "srand" => {
7985                if matches!(
7986                    self.peek(),
7987                    Token::Semicolon
7988                        | Token::RBrace
7989                        | Token::RParen
7990                        | Token::Eof
7991                        | Token::Comma
7992                        | Token::PipeForward
7993                ) {
7994                    Ok(Expr {
7995                        kind: ExprKind::Srand(None),
7996                        line,
7997                    })
7998                } else if matches!(self.peek(), Token::LParen) {
7999                    self.advance();
8000                    if matches!(self.peek(), Token::RParen) {
8001                        self.advance();
8002                        Ok(Expr {
8003                            kind: ExprKind::Srand(None),
8004                            line,
8005                        })
8006                    } else {
8007                        let a = self.parse_expression()?;
8008                        self.expect(&Token::RParen)?;
8009                        Ok(Expr {
8010                            kind: ExprKind::Srand(Some(Box::new(a))),
8011                            line,
8012                        })
8013                    }
8014                } else {
8015                    let a = self.parse_one_arg()?;
8016                    Ok(Expr {
8017                        kind: ExprKind::Srand(Some(Box::new(a))),
8018                        line,
8019                    })
8020                }
8021            }
8022            "hex" => {
8023                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8024                    return Ok(e);
8025                }
8026                let a = self.parse_one_arg_or_default()?;
8027                Ok(Expr {
8028                    kind: ExprKind::Hex(Box::new(a)),
8029                    line,
8030                })
8031            }
8032            "oct" => {
8033                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8034                    return Ok(e);
8035                }
8036                let a = self.parse_one_arg_or_default()?;
8037                Ok(Expr {
8038                    kind: ExprKind::Oct(Box::new(a)),
8039                    line,
8040                })
8041            }
8042            "chr" => {
8043                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8044                    return Ok(e);
8045                }
8046                let a = self.parse_one_arg_or_default()?;
8047                Ok(Expr {
8048                    kind: ExprKind::Chr(Box::new(a)),
8049                    line,
8050                })
8051            }
8052            "ord" => {
8053                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8054                    return Ok(e);
8055                }
8056                let a = self.parse_one_arg_or_default()?;
8057                Ok(Expr {
8058                    kind: ExprKind::Ord(Box::new(a)),
8059                    line,
8060                })
8061            }
8062            "lc" => {
8063                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8064                    return Ok(e);
8065                }
8066                let a = self.parse_one_arg_or_default()?;
8067                Ok(Expr {
8068                    kind: ExprKind::Lc(Box::new(a)),
8069                    line,
8070                })
8071            }
8072            "uc" => {
8073                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8074                    return Ok(e);
8075                }
8076                let a = self.parse_one_arg_or_default()?;
8077                Ok(Expr {
8078                    kind: ExprKind::Uc(Box::new(a)),
8079                    line,
8080                })
8081            }
8082            "lcfirst" => {
8083                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8084                    return Ok(e);
8085                }
8086                let a = self.parse_one_arg_or_default()?;
8087                Ok(Expr {
8088                    kind: ExprKind::Lcfirst(Box::new(a)),
8089                    line,
8090                })
8091            }
8092            "ucfirst" => {
8093                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8094                    return Ok(e);
8095                }
8096                let a = self.parse_one_arg_or_default()?;
8097                Ok(Expr {
8098                    kind: ExprKind::Ucfirst(Box::new(a)),
8099                    line,
8100                })
8101            }
8102            "fc" => {
8103                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8104                    return Ok(e);
8105                }
8106                let a = self.parse_one_arg_or_default()?;
8107                Ok(Expr {
8108                    kind: ExprKind::Fc(Box::new(a)),
8109                    line,
8110                })
8111            }
8112            "crypt" => {
8113                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8114                    return Ok(e);
8115                }
8116                let args = self.parse_builtin_args()?;
8117                if args.len() != 2 {
8118                    return Err(self.syntax_err("crypt requires two arguments", line));
8119                }
8120                Ok(Expr {
8121                    kind: ExprKind::Crypt {
8122                        plaintext: Box::new(args[0].clone()),
8123                        salt: Box::new(args[1].clone()),
8124                    },
8125                    line,
8126                })
8127            }
8128            "pos" => {
8129                if matches!(
8130                    self.peek(),
8131                    Token::Semicolon
8132                        | Token::RBrace
8133                        | Token::RParen
8134                        | Token::Eof
8135                        | Token::Comma
8136                        | Token::PipeForward
8137                ) {
8138                    Ok(Expr {
8139                        kind: ExprKind::Pos(None),
8140                        line,
8141                    })
8142                } else if matches!(self.peek(), Token::Assign) {
8143                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
8144                    self.advance();
8145                    let rhs = self.parse_assign_expr()?;
8146                    Ok(Expr {
8147                        kind: ExprKind::Assign {
8148                            target: Box::new(Expr {
8149                                kind: ExprKind::Pos(Some(Box::new(Expr {
8150                                    kind: ExprKind::ScalarVar("_".into()),
8151                                    line,
8152                                }))),
8153                                line,
8154                            }),
8155                            value: Box::new(rhs),
8156                        },
8157                        line,
8158                    })
8159                } else if matches!(self.peek(), Token::LParen) {
8160                    self.advance();
8161                    if matches!(self.peek(), Token::RParen) {
8162                        self.advance();
8163                        Ok(Expr {
8164                            kind: ExprKind::Pos(None),
8165                            line,
8166                        })
8167                    } else {
8168                        let a = self.parse_expression()?;
8169                        self.expect(&Token::RParen)?;
8170                        Ok(Expr {
8171                            kind: ExprKind::Pos(Some(Box::new(a))),
8172                            line,
8173                        })
8174                    }
8175                } else {
8176                    let saved = self.pos;
8177                    let subj = self.parse_unary()?;
8178                    if matches!(self.peek(), Token::Assign) {
8179                        self.advance();
8180                        let rhs = self.parse_assign_expr()?;
8181                        Ok(Expr {
8182                            kind: ExprKind::Assign {
8183                                target: Box::new(Expr {
8184                                    kind: ExprKind::Pos(Some(Box::new(subj))),
8185                                    line,
8186                                }),
8187                                value: Box::new(rhs),
8188                            },
8189                            line,
8190                        })
8191                    } else {
8192                        self.pos = saved;
8193                        let a = self.parse_one_arg()?;
8194                        Ok(Expr {
8195                            kind: ExprKind::Pos(Some(Box::new(a))),
8196                            line,
8197                        })
8198                    }
8199                }
8200            }
8201            "study" => {
8202                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8203                    return Ok(e);
8204                }
8205                let a = self.parse_one_arg_or_default()?;
8206                Ok(Expr {
8207                    kind: ExprKind::Study(Box::new(a)),
8208                    line,
8209                })
8210            }
8211            "push" => {
8212                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8213                    return Ok(e);
8214                }
8215                let args = self.parse_builtin_args()?;
8216                let (first, rest) = args
8217                    .split_first()
8218                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
8219                Ok(Expr {
8220                    kind: ExprKind::Push {
8221                        array: Box::new(first.clone()),
8222                        values: rest.to_vec(),
8223                    },
8224                    line,
8225                })
8226            }
8227            "pop" => {
8228                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8229                    return Ok(e);
8230                }
8231                let a = self.parse_one_arg_or_argv()?;
8232                Ok(Expr {
8233                    kind: ExprKind::Pop(Box::new(a)),
8234                    line,
8235                })
8236            }
8237            "shift" => {
8238                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8239                    return Ok(e);
8240                }
8241                let a = self.parse_one_arg_or_argv()?;
8242                Ok(Expr {
8243                    kind: ExprKind::Shift(Box::new(a)),
8244                    line,
8245                })
8246            }
8247            "unshift" => {
8248                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8249                    return Ok(e);
8250                }
8251                let args = self.parse_builtin_args()?;
8252                let (first, rest) = args
8253                    .split_first()
8254                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
8255                Ok(Expr {
8256                    kind: ExprKind::Unshift {
8257                        array: Box::new(first.clone()),
8258                        values: rest.to_vec(),
8259                    },
8260                    line,
8261                })
8262            }
8263            "splice" => {
8264                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8265                    return Ok(e);
8266                }
8267                let args = self.parse_builtin_args()?;
8268                let mut iter = args.into_iter();
8269                let array = Box::new(
8270                    iter.next()
8271                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
8272                );
8273                let offset = iter.next().map(Box::new);
8274                let length = iter.next().map(Box::new);
8275                let replacement: Vec<Expr> = iter.collect();
8276                Ok(Expr {
8277                    kind: ExprKind::Splice {
8278                        array,
8279                        offset,
8280                        length,
8281                        replacement,
8282                    },
8283                    line,
8284                })
8285            }
8286            "delete" => {
8287                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8288                    return Ok(e);
8289                }
8290                let a = self.parse_postfix()?;
8291                Ok(Expr {
8292                    kind: ExprKind::Delete(Box::new(a)),
8293                    line,
8294                })
8295            }
8296            "exists" => {
8297                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8298                    return Ok(e);
8299                }
8300                let a = self.parse_postfix()?;
8301                Ok(Expr {
8302                    kind: ExprKind::Exists(Box::new(a)),
8303                    line,
8304                })
8305            }
8306            "keys" => {
8307                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8308                    return Ok(e);
8309                }
8310                let a = self.parse_one_arg_or_default()?;
8311                Ok(Expr {
8312                    kind: ExprKind::Keys(Box::new(a)),
8313                    line,
8314                })
8315            }
8316            "values" => {
8317                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8318                    return Ok(e);
8319                }
8320                let a = self.parse_one_arg_or_default()?;
8321                Ok(Expr {
8322                    kind: ExprKind::Values(Box::new(a)),
8323                    line,
8324                })
8325            }
8326            "each" => {
8327                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8328                    return Ok(e);
8329                }
8330                let a = self.parse_one_arg_or_default()?;
8331                Ok(Expr {
8332                    kind: ExprKind::Each(Box::new(a)),
8333                    line,
8334                })
8335            }
8336            "fore" | "e" | "ep" => {
8337                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
8338                if matches!(self.peek(), Token::LBrace) {
8339                    let (block, list) = self.parse_block_list()?;
8340                    Ok(Expr {
8341                        kind: ExprKind::ForEachExpr {
8342                            block,
8343                            list: Box::new(list),
8344                        },
8345                        line,
8346                    })
8347                } else if self.in_pipe_rhs() {
8348                    // `|> ep` — bare ep at end of pipe: default to `say $_`
8349                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
8350                    let is_terminal = matches!(
8351                        self.peek(),
8352                        Token::Semicolon
8353                            | Token::RParen
8354                            | Token::Eof
8355                            | Token::PipeForward
8356                            | Token::RBrace
8357                    );
8358                    let block = if name == "ep" && is_terminal {
8359                        vec![Statement {
8360                            label: None,
8361                            kind: StmtKind::Expression(Expr {
8362                                kind: ExprKind::Say {
8363                                    handle: None,
8364                                    args: vec![Expr {
8365                                        kind: ExprKind::ScalarVar("_".into()),
8366                                        line,
8367                                    }],
8368                                },
8369                                line,
8370                            }),
8371                            line,
8372                        }]
8373                    } else {
8374                        let expr = self.parse_assign_expr_stop_at_pipe()?;
8375                        let expr = Self::lift_bareword_to_topic_call(expr);
8376                        vec![Statement {
8377                            label: None,
8378                            kind: StmtKind::Expression(expr),
8379                            line,
8380                        }]
8381                    };
8382                    let list = self.pipe_placeholder_list(line);
8383                    Ok(Expr {
8384                        kind: ExprKind::ForEachExpr {
8385                            block,
8386                            list: Box::new(list),
8387                        },
8388                        line,
8389                    })
8390                } else {
8391                    // `fore EXPR, LIST` — comma form
8392                    let expr = self.parse_assign_expr()?;
8393                    let expr = Self::lift_bareword_to_topic_call(expr);
8394                    self.expect(&Token::Comma)?;
8395                    let list_parts = self.parse_list_until_terminator()?;
8396                    let list_expr = if list_parts.len() == 1 {
8397                        list_parts.into_iter().next().unwrap()
8398                    } else {
8399                        Expr {
8400                            kind: ExprKind::List(list_parts),
8401                            line,
8402                        }
8403                    };
8404                    let block = vec![Statement {
8405                        label: None,
8406                        kind: StmtKind::Expression(expr),
8407                        line,
8408                    }];
8409                    Ok(Expr {
8410                        kind: ExprKind::ForEachExpr {
8411                            block,
8412                            list: Box::new(list_expr),
8413                        },
8414                        line,
8415                    })
8416                }
8417            }
8418            "rev" => {
8419                // `rev` — context-aware reverse: string in scalar, list in list context.
8420                // Defaults to $_ when no argument given.
8421                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
8422                // RBrace means we're inside a block like `map { rev }` - use $_ default.
8423                let a = if self.in_pipe_rhs()
8424                    && matches!(
8425                        self.peek(),
8426                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
8427                    ) {
8428                    self.pipe_placeholder_list(line)
8429                } else {
8430                    self.parse_one_arg_or_default()?
8431                };
8432                Ok(Expr {
8433                    kind: ExprKind::ScalarReverse(Box::new(a)),
8434                    line,
8435                })
8436            }
8437            "reverse" | "reversed" => {
8438                // On the RHS of `|>`, the operand is supplied by the piped LHS.
8439                let a = if self.in_pipe_rhs()
8440                    && matches!(
8441                        self.peek(),
8442                        Token::Semicolon
8443                            | Token::RBrace
8444                            | Token::RParen
8445                            | Token::Eof
8446                            | Token::PipeForward
8447                    ) {
8448                    self.pipe_placeholder_list(line)
8449                } else {
8450                    self.parse_one_arg()?
8451                };
8452                Ok(Expr {
8453                    kind: ExprKind::ReverseExpr(Box::new(a)),
8454                    line,
8455                })
8456            }
8457            "join" => {
8458                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8459                    return Ok(e);
8460                }
8461                let args = self.parse_builtin_args()?;
8462                if args.is_empty() {
8463                    return Err(self.syntax_err("join requires separator and list", line));
8464                }
8465                // `@list |> join(",")` — list slot is filled by the piped LHS.
8466                if args.len() < 2 && !self.in_pipe_rhs() {
8467                    return Err(self.syntax_err("join requires separator and list", line));
8468                }
8469                Ok(Expr {
8470                    kind: ExprKind::JoinExpr {
8471                        separator: Box::new(args[0].clone()),
8472                        list: Box::new(Expr {
8473                            kind: ExprKind::List(args[1..].to_vec()),
8474                            line,
8475                        }),
8476                    },
8477                    line,
8478                })
8479            }
8480            "split" => {
8481                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8482                    return Ok(e);
8483                }
8484                let args = self.parse_builtin_args()?;
8485                let pattern = args.first().cloned().unwrap_or(Expr {
8486                    kind: ExprKind::String(" ".into()),
8487                    line,
8488                });
8489                let string = args.get(1).cloned().unwrap_or(Expr {
8490                    kind: ExprKind::ScalarVar("_".into()),
8491                    line,
8492                });
8493                let limit = args.get(2).cloned().map(Box::new);
8494                Ok(Expr {
8495                    kind: ExprKind::SplitExpr {
8496                        pattern: Box::new(pattern),
8497                        string: Box::new(string),
8498                        limit,
8499                    },
8500                    line,
8501                })
8502            }
8503            "substr" => {
8504                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8505                    return Ok(e);
8506                }
8507                let args = self.parse_builtin_args()?;
8508                Ok(Expr {
8509                    kind: ExprKind::Substr {
8510                        string: Box::new(args[0].clone()),
8511                        offset: Box::new(args[1].clone()),
8512                        length: args.get(2).cloned().map(Box::new),
8513                        replacement: args.get(3).cloned().map(Box::new),
8514                    },
8515                    line,
8516                })
8517            }
8518            "index" => {
8519                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8520                    return Ok(e);
8521                }
8522                let args = self.parse_builtin_args()?;
8523                Ok(Expr {
8524                    kind: ExprKind::Index {
8525                        string: Box::new(args[0].clone()),
8526                        substr: Box::new(args[1].clone()),
8527                        position: args.get(2).cloned().map(Box::new),
8528                    },
8529                    line,
8530                })
8531            }
8532            "rindex" => {
8533                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8534                    return Ok(e);
8535                }
8536                let args = self.parse_builtin_args()?;
8537                Ok(Expr {
8538                    kind: ExprKind::Rindex {
8539                        string: Box::new(args[0].clone()),
8540                        substr: Box::new(args[1].clone()),
8541                        position: args.get(2).cloned().map(Box::new),
8542                    },
8543                    line,
8544                })
8545            }
8546            "sprintf" => {
8547                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8548                    return Ok(e);
8549                }
8550                let args = self.parse_builtin_args()?;
8551                let (first, rest) = args
8552                    .split_first()
8553                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
8554                Ok(Expr {
8555                    kind: ExprKind::Sprintf {
8556                        format: Box::new(first.clone()),
8557                        args: rest.to_vec(),
8558                    },
8559                    line,
8560                })
8561            }
8562            "map" | "flat_map" | "maps" | "flat_maps" => {
8563                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
8564                let stream = matches!(name.as_str(), "maps" | "flat_maps");
8565                if matches!(self.peek(), Token::LBrace) {
8566                    let (block, list) = self.parse_block_list()?;
8567                    Ok(Expr {
8568                        kind: ExprKind::MapExpr {
8569                            block,
8570                            list: Box::new(list),
8571                            flatten_array_refs,
8572                            stream,
8573                        },
8574                        line,
8575                    })
8576                } else {
8577                    let expr = self.parse_assign_expr_stop_at_pipe()?;
8578                    // Lift bareword to FuncCall($_) so `map sha512, @list`
8579                    // calls sha512($_) for each element instead of stringifying.
8580                    let expr = Self::lift_bareword_to_topic_call(expr);
8581                    let list_expr = if self.in_pipe_rhs()
8582                        && matches!(
8583                            self.peek(),
8584                            Token::Semicolon
8585                                | Token::RBrace
8586                                | Token::RParen
8587                                | Token::Eof
8588                                | Token::PipeForward
8589                        ) {
8590                        self.pipe_placeholder_list(line)
8591                    } else {
8592                        self.expect(&Token::Comma)?;
8593                        let list_parts = self.parse_list_until_terminator()?;
8594                        if list_parts.len() == 1 {
8595                            list_parts.into_iter().next().unwrap()
8596                        } else {
8597                            Expr {
8598                                kind: ExprKind::List(list_parts),
8599                                line,
8600                            }
8601                        }
8602                    };
8603                    Ok(Expr {
8604                        kind: ExprKind::MapExprComma {
8605                            expr: Box::new(expr),
8606                            list: Box::new(list_expr),
8607                            flatten_array_refs,
8608                            stream,
8609                        },
8610                        line,
8611                    })
8612                }
8613            }
8614            "match" => {
8615                if crate::compat_mode() {
8616                    return Err(self.syntax_err(
8617                        "algebraic `match` is a stryke extension (disabled by --compat)",
8618                        line,
8619                    ));
8620                }
8621                self.parse_algebraic_match_expr(line)
8622            }
8623            "grep" | "greps" | "filter" | "f" | "find_all" => {
8624                let keyword = match name.as_str() {
8625                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
8626                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
8627                    "filter" | "f" => crate::ast::GrepBuiltinKeyword::Filter,
8628                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
8629                    _ => unreachable!(),
8630                };
8631                if matches!(self.peek(), Token::LBrace) {
8632                    let (block, list) = self.parse_block_list()?;
8633                    Ok(Expr {
8634                        kind: ExprKind::GrepExpr {
8635                            block,
8636                            list: Box::new(list),
8637                            keyword,
8638                        },
8639                        line,
8640                    })
8641                } else {
8642                    let expr = self.parse_assign_expr_stop_at_pipe()?;
8643                    if self.in_pipe_rhs()
8644                        && matches!(
8645                            self.peek(),
8646                            Token::Semicolon
8647                                | Token::RBrace
8648                                | Token::RParen
8649                                | Token::Eof
8650                                | Token::PipeForward
8651                        )
8652                    {
8653                        // Pipe-RHS blockless form: `|> grep EXPR`
8654                        // For literals, desugar to `$_ eq/== EXPR` so
8655                        // `|> filter 't'` keeps only elements equal to 't'.
8656                        // For regexes, desugar to `$_ =~ EXPR`.
8657                        let list = self.pipe_placeholder_list(line);
8658                        let topic = Expr {
8659                            kind: ExprKind::ScalarVar("_".into()),
8660                            line,
8661                        };
8662                        let test = match &expr.kind {
8663                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
8664                                kind: ExprKind::BinOp {
8665                                    op: BinOp::NumEq,
8666                                    left: Box::new(topic),
8667                                    right: Box::new(expr),
8668                                },
8669                                line,
8670                            },
8671                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
8672                                kind: ExprKind::BinOp {
8673                                    op: BinOp::StrEq,
8674                                    left: Box::new(topic),
8675                                    right: Box::new(expr),
8676                                },
8677                                line,
8678                            },
8679                            ExprKind::Regex { .. } => Expr {
8680                                kind: ExprKind::BinOp {
8681                                    op: BinOp::BindMatch,
8682                                    left: Box::new(topic),
8683                                    right: Box::new(expr),
8684                                },
8685                                line,
8686                            },
8687                            _ => {
8688                                // Non-literal (e.g. `defined`): lift bareword to call
8689                                Self::lift_bareword_to_topic_call(expr)
8690                            }
8691                        };
8692                        let block = vec![Statement {
8693                            label: None,
8694                            kind: StmtKind::Expression(test),
8695                            line,
8696                        }];
8697                        Ok(Expr {
8698                            kind: ExprKind::GrepExpr {
8699                                block,
8700                                list: Box::new(list),
8701                                keyword,
8702                            },
8703                            line,
8704                        })
8705                    } else {
8706                        let expr = Self::lift_bareword_to_topic_call(expr);
8707                        self.expect(&Token::Comma)?;
8708                        let list_parts = self.parse_list_until_terminator()?;
8709                        let list_expr = if list_parts.len() == 1 {
8710                            list_parts.into_iter().next().unwrap()
8711                        } else {
8712                            Expr {
8713                                kind: ExprKind::List(list_parts),
8714                                line,
8715                            }
8716                        };
8717                        Ok(Expr {
8718                            kind: ExprKind::GrepExprComma {
8719                                expr: Box::new(expr),
8720                                list: Box::new(list_expr),
8721                                keyword,
8722                            },
8723                            line,
8724                        })
8725                    }
8726                }
8727            }
8728            "sort" => {
8729                use crate::ast::SortComparator;
8730                if matches!(self.peek(), Token::LBrace) {
8731                    let block = self.parse_block()?;
8732                    let _ = self.eat(&Token::Comma);
8733                    let list = if self.in_pipe_rhs()
8734                        && matches!(
8735                            self.peek(),
8736                            Token::Semicolon
8737                                | Token::RBrace
8738                                | Token::RParen
8739                                | Token::Eof
8740                                | Token::PipeForward
8741                        ) {
8742                        self.pipe_placeholder_list(line)
8743                    } else {
8744                        self.parse_expression()?
8745                    };
8746                    Ok(Expr {
8747                        kind: ExprKind::SortExpr {
8748                            cmp: Some(SortComparator::Block(block)),
8749                            list: Box::new(list),
8750                        },
8751                        line,
8752                    })
8753                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
8754                    // Blockless comparator: `sort $a <=> $b, @list`
8755                    let block = self.parse_block_or_bareword_cmp_block()?;
8756                    let _ = self.eat(&Token::Comma);
8757                    let list = if self.in_pipe_rhs()
8758                        && matches!(
8759                            self.peek(),
8760                            Token::Semicolon
8761                                | Token::RBrace
8762                                | Token::RParen
8763                                | Token::Eof
8764                                | Token::PipeForward
8765                        ) {
8766                        self.pipe_placeholder_list(line)
8767                    } else {
8768                        self.parse_expression()?
8769                    };
8770                    Ok(Expr {
8771                        kind: ExprKind::SortExpr {
8772                            cmp: Some(SortComparator::Block(block)),
8773                            list: Box::new(list),
8774                        },
8775                        line,
8776                    })
8777                } else if matches!(self.peek(), Token::ScalarVar(_)) {
8778                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized
8779                    self.suppress_indirect_paren_call =
8780                        self.suppress_indirect_paren_call.saturating_add(1);
8781                    let code = self.parse_assign_expr()?;
8782                    self.suppress_indirect_paren_call =
8783                        self.suppress_indirect_paren_call.saturating_sub(1);
8784                    let list = if matches!(self.peek(), Token::LParen) {
8785                        self.advance();
8786                        let e = self.parse_expression()?;
8787                        self.expect(&Token::RParen)?;
8788                        e
8789                    } else {
8790                        self.parse_expression()?
8791                    };
8792                    Ok(Expr {
8793                        kind: ExprKind::SortExpr {
8794                            cmp: Some(SortComparator::Code(Box::new(code))),
8795                            list: Box::new(list),
8796                        },
8797                        line,
8798                    })
8799                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
8800                {
8801                    // Blockless comparator via bare sub name: `sort my_cmp @list`
8802                    let block = self.parse_block_or_bareword_cmp_block()?;
8803                    let _ = self.eat(&Token::Comma);
8804                    let list = if self.in_pipe_rhs()
8805                        && matches!(
8806                            self.peek(),
8807                            Token::Semicolon
8808                                | Token::RBrace
8809                                | Token::RParen
8810                                | Token::Eof
8811                                | Token::PipeForward
8812                        ) {
8813                        self.pipe_placeholder_list(line)
8814                    } else {
8815                        self.parse_expression()?
8816                    };
8817                    Ok(Expr {
8818                        kind: ExprKind::SortExpr {
8819                            cmp: Some(SortComparator::Block(block)),
8820                            list: Box::new(list),
8821                        },
8822                        line,
8823                    })
8824                } else {
8825                    // Bare `sort` with no comparator and no list: only allowed
8826                    // as the RHS of `|>`, where the list comes from the LHS.
8827                    let list = if self.in_pipe_rhs()
8828                        && matches!(
8829                            self.peek(),
8830                            Token::Semicolon
8831                                | Token::RBrace
8832                                | Token::RParen
8833                                | Token::Eof
8834                                | Token::PipeForward
8835                        ) {
8836                        self.pipe_placeholder_list(line)
8837                    } else {
8838                        self.parse_expression()?
8839                    };
8840                    Ok(Expr {
8841                        kind: ExprKind::SortExpr {
8842                            cmp: None,
8843                            list: Box::new(list),
8844                        },
8845                        line,
8846                    })
8847                }
8848            }
8849            "reduce" | "fold" | "inject" => {
8850                let (block, list) = self.parse_block_list()?;
8851                Ok(Expr {
8852                    kind: ExprKind::ReduceExpr {
8853                        block,
8854                        list: Box::new(list),
8855                    },
8856                    line,
8857                })
8858            }
8859            // Parallel extensions
8860            "pmap" => {
8861                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8862                Ok(Expr {
8863                    kind: ExprKind::PMapExpr {
8864                        block,
8865                        list: Box::new(list),
8866                        progress: progress.map(Box::new),
8867                        flat_outputs: false,
8868                        on_cluster: None,
8869                        stream: false,
8870                    },
8871                    line,
8872                })
8873            }
8874            "pmap_on" => {
8875                let (cluster, block, list, progress) =
8876                    self.parse_cluster_block_then_list_optional_progress()?;
8877                Ok(Expr {
8878                    kind: ExprKind::PMapExpr {
8879                        block,
8880                        list: Box::new(list),
8881                        progress: progress.map(Box::new),
8882                        flat_outputs: false,
8883                        on_cluster: Some(Box::new(cluster)),
8884                        stream: false,
8885                    },
8886                    line,
8887                })
8888            }
8889            "pflat_map" => {
8890                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8891                Ok(Expr {
8892                    kind: ExprKind::PMapExpr {
8893                        block,
8894                        list: Box::new(list),
8895                        progress: progress.map(Box::new),
8896                        flat_outputs: true,
8897                        on_cluster: None,
8898                        stream: false,
8899                    },
8900                    line,
8901                })
8902            }
8903            "pflat_map_on" => {
8904                let (cluster, block, list, progress) =
8905                    self.parse_cluster_block_then_list_optional_progress()?;
8906                Ok(Expr {
8907                    kind: ExprKind::PMapExpr {
8908                        block,
8909                        list: Box::new(list),
8910                        progress: progress.map(Box::new),
8911                        flat_outputs: true,
8912                        on_cluster: Some(Box::new(cluster)),
8913                        stream: false,
8914                    },
8915                    line,
8916                })
8917            }
8918            "pmaps" => {
8919                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8920                Ok(Expr {
8921                    kind: ExprKind::PMapExpr {
8922                        block,
8923                        list: Box::new(list),
8924                        progress: progress.map(Box::new),
8925                        flat_outputs: false,
8926                        on_cluster: None,
8927                        stream: true,
8928                    },
8929                    line,
8930                })
8931            }
8932            "pflat_maps" => {
8933                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8934                Ok(Expr {
8935                    kind: ExprKind::PMapExpr {
8936                        block,
8937                        list: Box::new(list),
8938                        progress: progress.map(Box::new),
8939                        flat_outputs: true,
8940                        on_cluster: None,
8941                        stream: true,
8942                    },
8943                    line,
8944                })
8945            }
8946            "pgreps" => {
8947                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8948                Ok(Expr {
8949                    kind: ExprKind::PGrepExpr {
8950                        block,
8951                        list: Box::new(list),
8952                        progress: progress.map(Box::new),
8953                        stream: true,
8954                    },
8955                    line,
8956                })
8957            }
8958            "pmap_chunked" => {
8959                let chunk_size = self.parse_assign_expr()?;
8960                let block = self.parse_block_or_bareword_block()?;
8961                self.eat(&Token::Comma);
8962                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
8963                Ok(Expr {
8964                    kind: ExprKind::PMapChunkedExpr {
8965                        chunk_size: Box::new(chunk_size),
8966                        block,
8967                        list: Box::new(list),
8968                        progress: progress.map(Box::new),
8969                    },
8970                    line,
8971                })
8972            }
8973            "pgrep" => {
8974                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8975                Ok(Expr {
8976                    kind: ExprKind::PGrepExpr {
8977                        block,
8978                        list: Box::new(list),
8979                        progress: progress.map(Box::new),
8980                        stream: false,
8981                    },
8982                    line,
8983                })
8984            }
8985            "pfor" => {
8986                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
8987                Ok(Expr {
8988                    kind: ExprKind::PForExpr {
8989                        block,
8990                        list: Box::new(list),
8991                        progress: progress.map(Box::new),
8992                    },
8993                    line,
8994                })
8995            }
8996            "par_lines" | "par_walk" => {
8997                let args = self.parse_builtin_args()?;
8998                if args.len() < 2 {
8999                    return Err(
9000                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9001                    );
9002                }
9003
9004                if name == "par_lines" {
9005                    Ok(Expr {
9006                        kind: ExprKind::ParLinesExpr {
9007                            path: Box::new(args[0].clone()),
9008                            callback: Box::new(args[1].clone()),
9009                            progress: None,
9010                        },
9011                        line,
9012                    })
9013                } else {
9014                    Ok(Expr {
9015                        kind: ExprKind::ParWalkExpr {
9016                            path: Box::new(args[0].clone()),
9017                            callback: Box::new(args[1].clone()),
9018                            progress: None,
9019                        },
9020                        line,
9021                    })
9022                }
9023            }
9024            "pwatch" | "watch" => {
9025                let args = self.parse_builtin_args()?;
9026                if args.len() < 2 {
9027                    return Err(
9028                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9029                    );
9030                }
9031                Ok(Expr {
9032                    kind: ExprKind::PwatchExpr {
9033                        path: Box::new(args[0].clone()),
9034                        callback: Box::new(args[1].clone()),
9035                    },
9036                    line,
9037                })
9038            }
9039            "fan" => {
9040                // fan { BLOCK }            — no count, block body
9041                // fan COUNT { BLOCK }      — count + block body
9042                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
9043                // fan COUNT EXPR;          — count + blockless body
9044                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
9045                let (count, block) = self.parse_fan_count_and_block(line)?;
9046                let progress = self.parse_fan_optional_progress("fan")?;
9047                Ok(Expr {
9048                    kind: ExprKind::FanExpr {
9049                        count,
9050                        block,
9051                        progress,
9052                        capture: false,
9053                    },
9054                    line,
9055                })
9056            }
9057            "fan_cap" => {
9058                let (count, block) = self.parse_fan_count_and_block(line)?;
9059                let progress = self.parse_fan_optional_progress("fan_cap")?;
9060                Ok(Expr {
9061                    kind: ExprKind::FanExpr {
9062                        count,
9063                        block,
9064                        progress,
9065                        capture: true,
9066                    },
9067                    line,
9068                })
9069            }
9070            "async" => {
9071                if !matches!(self.peek(), Token::LBrace) {
9072                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
9073                }
9074                let block = self.parse_block()?;
9075                Ok(Expr {
9076                    kind: ExprKind::AsyncBlock { body: block },
9077                    line,
9078                })
9079            }
9080            "spawn" => {
9081                if !matches!(self.peek(), Token::LBrace) {
9082                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
9083                }
9084                let block = self.parse_block()?;
9085                Ok(Expr {
9086                    kind: ExprKind::SpawnBlock { body: block },
9087                    line,
9088                })
9089            }
9090            "trace" => {
9091                if !matches!(self.peek(), Token::LBrace) {
9092                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
9093                }
9094                let block = self.parse_block()?;
9095                Ok(Expr {
9096                    kind: ExprKind::Trace { body: block },
9097                    line,
9098                })
9099            }
9100            "timer" => {
9101                let block = self.parse_block_or_bareword_block_no_args()?;
9102                Ok(Expr {
9103                    kind: ExprKind::Timer { body: block },
9104                    line,
9105                })
9106            }
9107            "bench" => {
9108                let block = self.parse_block_or_bareword_block_no_args()?;
9109                let times = Box::new(self.parse_expression()?);
9110                Ok(Expr {
9111                    kind: ExprKind::Bench { body: block, times },
9112                    line,
9113                })
9114            }
9115            "spinner" => {
9116                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
9117                let (message, body) = if matches!(self.peek(), Token::LBrace) {
9118                    let body = self.parse_block()?;
9119                    (
9120                        Box::new(Expr {
9121                            kind: ExprKind::String("working".to_string()),
9122                            line,
9123                        }),
9124                        body,
9125                    )
9126                } else {
9127                    let msg = self.parse_assign_expr()?;
9128                    let body = self.parse_block()?;
9129                    (Box::new(msg), body)
9130                };
9131                Ok(Expr {
9132                    kind: ExprKind::Spinner { message, body },
9133                    line,
9134                })
9135            }
9136            "thread" | "t" => {
9137                // `thread EXPR stage1 stage2 ...` — threading macro (like Clojure's ->>)
9138                // `t` is a short alias for `thread`
9139                // Each stage is either:
9140                //   - `ident` — bare function call
9141                //   - `ident { block }` — function with block arg
9142                //   - `ident arg1 arg2 { block }` — function with args and optional block
9143                //   - `sub { block }` — standalone anonymous block
9144                //   - `>{ block }` — shorthand for standalone anonymous block
9145                // Desugars to: EXPR |> stage1 |> stage2 |> ...
9146                self.parse_thread_macro(line)
9147            }
9148            "retry" => {
9149                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
9150                // An optional comma before `times` is allowed in both forms.
9151                let body = if matches!(self.peek(), Token::LBrace) {
9152                    self.parse_block()?
9153                } else {
9154                    let bw_line = self.peek_line();
9155                    let Token::Ident(ref name) = self.peek().clone() else {
9156                        return Err(self
9157                            .syntax_err("retry: expected block or bareword function name", line));
9158                    };
9159                    let name = name.clone();
9160                    self.advance();
9161                    vec![Statement::new(
9162                        StmtKind::Expression(Expr {
9163                            kind: ExprKind::FuncCall { name, args: vec![] },
9164                            line: bw_line,
9165                        }),
9166                        bw_line,
9167                    )]
9168                };
9169                self.eat(&Token::Comma);
9170                match self.peek() {
9171                    Token::Ident(ref s) if s == "times" => {
9172                        self.advance();
9173                    }
9174                    _ => {
9175                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
9176                    }
9177                }
9178                self.expect(&Token::FatArrow)?;
9179                let times = Box::new(self.parse_assign_expr()?);
9180                let mut backoff = RetryBackoff::None;
9181                if self.eat(&Token::Comma) {
9182                    match self.peek() {
9183                        Token::Ident(ref s) if s == "backoff" => {
9184                            self.advance();
9185                        }
9186                        _ => {
9187                            return Err(
9188                                self.syntax_err("retry: expected `backoff =>` after comma", line)
9189                            );
9190                        }
9191                    }
9192                    self.expect(&Token::FatArrow)?;
9193                    let Token::Ident(mode) = self.peek().clone() else {
9194                        return Err(self.syntax_err(
9195                            "retry: expected backoff mode (none, linear, exponential)",
9196                            line,
9197                        ));
9198                    };
9199                    backoff = match mode.as_str() {
9200                        "none" => RetryBackoff::None,
9201                        "linear" => RetryBackoff::Linear,
9202                        "exponential" => RetryBackoff::Exponential,
9203                        _ => {
9204                            return Err(
9205                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
9206                            );
9207                        }
9208                    };
9209                    self.advance();
9210                }
9211                Ok(Expr {
9212                    kind: ExprKind::RetryBlock {
9213                        body,
9214                        times,
9215                        backoff,
9216                    },
9217                    line,
9218                })
9219            }
9220            "rate_limit" => {
9221                self.expect(&Token::LParen)?;
9222                let max = Box::new(self.parse_assign_expr()?);
9223                self.expect(&Token::Comma)?;
9224                let window = Box::new(self.parse_assign_expr()?);
9225                self.expect(&Token::RParen)?;
9226                let body = self.parse_block_or_bareword_block_no_args()?;
9227                let slot = self.alloc_rate_limit_slot();
9228                Ok(Expr {
9229                    kind: ExprKind::RateLimitBlock {
9230                        slot,
9231                        max,
9232                        window,
9233                        body,
9234                    },
9235                    line,
9236                })
9237            }
9238            "every" => {
9239                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
9240                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
9241                let has_paren = self.eat(&Token::LParen);
9242                let interval = Box::new(self.parse_assign_expr()?);
9243                if has_paren {
9244                    self.expect(&Token::RParen)?;
9245                }
9246                let body = if matches!(self.peek(), Token::LBrace) {
9247                    self.parse_block()?
9248                } else {
9249                    let bline = self.peek_line();
9250                    let expr = self.parse_assign_expr()?;
9251                    vec![Statement::new(StmtKind::Expression(expr), bline)]
9252                };
9253                Ok(Expr {
9254                    kind: ExprKind::EveryBlock { interval, body },
9255                    line,
9256                })
9257            }
9258            "gen" => {
9259                if !matches!(self.peek(), Token::LBrace) {
9260                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
9261                }
9262                let body = self.parse_block()?;
9263                Ok(Expr {
9264                    kind: ExprKind::GenBlock { body },
9265                    line,
9266                })
9267            }
9268            "yield" => {
9269                let e = self.parse_assign_expr()?;
9270                Ok(Expr {
9271                    kind: ExprKind::Yield(Box::new(e)),
9272                    line,
9273                })
9274            }
9275            "await" => {
9276                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9277                    return Ok(e);
9278                }
9279                // `await` defaults to `$_` so `map { await } @tasks` works
9280                // (Perl-style topic-defaulting unary).
9281                let a = self.parse_one_arg_or_default()?;
9282                Ok(Expr {
9283                    kind: ExprKind::Await(Box::new(a)),
9284                    line,
9285                })
9286            }
9287            "slurp" | "cat" | "c" => {
9288                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9289                    return Ok(e);
9290                }
9291                let a = self.parse_one_arg_or_default()?;
9292                Ok(Expr {
9293                    kind: ExprKind::Slurp(Box::new(a)),
9294                    line,
9295                })
9296            }
9297            "capture" => {
9298                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9299                    return Ok(e);
9300                }
9301                let a = self.parse_one_arg()?;
9302                Ok(Expr {
9303                    kind: ExprKind::Capture(Box::new(a)),
9304                    line,
9305                })
9306            }
9307            "fetch_url" => {
9308                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9309                    return Ok(e);
9310                }
9311                let a = self.parse_one_arg()?;
9312                Ok(Expr {
9313                    kind: ExprKind::FetchUrl(Box::new(a)),
9314                    line,
9315                })
9316            }
9317            "pchannel" => {
9318                let capacity = if self.eat(&Token::LParen) {
9319                    if matches!(self.peek(), Token::RParen) {
9320                        self.advance();
9321                        None
9322                    } else {
9323                        let e = self.parse_expression()?;
9324                        self.expect(&Token::RParen)?;
9325                        Some(Box::new(e))
9326                    }
9327                } else {
9328                    None
9329                };
9330                Ok(Expr {
9331                    kind: ExprKind::Pchannel { capacity },
9332                    line,
9333                })
9334            }
9335            "psort" => {
9336                if matches!(self.peek(), Token::LBrace)
9337                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
9338                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9339                {
9340                    let block = self.parse_block_or_bareword_cmp_block()?;
9341                    self.eat(&Token::Comma);
9342                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9343                    Ok(Expr {
9344                        kind: ExprKind::PSortExpr {
9345                            cmp: Some(block),
9346                            list: Box::new(list),
9347                            progress: progress.map(Box::new),
9348                        },
9349                        line,
9350                    })
9351                } else {
9352                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9353                    Ok(Expr {
9354                        kind: ExprKind::PSortExpr {
9355                            cmp: None,
9356                            list: Box::new(list),
9357                            progress: progress.map(Box::new),
9358                        },
9359                        line,
9360                    })
9361                }
9362            }
9363            "preduce" => {
9364                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9365                Ok(Expr {
9366                    kind: ExprKind::PReduceExpr {
9367                        block,
9368                        list: Box::new(list),
9369                        progress: progress.map(Box::new),
9370                    },
9371                    line,
9372                })
9373            }
9374            "preduce_init" => {
9375                let (init, block, list, progress) =
9376                    self.parse_init_block_then_list_optional_progress()?;
9377                Ok(Expr {
9378                    kind: ExprKind::PReduceInitExpr {
9379                        init: Box::new(init),
9380                        block,
9381                        list: Box::new(list),
9382                        progress: progress.map(Box::new),
9383                    },
9384                    line,
9385                })
9386            }
9387            "pmap_reduce" => {
9388                let map_block = self.parse_block_or_bareword_block()?;
9389                // After the map block, expect either a `{ REDUCE }` block, or
9390                // after an eaten comma, a blockless reduce expr (`$a + $b`).
9391                let reduce_block = if matches!(self.peek(), Token::LBrace) {
9392                    self.parse_block()?
9393                } else {
9394                    // comma separates blockless map from blockless reduce
9395                    self.expect(&Token::Comma)?;
9396                    self.parse_block_or_bareword_cmp_block()?
9397                };
9398                self.eat(&Token::Comma);
9399                let line = self.peek_line();
9400                if let Token::Ident(ref kw) = self.peek().clone() {
9401                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
9402                        self.advance();
9403                        self.expect(&Token::FatArrow)?;
9404                        let prog = self.parse_assign_expr()?;
9405                        return Ok(Expr {
9406                            kind: ExprKind::PMapReduceExpr {
9407                                map_block,
9408                                reduce_block,
9409                                list: Box::new(Expr {
9410                                    kind: ExprKind::List(vec![]),
9411                                    line,
9412                                }),
9413                                progress: Some(Box::new(prog)),
9414                            },
9415                            line,
9416                        });
9417                    }
9418                }
9419                if matches!(
9420                    self.peek(),
9421                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
9422                ) {
9423                    return Ok(Expr {
9424                        kind: ExprKind::PMapReduceExpr {
9425                            map_block,
9426                            reduce_block,
9427                            list: Box::new(Expr {
9428                                kind: ExprKind::List(vec![]),
9429                                line,
9430                            }),
9431                            progress: None,
9432                        },
9433                        line,
9434                    });
9435                }
9436                let mut parts = vec![self.parse_assign_expr()?];
9437                loop {
9438                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
9439                        break;
9440                    }
9441                    if matches!(
9442                        self.peek(),
9443                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
9444                    ) {
9445                        break;
9446                    }
9447                    if let Token::Ident(ref kw) = self.peek().clone() {
9448                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
9449                            self.advance();
9450                            self.expect(&Token::FatArrow)?;
9451                            let prog = self.parse_assign_expr()?;
9452                            return Ok(Expr {
9453                                kind: ExprKind::PMapReduceExpr {
9454                                    map_block,
9455                                    reduce_block,
9456                                    list: Box::new(merge_expr_list(parts)),
9457                                    progress: Some(Box::new(prog)),
9458                                },
9459                                line,
9460                            });
9461                        }
9462                    }
9463                    parts.push(self.parse_assign_expr()?);
9464                }
9465                Ok(Expr {
9466                    kind: ExprKind::PMapReduceExpr {
9467                        map_block,
9468                        reduce_block,
9469                        list: Box::new(merge_expr_list(parts)),
9470                        progress: None,
9471                    },
9472                    line,
9473                })
9474            }
9475            "puniq" => {
9476                if self.pipe_supplies_slurped_list_operand() {
9477                    return Ok(Expr {
9478                        kind: ExprKind::FuncCall {
9479                            name: "puniq".to_string(),
9480                            args: vec![],
9481                        },
9482                        line,
9483                    });
9484                }
9485                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9486                let mut args = vec![list];
9487                if let Some(p) = progress {
9488                    args.push(p);
9489                }
9490                Ok(Expr {
9491                    kind: ExprKind::FuncCall {
9492                        name: "puniq".to_string(),
9493                        args,
9494                    },
9495                    line,
9496                })
9497            }
9498            "pfirst" => {
9499                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9500                let cr = Expr {
9501                    kind: ExprKind::CodeRef {
9502                        params: vec![],
9503                        body: block,
9504                    },
9505                    line,
9506                };
9507                let mut args = vec![cr, list];
9508                if let Some(p) = progress {
9509                    args.push(p);
9510                }
9511                Ok(Expr {
9512                    kind: ExprKind::FuncCall {
9513                        name: "pfirst".to_string(),
9514                        args,
9515                    },
9516                    line,
9517                })
9518            }
9519            "pany" => {
9520                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9521                let cr = Expr {
9522                    kind: ExprKind::CodeRef {
9523                        params: vec![],
9524                        body: block,
9525                    },
9526                    line,
9527                };
9528                let mut args = vec![cr, list];
9529                if let Some(p) = progress {
9530                    args.push(p);
9531                }
9532                Ok(Expr {
9533                    kind: ExprKind::FuncCall {
9534                        name: "pany".to_string(),
9535                        args,
9536                    },
9537                    line,
9538                })
9539            }
9540            "uniq" | "distinct" => {
9541                if self.pipe_supplies_slurped_list_operand() {
9542                    return Ok(Expr {
9543                        kind: ExprKind::FuncCall {
9544                            name: name.clone(),
9545                            args: vec![],
9546                        },
9547                        line,
9548                    });
9549                }
9550                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9551                if progress.is_some() {
9552                    return Err(self.syntax_err(
9553                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
9554                        line,
9555                    ));
9556                }
9557                Ok(Expr {
9558                    kind: ExprKind::FuncCall {
9559                        name: name.clone(),
9560                        args: vec![list],
9561                    },
9562                    line,
9563                })
9564            }
9565            "flatten" => {
9566                if self.pipe_supplies_slurped_list_operand() {
9567                    return Ok(Expr {
9568                        kind: ExprKind::FuncCall {
9569                            name: "flatten".to_string(),
9570                            args: vec![],
9571                        },
9572                        line,
9573                    });
9574                }
9575                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9576                if progress.is_some() {
9577                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
9578                }
9579                Ok(Expr {
9580                    kind: ExprKind::FuncCall {
9581                        name: "flatten".to_string(),
9582                        args: vec![list],
9583                    },
9584                    line,
9585                })
9586            }
9587            "set" => {
9588                if self.pipe_supplies_slurped_list_operand() {
9589                    return Ok(Expr {
9590                        kind: ExprKind::FuncCall {
9591                            name: "set".to_string(),
9592                            args: vec![],
9593                        },
9594                        line,
9595                    });
9596                }
9597                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9598                if progress.is_some() {
9599                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
9600                }
9601                Ok(Expr {
9602                    kind: ExprKind::FuncCall {
9603                        name: "set".to_string(),
9604                        args: vec![list],
9605                    },
9606                    line,
9607                })
9608            }
9609            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
9610            // Defaults to `$_` when no arg is given, like `length`. See
9611            // `builtin_file_size` in builtins.rs for the runtime behavior.
9612            "size" => {
9613                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9614                    return Ok(e);
9615                }
9616                if self.pipe_supplies_slurped_list_operand() {
9617                    return Ok(Expr {
9618                        kind: ExprKind::FuncCall {
9619                            name: "size".to_string(),
9620                            args: vec![],
9621                        },
9622                        line,
9623                    });
9624                }
9625                let a = self.parse_one_arg_or_default()?;
9626                Ok(Expr {
9627                    kind: ExprKind::FuncCall {
9628                        name: "size".to_string(),
9629                        args: vec![a],
9630                    },
9631                    line,
9632                })
9633            }
9634            "list_count" | "list_size" | "count" | "len" | "cnt" => {
9635                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9636                    return Ok(e);
9637                }
9638                if self.pipe_supplies_slurped_list_operand() {
9639                    return Ok(Expr {
9640                        kind: ExprKind::FuncCall {
9641                            name: name.clone(),
9642                            args: vec![],
9643                        },
9644                        line,
9645                    });
9646                }
9647                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9648                if progress.is_some() {
9649                    return Err(self.syntax_err(
9650                        "`progress =>` is not supported for list_count / list_size / count / cnt",
9651                        line,
9652                    ));
9653                }
9654                Ok(Expr {
9655                    kind: ExprKind::FuncCall {
9656                        name: name.clone(),
9657                        args: vec![list],
9658                    },
9659                    line,
9660                })
9661            }
9662            "shuffle" | "shuffled" => {
9663                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9664                    return Ok(e);
9665                }
9666                if self.pipe_supplies_slurped_list_operand() {
9667                    return Ok(Expr {
9668                        kind: ExprKind::FuncCall {
9669                            name: "shuffle".to_string(),
9670                            args: vec![],
9671                        },
9672                        line,
9673                    });
9674                }
9675                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9676                if progress.is_some() {
9677                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
9678                }
9679                Ok(Expr {
9680                    kind: ExprKind::FuncCall {
9681                        name: "shuffle".to_string(),
9682                        args: vec![list],
9683                    },
9684                    line,
9685                })
9686            }
9687            "chunked" => {
9688                let mut parts = Vec::new();
9689                if self.eat(&Token::LParen) {
9690                    if !matches!(self.peek(), Token::RParen) {
9691                        parts.push(self.parse_assign_expr()?);
9692                        while self.eat(&Token::Comma) {
9693                            if matches!(self.peek(), Token::RParen) {
9694                                break;
9695                            }
9696                            parts.push(self.parse_assign_expr()?);
9697                        }
9698                    }
9699                    self.expect(&Token::RParen)?;
9700                } else {
9701                    // Paren-less `chunked N`: `|>` is a hard terminator, not
9702                    // an operator inside the arg (see
9703                    // `parse_assign_expr_stop_at_pipe`).
9704                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
9705                    loop {
9706                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
9707                            break;
9708                        }
9709                        if matches!(
9710                            self.peek(),
9711                            Token::Semicolon
9712                                | Token::RBrace
9713                                | Token::RParen
9714                                | Token::Eof
9715                                | Token::PipeForward
9716                        ) {
9717                            break;
9718                        }
9719                        if self.peek_is_postfix_stmt_modifier_keyword() {
9720                            break;
9721                        }
9722                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
9723                    }
9724                }
9725                if parts.len() == 1 {
9726                    let n = parts.pop().unwrap();
9727                    return Ok(Expr {
9728                        kind: ExprKind::FuncCall {
9729                            name: "chunked".to_string(),
9730                            args: vec![n],
9731                        },
9732                        line,
9733                    });
9734                }
9735                if parts.is_empty() {
9736                    return Ok(Expr {
9737                        kind: ExprKind::FuncCall {
9738                            name: "chunked".to_string(),
9739                            args: parts,
9740                        },
9741                        line,
9742                    });
9743                }
9744                if parts.len() == 2 {
9745                    let n = parts.pop().unwrap();
9746                    let list = parts.pop().unwrap();
9747                    return Ok(Expr {
9748                        kind: ExprKind::FuncCall {
9749                            name: "chunked".to_string(),
9750                            args: vec![list, n],
9751                        },
9752                        line,
9753                    });
9754                }
9755                Err(self.syntax_err(
9756                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
9757                    line,
9758                ))
9759            }
9760            "windowed" => {
9761                let mut parts = Vec::new();
9762                if self.eat(&Token::LParen) {
9763                    if !matches!(self.peek(), Token::RParen) {
9764                        parts.push(self.parse_assign_expr()?);
9765                        while self.eat(&Token::Comma) {
9766                            if matches!(self.peek(), Token::RParen) {
9767                                break;
9768                            }
9769                            parts.push(self.parse_assign_expr()?);
9770                        }
9771                    }
9772                    self.expect(&Token::RParen)?;
9773                } else {
9774                    // Paren-less `windowed N`: same `|>`-terminator rule as
9775                    // `chunked` above.
9776                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
9777                    loop {
9778                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
9779                            break;
9780                        }
9781                        if matches!(
9782                            self.peek(),
9783                            Token::Semicolon
9784                                | Token::RBrace
9785                                | Token::RParen
9786                                | Token::Eof
9787                                | Token::PipeForward
9788                        ) {
9789                            break;
9790                        }
9791                        if self.peek_is_postfix_stmt_modifier_keyword() {
9792                            break;
9793                        }
9794                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
9795                    }
9796                }
9797                if parts.len() == 1 {
9798                    let n = parts.pop().unwrap();
9799                    return Ok(Expr {
9800                        kind: ExprKind::FuncCall {
9801                            name: "windowed".to_string(),
9802                            args: vec![n],
9803                        },
9804                        line,
9805                    });
9806                }
9807                if parts.is_empty() {
9808                    return Ok(Expr {
9809                        kind: ExprKind::FuncCall {
9810                            name: "windowed".to_string(),
9811                            args: parts,
9812                        },
9813                        line,
9814                    });
9815                }
9816                if parts.len() == 2 {
9817                    let n = parts.pop().unwrap();
9818                    let list = parts.pop().unwrap();
9819                    return Ok(Expr {
9820                        kind: ExprKind::FuncCall {
9821                            name: "windowed".to_string(),
9822                            args: vec![list, n],
9823                        },
9824                        line,
9825                    });
9826                }
9827                Err(self.syntax_err(
9828                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
9829                    line,
9830                ))
9831            }
9832            "any" | "all" | "none" => {
9833                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9834                if progress.is_some() {
9835                    return Err(self.syntax_err(
9836                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
9837                        line,
9838                    ));
9839                }
9840                let cr = Expr {
9841                    kind: ExprKind::CodeRef {
9842                        params: vec![],
9843                        body: block,
9844                    },
9845                    line,
9846                };
9847                Ok(Expr {
9848                    kind: ExprKind::FuncCall {
9849                        name: name.clone(),
9850                        args: vec![cr, list],
9851                    },
9852                    line,
9853                })
9854            }
9855            // Ruby `detect` / `find` — same as `List::Util::first` (first element matching block).
9856            "first" | "detect" | "find" => {
9857                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9858                if progress.is_some() {
9859                    return Err(self.syntax_err(
9860                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
9861                        line,
9862                    ));
9863                }
9864                let cr = Expr {
9865                    kind: ExprKind::CodeRef {
9866                        params: vec![],
9867                        body: block,
9868                    },
9869                    line,
9870                };
9871                Ok(Expr {
9872                    kind: ExprKind::FuncCall {
9873                        name: "first".to_string(),
9874                        args: vec![cr, list],
9875                    },
9876                    line,
9877                })
9878            }
9879            "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9880            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
9881                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9882                if progress.is_some() {
9883                    return Err(
9884                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
9885                    );
9886                }
9887                let cr = Expr {
9888                    kind: ExprKind::CodeRef {
9889                        params: vec![],
9890                        body: block,
9891                    },
9892                    line,
9893                };
9894                Ok(Expr {
9895                    kind: ExprKind::FuncCall {
9896                        name: name.to_string(),
9897                        args: vec![cr, list],
9898                    },
9899                    line,
9900                })
9901            }
9902            "group_by" | "chunk_by" => {
9903                if matches!(self.peek(), Token::LBrace) {
9904                    let (block, list) = self.parse_block_list()?;
9905                    let cr = Expr {
9906                        kind: ExprKind::CodeRef {
9907                            params: vec![],
9908                            body: block,
9909                        },
9910                        line,
9911                    };
9912                    Ok(Expr {
9913                        kind: ExprKind::FuncCall {
9914                            name: name.to_string(),
9915                            args: vec![cr, list],
9916                        },
9917                        line,
9918                    })
9919                } else {
9920                    let key_expr = self.parse_assign_expr()?;
9921                    self.expect(&Token::Comma)?;
9922                    let list_parts = self.parse_list_until_terminator()?;
9923                    let list_expr = if list_parts.len() == 1 {
9924                        list_parts.into_iter().next().unwrap()
9925                    } else {
9926                        Expr {
9927                            kind: ExprKind::List(list_parts),
9928                            line,
9929                        }
9930                    };
9931                    Ok(Expr {
9932                        kind: ExprKind::FuncCall {
9933                            name: name.to_string(),
9934                            args: vec![key_expr, list_expr],
9935                        },
9936                        line,
9937                    })
9938                }
9939            }
9940            "with_index" => {
9941                if self.pipe_supplies_slurped_list_operand() {
9942                    return Ok(Expr {
9943                        kind: ExprKind::FuncCall {
9944                            name: "with_index".to_string(),
9945                            args: vec![],
9946                        },
9947                        line,
9948                    });
9949                }
9950                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9951                if progress.is_some() {
9952                    return Err(
9953                        self.syntax_err("`progress =>` is not supported for with_index", line)
9954                    );
9955                }
9956                Ok(Expr {
9957                    kind: ExprKind::FuncCall {
9958                        name: "with_index".to_string(),
9959                        args: vec![list],
9960                    },
9961                    line,
9962                })
9963            }
9964            "pcache" => {
9965                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9966                Ok(Expr {
9967                    kind: ExprKind::PcacheExpr {
9968                        block,
9969                        list: Box::new(list),
9970                        progress: progress.map(Box::new),
9971                    },
9972                    line,
9973                })
9974            }
9975            "pselect" => {
9976                let paren = self.eat(&Token::LParen);
9977                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
9978                if paren {
9979                    self.expect(&Token::RParen)?;
9980                }
9981                if receivers.is_empty() {
9982                    return Err(self.syntax_err("pselect needs at least one receiver", line));
9983                }
9984                Ok(Expr {
9985                    kind: ExprKind::PselectExpr {
9986                        receivers,
9987                        timeout: timeout.map(Box::new),
9988                    },
9989                    line,
9990                })
9991            }
9992            "open" => {
9993                let paren = matches!(self.peek(), Token::LParen);
9994                if paren {
9995                    self.advance();
9996                }
9997                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
9998                    self.advance();
9999                    let name = self.parse_scalar_var_name()?;
10000                    self.expect(&Token::Comma)?;
10001                    let mode = self.parse_assign_expr()?;
10002                    let file = if self.eat(&Token::Comma) {
10003                        Some(self.parse_assign_expr()?)
10004                    } else {
10005                        None
10006                    };
10007                    if paren {
10008                        self.expect(&Token::RParen)?;
10009                    }
10010                    Ok(Expr {
10011                        kind: ExprKind::Open {
10012                            handle: Box::new(Expr {
10013                                kind: ExprKind::OpenMyHandle { name },
10014                                line,
10015                            }),
10016                            mode: Box::new(mode),
10017                            file: file.map(Box::new),
10018                        },
10019                        line,
10020                    })
10021                } else {
10022                    let args = if paren {
10023                        self.parse_arg_list()?
10024                    } else {
10025                        self.parse_list_until_terminator()?
10026                    };
10027                    if paren {
10028                        self.expect(&Token::RParen)?;
10029                    }
10030                    if args.len() < 2 {
10031                        return Err(self.syntax_err("open requires at least 2 arguments", line));
10032                    }
10033                    Ok(Expr {
10034                        kind: ExprKind::Open {
10035                            handle: Box::new(args[0].clone()),
10036                            mode: Box::new(args[1].clone()),
10037                            file: args.get(2).cloned().map(Box::new),
10038                        },
10039                        line,
10040                    })
10041                }
10042            }
10043            "close" => {
10044                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10045                    return Ok(e);
10046                }
10047                let a = self.parse_one_arg_or_default()?;
10048                Ok(Expr {
10049                    kind: ExprKind::Close(Box::new(a)),
10050                    line,
10051                })
10052            }
10053            "opendir" => {
10054                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10055                    return Ok(e);
10056                }
10057                let args = self.parse_builtin_args()?;
10058                if args.len() != 2 {
10059                    return Err(self.syntax_err("opendir requires two arguments", line));
10060                }
10061                Ok(Expr {
10062                    kind: ExprKind::Opendir {
10063                        handle: Box::new(args[0].clone()),
10064                        path: Box::new(args[1].clone()),
10065                    },
10066                    line,
10067                })
10068            }
10069            "readdir" => {
10070                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10071                    return Ok(e);
10072                }
10073                let a = self.parse_one_arg()?;
10074                Ok(Expr {
10075                    kind: ExprKind::Readdir(Box::new(a)),
10076                    line,
10077                })
10078            }
10079            "closedir" => {
10080                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10081                    return Ok(e);
10082                }
10083                let a = self.parse_one_arg()?;
10084                Ok(Expr {
10085                    kind: ExprKind::Closedir(Box::new(a)),
10086                    line,
10087                })
10088            }
10089            "rewinddir" => {
10090                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10091                    return Ok(e);
10092                }
10093                let a = self.parse_one_arg()?;
10094                Ok(Expr {
10095                    kind: ExprKind::Rewinddir(Box::new(a)),
10096                    line,
10097                })
10098            }
10099            "telldir" => {
10100                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10101                    return Ok(e);
10102                }
10103                let a = self.parse_one_arg()?;
10104                Ok(Expr {
10105                    kind: ExprKind::Telldir(Box::new(a)),
10106                    line,
10107                })
10108            }
10109            "seekdir" => {
10110                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10111                    return Ok(e);
10112                }
10113                let args = self.parse_builtin_args()?;
10114                if args.len() != 2 {
10115                    return Err(self.syntax_err("seekdir requires two arguments", line));
10116                }
10117                Ok(Expr {
10118                    kind: ExprKind::Seekdir {
10119                        handle: Box::new(args[0].clone()),
10120                        position: Box::new(args[1].clone()),
10121                    },
10122                    line,
10123                })
10124            }
10125            "eof" => {
10126                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10127                    return Ok(e);
10128                }
10129                if matches!(self.peek(), Token::LParen) {
10130                    self.advance();
10131                    if matches!(self.peek(), Token::RParen) {
10132                        self.advance();
10133                        Ok(Expr {
10134                            kind: ExprKind::Eof(None),
10135                            line,
10136                        })
10137                    } else {
10138                        let a = self.parse_expression()?;
10139                        self.expect(&Token::RParen)?;
10140                        Ok(Expr {
10141                            kind: ExprKind::Eof(Some(Box::new(a))),
10142                            line,
10143                        })
10144                    }
10145                } else {
10146                    Ok(Expr {
10147                        kind: ExprKind::Eof(None),
10148                        line,
10149                    })
10150                }
10151            }
10152            "system" => {
10153                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10154                    return Ok(e);
10155                }
10156                let args = self.parse_builtin_args()?;
10157                Ok(Expr {
10158                    kind: ExprKind::System(args),
10159                    line,
10160                })
10161            }
10162            "exec" => {
10163                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10164                    return Ok(e);
10165                }
10166                let args = self.parse_builtin_args()?;
10167                Ok(Expr {
10168                    kind: ExprKind::Exec(args),
10169                    line,
10170                })
10171            }
10172            "eval" => {
10173                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10174                    return Ok(e);
10175                }
10176                let a = if matches!(self.peek(), Token::LBrace) {
10177                    let block = self.parse_block()?;
10178                    Expr {
10179                        kind: ExprKind::CodeRef {
10180                            params: vec![],
10181                            body: block,
10182                        },
10183                        line,
10184                    }
10185                } else {
10186                    self.parse_one_arg_or_default()?
10187                };
10188                Ok(Expr {
10189                    kind: ExprKind::Eval(Box::new(a)),
10190                    line,
10191                })
10192            }
10193            "do" => {
10194                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10195                    return Ok(e);
10196                }
10197                let a = self.parse_one_arg()?;
10198                Ok(Expr {
10199                    kind: ExprKind::Do(Box::new(a)),
10200                    line,
10201                })
10202            }
10203            "require" => {
10204                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10205                    return Ok(e);
10206                }
10207                let a = self.parse_one_arg()?;
10208                Ok(Expr {
10209                    kind: ExprKind::Require(Box::new(a)),
10210                    line,
10211                })
10212            }
10213            "exit" => {
10214                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10215                    return Ok(e);
10216                }
10217                if matches!(
10218                    self.peek(),
10219                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
10220                ) {
10221                    Ok(Expr {
10222                        kind: ExprKind::Exit(None),
10223                        line,
10224                    })
10225                } else {
10226                    let a = self.parse_one_arg()?;
10227                    Ok(Expr {
10228                        kind: ExprKind::Exit(Some(Box::new(a))),
10229                        line,
10230                    })
10231                }
10232            }
10233            "chdir" => {
10234                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10235                    return Ok(e);
10236                }
10237                let a = self.parse_one_arg_or_default()?;
10238                Ok(Expr {
10239                    kind: ExprKind::Chdir(Box::new(a)),
10240                    line,
10241                })
10242            }
10243            "mkdir" => {
10244                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10245                    return Ok(e);
10246                }
10247                let args = self.parse_builtin_args()?;
10248                Ok(Expr {
10249                    kind: ExprKind::Mkdir {
10250                        path: Box::new(args[0].clone()),
10251                        mode: args.get(1).cloned().map(Box::new),
10252                    },
10253                    line,
10254                })
10255            }
10256            "unlink" | "rm" => {
10257                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10258                    return Ok(e);
10259                }
10260                let args = self.parse_builtin_args()?;
10261                Ok(Expr {
10262                    kind: ExprKind::Unlink(args),
10263                    line,
10264                })
10265            }
10266            "rename" => {
10267                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10268                    return Ok(e);
10269                }
10270                let args = self.parse_builtin_args()?;
10271                if args.len() != 2 {
10272                    return Err(self.syntax_err("rename requires two arguments", line));
10273                }
10274                Ok(Expr {
10275                    kind: ExprKind::Rename {
10276                        old: Box::new(args[0].clone()),
10277                        new: Box::new(args[1].clone()),
10278                    },
10279                    line,
10280                })
10281            }
10282            "chmod" => {
10283                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10284                    return Ok(e);
10285                }
10286                let args = self.parse_builtin_args()?;
10287                if args.len() < 2 {
10288                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
10289                }
10290                Ok(Expr {
10291                    kind: ExprKind::Chmod(args),
10292                    line,
10293                })
10294            }
10295            "chown" => {
10296                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10297                    return Ok(e);
10298                }
10299                let args = self.parse_builtin_args()?;
10300                if args.len() < 3 {
10301                    return Err(
10302                        self.syntax_err("chown requires uid, gid, and at least one file", line)
10303                    );
10304                }
10305                Ok(Expr {
10306                    kind: ExprKind::Chown(args),
10307                    line,
10308                })
10309            }
10310            "stat" => {
10311                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10312                    return Ok(e);
10313                }
10314                let args = self.parse_builtin_args()?;
10315                let arg = if args.len() == 1 {
10316                    args[0].clone()
10317                } else if args.is_empty() {
10318                    Expr {
10319                        kind: ExprKind::ScalarVar("_".into()),
10320                        line,
10321                    }
10322                } else {
10323                    return Err(self.syntax_err("stat requires zero or one argument", line));
10324                };
10325                Ok(Expr {
10326                    kind: ExprKind::Stat(Box::new(arg)),
10327                    line,
10328                })
10329            }
10330            "lstat" => {
10331                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10332                    return Ok(e);
10333                }
10334                let args = self.parse_builtin_args()?;
10335                let arg = if args.len() == 1 {
10336                    args[0].clone()
10337                } else if args.is_empty() {
10338                    Expr {
10339                        kind: ExprKind::ScalarVar("_".into()),
10340                        line,
10341                    }
10342                } else {
10343                    return Err(self.syntax_err("lstat requires zero or one argument", line));
10344                };
10345                Ok(Expr {
10346                    kind: ExprKind::Lstat(Box::new(arg)),
10347                    line,
10348                })
10349            }
10350            "link" => {
10351                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10352                    return Ok(e);
10353                }
10354                let args = self.parse_builtin_args()?;
10355                if args.len() != 2 {
10356                    return Err(self.syntax_err("link requires two arguments", line));
10357                }
10358                Ok(Expr {
10359                    kind: ExprKind::Link {
10360                        old: Box::new(args[0].clone()),
10361                        new: Box::new(args[1].clone()),
10362                    },
10363                    line,
10364                })
10365            }
10366            "symlink" => {
10367                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10368                    return Ok(e);
10369                }
10370                let args = self.parse_builtin_args()?;
10371                if args.len() != 2 {
10372                    return Err(self.syntax_err("symlink requires two arguments", line));
10373                }
10374                Ok(Expr {
10375                    kind: ExprKind::Symlink {
10376                        old: Box::new(args[0].clone()),
10377                        new: Box::new(args[1].clone()),
10378                    },
10379                    line,
10380                })
10381            }
10382            "readlink" => {
10383                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10384                    return Ok(e);
10385                }
10386                let args = self.parse_builtin_args()?;
10387                let arg = if args.len() == 1 {
10388                    args[0].clone()
10389                } else if args.is_empty() {
10390                    Expr {
10391                        kind: ExprKind::ScalarVar("_".into()),
10392                        line,
10393                    }
10394                } else {
10395                    return Err(self.syntax_err("readlink requires zero or one argument", line));
10396                };
10397                Ok(Expr {
10398                    kind: ExprKind::Readlink(Box::new(arg)),
10399                    line,
10400                })
10401            }
10402            "files" => {
10403                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10404                    return Ok(e);
10405                }
10406                let args = self.parse_builtin_args()?;
10407                Ok(Expr {
10408                    kind: ExprKind::Files(args),
10409                    line,
10410                })
10411            }
10412            "filesf" => {
10413                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10414                    return Ok(e);
10415                }
10416                let args = self.parse_builtin_args()?;
10417                Ok(Expr {
10418                    kind: ExprKind::Filesf(args),
10419                    line,
10420                })
10421            }
10422            "fr" => {
10423                let args = self.parse_builtin_args()?;
10424                Ok(Expr {
10425                    kind: ExprKind::FilesfRecursive(args),
10426                    line,
10427                })
10428            }
10429            "dirs" | "d" => {
10430                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10431                    return Ok(e);
10432                }
10433                let args = self.parse_builtin_args()?;
10434                Ok(Expr {
10435                    kind: ExprKind::Dirs(args),
10436                    line,
10437                })
10438            }
10439            "dr" => {
10440                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10441                    return Ok(e);
10442                }
10443                let args = self.parse_builtin_args()?;
10444                Ok(Expr {
10445                    kind: ExprKind::DirsRecursive(args),
10446                    line,
10447                })
10448            }
10449            "sym_links" => {
10450                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10451                    return Ok(e);
10452                }
10453                let args = self.parse_builtin_args()?;
10454                Ok(Expr {
10455                    kind: ExprKind::SymLinks(args),
10456                    line,
10457                })
10458            }
10459            "sockets" => {
10460                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10461                    return Ok(e);
10462                }
10463                let args = self.parse_builtin_args()?;
10464                Ok(Expr {
10465                    kind: ExprKind::Sockets(args),
10466                    line,
10467                })
10468            }
10469            "pipes" => {
10470                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10471                    return Ok(e);
10472                }
10473                let args = self.parse_builtin_args()?;
10474                Ok(Expr {
10475                    kind: ExprKind::Pipes(args),
10476                    line,
10477                })
10478            }
10479            "block_devices" => {
10480                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10481                    return Ok(e);
10482                }
10483                let args = self.parse_builtin_args()?;
10484                Ok(Expr {
10485                    kind: ExprKind::BlockDevices(args),
10486                    line,
10487                })
10488            }
10489            "char_devices" => {
10490                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10491                    return Ok(e);
10492                }
10493                let args = self.parse_builtin_args()?;
10494                Ok(Expr {
10495                    kind: ExprKind::CharDevices(args),
10496                    line,
10497                })
10498            }
10499            "glob" => {
10500                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10501                    return Ok(e);
10502                }
10503                let args = self.parse_builtin_args()?;
10504                Ok(Expr {
10505                    kind: ExprKind::Glob(args),
10506                    line,
10507                })
10508            }
10509            "glob_par" => {
10510                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10511                    return Ok(e);
10512                }
10513                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
10514                Ok(Expr {
10515                    kind: ExprKind::GlobPar { args, progress },
10516                    line,
10517                })
10518            }
10519            "par_sed" => {
10520                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10521                    return Ok(e);
10522                }
10523                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
10524                Ok(Expr {
10525                    kind: ExprKind::ParSed { args, progress },
10526                    line,
10527                })
10528            }
10529            "bless" => {
10530                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10531                    return Ok(e);
10532                }
10533                let args = self.parse_builtin_args()?;
10534                Ok(Expr {
10535                    kind: ExprKind::Bless {
10536                        ref_expr: Box::new(args[0].clone()),
10537                        class: args.get(1).cloned().map(Box::new),
10538                    },
10539                    line,
10540                })
10541            }
10542            "caller" => {
10543                if matches!(self.peek(), Token::LParen) {
10544                    self.advance();
10545                    if matches!(self.peek(), Token::RParen) {
10546                        self.advance();
10547                        Ok(Expr {
10548                            kind: ExprKind::Caller(None),
10549                            line,
10550                        })
10551                    } else {
10552                        let a = self.parse_expression()?;
10553                        self.expect(&Token::RParen)?;
10554                        Ok(Expr {
10555                            kind: ExprKind::Caller(Some(Box::new(a))),
10556                            line,
10557                        })
10558                    }
10559                } else {
10560                    Ok(Expr {
10561                        kind: ExprKind::Caller(None),
10562                        line,
10563                    })
10564                }
10565            }
10566            "wantarray" => {
10567                if matches!(self.peek(), Token::LParen) {
10568                    self.advance();
10569                    self.expect(&Token::RParen)?;
10570                }
10571                Ok(Expr {
10572                    kind: ExprKind::Wantarray,
10573                    line,
10574                })
10575            }
10576            "sub" | "fn" => {
10577                // Anonymous sub/fn — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
10578                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
10579                let body = self.parse_block()?;
10580                Ok(Expr {
10581                    kind: ExprKind::CodeRef { params, body },
10582                    line,
10583                })
10584            }
10585            _ => {
10586                // Generic function call
10587                // Check for fat arrow (bareword string in hash)
10588                if matches!(self.peek(), Token::FatArrow) {
10589                    return Ok(Expr {
10590                        kind: ExprKind::String(name),
10591                        line,
10592                    });
10593                }
10594                // Function call with optional parens
10595                if matches!(self.peek(), Token::LParen) {
10596                    self.advance();
10597                    let args = self.parse_arg_list()?;
10598                    self.expect(&Token::RParen)?;
10599                    Ok(Expr {
10600                        kind: ExprKind::FuncCall { name, args },
10601                        line,
10602                    })
10603                } else if self.peek().is_term_start()
10604                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
10605                        && matches!(self.peek_at(1), Token::Ident(_)))
10606                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
10607                {
10608                    // Perl allows func arg without parens
10609                    // Guard: `sub <name> { }` is a named sub declaration (new
10610                    // statement), not an argument to the preceding call.
10611                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
10612                    // barewords (used by thread macro so `t Color::Red p` treats
10613                    // `p` as a stage, not an argument to the enum variant), but
10614                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
10615                    let args = self.parse_list_until_terminator()?;
10616                    Ok(Expr {
10617                        kind: ExprKind::FuncCall { name, args },
10618                        line,
10619                    })
10620                } else {
10621                    // No parens, no visible arguments — emit a Bareword.
10622                    // At runtime, Bareword tries sub resolution first (zero-arg
10623                    // call) and falls back to a string value.  stryke extension
10624                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
10625                    // with `$_` injection separately.
10626                    Ok(Expr {
10627                        kind: ExprKind::Bareword(name),
10628                        line,
10629                    })
10630                }
10631            }
10632        }
10633    }
10634
10635    fn parse_print_like(
10636        &mut self,
10637        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
10638    ) -> PerlResult<Expr> {
10639        let line = self.peek_line();
10640        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
10641        let handle = if let Token::Ident(ref h) = self.peek().clone() {
10642            if h.chars().all(|c| c.is_uppercase() || c == '_')
10643                && !matches!(self.peek(), Token::LParen)
10644            {
10645                let h = h.clone();
10646                let saved = self.pos;
10647                self.advance();
10648                // Verify next token is a term start (not operator)
10649                if self.peek().is_term_start()
10650                    || matches!(
10651                        self.peek(),
10652                        Token::DoubleString(_) | Token::BacktickString(_) | Token::SingleString(_)
10653                    )
10654                {
10655                    Some(h)
10656                } else {
10657                    self.pos = saved;
10658                    None
10659                }
10660            } else {
10661                None
10662            }
10663        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
10664            // `print $fh "msg"` — scalar variable as indirect filehandle.
10665            // Treat as handle when the next token (after $var) is a term-start or
10666            // string literal *without* a preceding comma/operator, matching Perl's
10667            // indirect-object heuristic.
10668            // Exclude `$_` — it's virtually always the topic variable, not a handle.
10669            // Exclude `[` and `{` — those are array/hash subscripts on the variable
10670            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
10671            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
10672            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
10673            let v = v.clone();
10674            if v == "_" {
10675                None
10676            } else {
10677                let saved = self.pos;
10678                self.advance();
10679                let next = self.peek().clone();
10680                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
10681                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
10682                if !is_stmt_modifier
10683                    && !matches!(next, Token::LBracket | Token::LBrace)
10684                    && (next.is_term_start()
10685                        || matches!(
10686                            next,
10687                            Token::DoubleString(_)
10688                                | Token::BacktickString(_)
10689                                | Token::SingleString(_)
10690                        ))
10691                {
10692                    // Next token looks like a print argument — $var is the handle.
10693                    Some(format!("${v}"))
10694                } else {
10695                    self.pos = saved;
10696                    None
10697                }
10698            }
10699        } else {
10700            None
10701        };
10702        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
10703        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
10704        // are given, prints $_." (Same convention as the topic-default unary
10705        // builtins handled in `parse_one_arg_or_default`.)
10706        let args =
10707            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
10708                let line_topic = self.peek_line();
10709                self.advance(); // (
10710                self.advance(); // )
10711                vec![Expr {
10712                    kind: ExprKind::ScalarVar("_".into()),
10713                    line: line_topic,
10714                }]
10715            } else {
10716                self.parse_list_until_terminator()?
10717            };
10718        Ok(Expr {
10719            kind: make(handle, args),
10720            line,
10721        })
10722    }
10723
10724    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
10725        let block = self.parse_block()?;
10726        let block_end_line = self.prev_line();
10727        self.eat(&Token::Comma);
10728        // On the RHS of `|>`, the list operand is supplied by the piped LHS
10729        // and will be substituted at desugar time — accept a placeholder when
10730        // we're at a terminator here or on a new line (implicit semicolon).
10731        if self.in_pipe_rhs()
10732            && (matches!(
10733                self.peek(),
10734                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10735            ) || self.peek_line() > block_end_line)
10736        {
10737            let line = self.peek_line();
10738            return Ok((block, self.pipe_placeholder_list(line)));
10739        }
10740        let list = self.parse_expression()?;
10741        Ok((block, list))
10742    }
10743
10744    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
10745    /// When `paren` is true, stops at `)` as well as normal terminators.
10746    fn parse_comma_expr_list_with_timeout_tail(
10747        &mut self,
10748        paren: bool,
10749    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
10750        let mut parts = vec![self.parse_assign_expr()?];
10751        loop {
10752            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10753                break;
10754            }
10755            if paren && matches!(self.peek(), Token::RParen) {
10756                break;
10757            }
10758            if matches!(
10759                self.peek(),
10760                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10761            ) {
10762                break;
10763            }
10764            if self.peek_is_postfix_stmt_modifier_keyword() {
10765                break;
10766            }
10767            if let Token::Ident(ref kw) = self.peek().clone() {
10768                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
10769                    self.advance();
10770                    self.expect(&Token::FatArrow)?;
10771                    let t = self.parse_assign_expr()?;
10772                    return Ok((parts, Some(t)));
10773                }
10774            }
10775            parts.push(self.parse_assign_expr()?);
10776        }
10777        Ok((parts, None))
10778    }
10779
10780    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
10781    fn parse_init_block_then_list_optional_progress(
10782        &mut self,
10783    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
10784        let init = self.parse_assign_expr()?;
10785        self.expect(&Token::Comma)?;
10786        let block = self.parse_block_or_bareword_block()?;
10787        self.eat(&Token::Comma);
10788        let line = self.peek_line();
10789        if let Token::Ident(ref kw) = self.peek().clone() {
10790            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10791                self.advance();
10792                self.expect(&Token::FatArrow)?;
10793                let prog = self.parse_assign_expr()?;
10794                return Ok((
10795                    init,
10796                    block,
10797                    Expr {
10798                        kind: ExprKind::List(vec![]),
10799                        line,
10800                    },
10801                    Some(prog),
10802                ));
10803            }
10804        }
10805        if matches!(
10806            self.peek(),
10807            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10808        ) {
10809            return Ok((
10810                init,
10811                block,
10812                Expr {
10813                    kind: ExprKind::List(vec![]),
10814                    line,
10815                },
10816                None,
10817            ));
10818        }
10819        let mut parts = vec![self.parse_assign_expr()?];
10820        loop {
10821            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10822                break;
10823            }
10824            if matches!(
10825                self.peek(),
10826                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10827            ) {
10828                break;
10829            }
10830            if self.peek_is_postfix_stmt_modifier_keyword() {
10831                break;
10832            }
10833            if let Token::Ident(ref kw) = self.peek().clone() {
10834                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10835                    self.advance();
10836                    self.expect(&Token::FatArrow)?;
10837                    let prog = self.parse_assign_expr()?;
10838                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
10839                }
10840            }
10841            parts.push(self.parse_assign_expr()?);
10842        }
10843        Ok((init, block, merge_expr_list(parts), None))
10844    }
10845
10846    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
10847    fn parse_cluster_block_then_list_optional_progress(
10848        &mut self,
10849    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
10850        let cluster = self.parse_assign_expr()?;
10851        let block = self.parse_block_or_bareword_block()?;
10852        self.eat(&Token::Comma);
10853        let line = self.peek_line();
10854        if let Token::Ident(ref kw) = self.peek().clone() {
10855            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10856                self.advance();
10857                self.expect(&Token::FatArrow)?;
10858                let prog = self.parse_assign_expr_stop_at_pipe()?;
10859                return Ok((
10860                    cluster,
10861                    block,
10862                    Expr {
10863                        kind: ExprKind::List(vec![]),
10864                        line,
10865                    },
10866                    Some(prog),
10867                ));
10868            }
10869        }
10870        let empty_list_ok = matches!(
10871            self.peek(),
10872            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10873        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
10874        if empty_list_ok {
10875            return Ok((
10876                cluster,
10877                block,
10878                Expr {
10879                    kind: ExprKind::List(vec![]),
10880                    line,
10881                },
10882                None,
10883            ));
10884        }
10885        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
10886        loop {
10887            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10888                break;
10889            }
10890            if matches!(
10891                self.peek(),
10892                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10893            ) {
10894                break;
10895            }
10896            if self.peek_is_postfix_stmt_modifier_keyword() {
10897                break;
10898            }
10899            if let Token::Ident(ref kw) = self.peek().clone() {
10900                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10901                    self.advance();
10902                    self.expect(&Token::FatArrow)?;
10903                    let prog = self.parse_assign_expr_stop_at_pipe()?;
10904                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
10905                }
10906            }
10907            parts.push(self.parse_assign_expr_stop_at_pipe()?);
10908        }
10909        Ok((cluster, block, merge_expr_list(parts), None))
10910    }
10911
10912    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
10913    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
10914    ///
10915    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
10916    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
10917    /// stage — individual list parts and the progress value parse through
10918    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
10919    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
10920    fn parse_block_then_list_optional_progress(
10921        &mut self,
10922    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
10923        let block = self.parse_block_or_bareword_block()?;
10924        self.eat(&Token::Comma);
10925        let line = self.peek_line();
10926        if let Token::Ident(ref kw) = self.peek().clone() {
10927            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10928                self.advance();
10929                self.expect(&Token::FatArrow)?;
10930                let prog = self.parse_assign_expr_stop_at_pipe()?;
10931                return Ok((
10932                    block,
10933                    Expr {
10934                        kind: ExprKind::List(vec![]),
10935                        line,
10936                    },
10937                    Some(prog),
10938                ));
10939            }
10940        }
10941        // An empty list operand is allowed when the next token terminates the
10942        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
10943        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
10944        // terminator — left-associative chaining leaves the outer `|>` for
10945        // the enclosing pipe-forward loop.
10946        let empty_list_ok = matches!(
10947            self.peek(),
10948            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10949        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
10950        if empty_list_ok {
10951            return Ok((
10952                block,
10953                Expr {
10954                    kind: ExprKind::List(vec![]),
10955                    line,
10956                },
10957                None,
10958            ));
10959        }
10960        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
10961        loop {
10962            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10963                break;
10964            }
10965            if matches!(
10966                self.peek(),
10967                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
10968            ) {
10969                break;
10970            }
10971            if self.peek_is_postfix_stmt_modifier_keyword() {
10972                break;
10973            }
10974            if let Token::Ident(ref kw) = self.peek().clone() {
10975                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10976                    self.advance();
10977                    self.expect(&Token::FatArrow)?;
10978                    let prog = self.parse_assign_expr_stop_at_pipe()?;
10979                    return Ok((block, merge_expr_list(parts), Some(prog)));
10980                }
10981            }
10982            parts.push(self.parse_assign_expr_stop_at_pipe()?);
10983        }
10984        Ok((block, merge_expr_list(parts), None))
10985    }
10986
10987    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
10988    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
10989        // `fan { BLOCK }` — no count
10990        if matches!(self.peek(), Token::LBrace) {
10991            let block = self.parse_block()?;
10992            return Ok((None, block));
10993        }
10994        let saved = self.pos;
10995        // Not a brace — first expr could be count or body
10996        let first = self.parse_postfix()?;
10997        if matches!(self.peek(), Token::LBrace) {
10998            // `fan COUNT { BLOCK }`
10999            let block = self.parse_block()?;
11000            Ok((Some(Box::new(first)), block))
11001        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
11002            || (matches!(self.peek(), Token::Comma)
11003                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
11004        {
11005            // `fan EXPR;` — no count, first is the body
11006            let block = self.bareword_to_no_arg_block(first);
11007            Ok((None, block))
11008        } else if matches!(first.kind, ExprKind::Integer(_)) {
11009            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
11010            self.eat(&Token::Comma);
11011            let body = self.parse_fan_blockless_body(line)?;
11012            Ok((Some(Box::new(first)), body))
11013        } else {
11014            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
11015            // — backtrack and re-parse as a full body expression.
11016            self.pos = saved;
11017            let body = self.parse_fan_blockless_body(line)?;
11018            Ok((None, body))
11019        }
11020    }
11021
11022    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
11023    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
11024        if matches!(self.peek(), Token::LBrace) {
11025            return self.parse_block();
11026        }
11027        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
11028        if let Token::Ident(ref name) = self.peek().clone() {
11029            if matches!(
11030                self.peek_at(1),
11031                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11032            ) {
11033                let name = name.clone();
11034                self.advance();
11035                let body = Expr {
11036                    kind: ExprKind::FuncCall { name, args: vec![] },
11037                    line,
11038                };
11039                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11040            }
11041        }
11042        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
11043        let expr = self.parse_assign_expr_stop_at_pipe()?;
11044        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11045    }
11046
11047    /// Wrap a parsed expression as a single-statement block, converting bare
11048    /// identifiers to zero-arg calls (`work` → `work()`).
11049    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
11050        let line = expr.line;
11051        let body = match &expr.kind {
11052            ExprKind::Bareword(name) => Expr {
11053                kind: ExprKind::FuncCall {
11054                    name: name.clone(),
11055                    args: vec![],
11056                },
11057                line,
11058            },
11059            _ => expr,
11060        };
11061        vec![Statement::new(StmtKind::Expression(body), line)]
11062    }
11063
11064    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
11065    ///
11066    /// When the next token is `{`, delegates to [`Self::parse_block`].
11067    /// Otherwise parses a single postfix expression and wraps it as a call
11068    /// with `$_` as argument (for barewords) or a plain expression statement:
11069    ///
11070    /// - Bareword `foo` → `{ foo($_) }`
11071    /// - Other expr     → `{ EXPR }`
11072    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
11073        if matches!(self.peek(), Token::LBrace) {
11074            return self.parse_block();
11075        }
11076        let line = self.peek_line();
11077        // A lone identifier followed by a list-terminator is a bare sub name:
11078        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
11079        if let Token::Ident(ref name) = self.peek().clone() {
11080            if matches!(
11081                self.peek_at(1),
11082                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11083            ) {
11084                let name = name.clone();
11085                self.advance();
11086                let body = Expr {
11087                    kind: ExprKind::FuncCall {
11088                        name,
11089                        args: vec![Expr {
11090                            kind: ExprKind::ScalarVar("_".to_string()),
11091                            line,
11092                        }],
11093                    },
11094                    line,
11095                };
11096                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11097            }
11098        }
11099        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
11100        let expr = self.parse_assign_expr_stop_at_pipe()?;
11101        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11102    }
11103
11104    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
11105    /// bare function takes no args (body runs stand-alone, not per-element).
11106    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
11107    /// greedily swallow subsequent tokens as function arguments.
11108    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
11109        if matches!(self.peek(), Token::LBrace) {
11110            return self.parse_block();
11111        }
11112        let line = self.peek_line();
11113        if let Token::Ident(ref name) = self.peek().clone() {
11114            if matches!(
11115                self.peek_at(1),
11116                Token::Comma
11117                    | Token::Semicolon
11118                    | Token::RBrace
11119                    | Token::Eof
11120                    | Token::PipeForward
11121                    | Token::Integer(_)
11122            ) {
11123                let name = name.clone();
11124                self.advance();
11125                let body = Expr {
11126                    kind: ExprKind::FuncCall { name, args: vec![] },
11127                    line,
11128                };
11129                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11130            }
11131        }
11132        let expr = self.parse_postfix()?;
11133        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11134    }
11135
11136    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
11137    /// treated as a bare sub name (e.g. inside `sort`).
11138    /// True for any bareword the parser treats as a known builtin / keyword —
11139    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
11140    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
11141    /// as a comparator name if it *isn't* a known bareword). Previously named
11142    /// `is_perl_keyword`, which was misleading.
11143    fn is_known_bareword(name: &str) -> bool {
11144        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
11145    }
11146
11147    /// True iff `name` appears as any spelling (primary *or* alias) in a
11148    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
11149    /// up in the parser-level keyword lists but are still callable at
11150    /// runtime — so `map { tj }` can default to `tj($_)` the same way
11151    /// `map { to_json }` does.
11152    fn is_try_builtin_name(name: &str) -> bool {
11153        crate::builtins::BUILTIN_ARMS
11154            .iter()
11155            .any(|arm| arm.contains(&name))
11156    }
11157
11158    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
11159    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
11160    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
11161    /// is derived from this list by `build.rs`.
11162    fn is_perl5_core(name: &str) -> bool {
11163        matches!(
11164            name,
11165            // ── array / list ────────────────────────────────────────────
11166            "map" | "grep" | "sort" | "reverse" | "join" | "split"
11167            | "push" | "pop" | "shift" | "unshift" | "splice"
11168            | "pack" | "unpack"
11169            // ── hash ────────────────────────────────────────────────────
11170            | "keys" | "values" | "each"
11171            // ── string ──────────────────────────────────────────────────
11172            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
11173            | "lc" | "uc" | "lcfirst" | "ucfirst"
11174            | "length" | "substr" | "index" | "rindex"
11175            | "sprintf" | "printf" | "print" | "say"
11176            | "pos" | "quotemeta" | "study"
11177            // ── numeric ─────────────────────────────────────────────────
11178            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
11179            | "exp" | "log" | "rand" | "srand"
11180            // ── time ────────────────────────────────────────────────────
11181            | "time" | "localtime" | "gmtime"
11182            // ── type / reflection ───────────────────────────────────────
11183            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
11184            | "caller" | "delete" | "exists" | "bless" | "prototype"
11185            | "tie" | "untie" | "tied"
11186            // ── io ──────────────────────────────────────────────────────
11187            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
11188            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
11189            | "format" | "formline" | "select" | "vec"
11190            | "sysopen" | "sysread" | "sysseek" | "syswrite"
11191            // ── filesystem ──────────────────────────────────────────────
11192            | "stat" | "lstat" | "rename" | "unlink" | "utime"
11193            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
11194            | "glob" | "opendir" | "readdir" | "closedir"
11195            | "link" | "readlink" | "symlink"
11196            // ── ipc ─────────────────────────────────────────────────────
11197            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
11198            // ── sysv ipc ────────────────────────────────────────────────
11199            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
11200            | "semctl" | "semget" | "semop"
11201            | "shmctl" | "shmget" | "shmread" | "shmwrite"
11202            // ── process / system ────────────────────────────────────────
11203            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
11204            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
11205            | "chroot" | "times" | "umask" | "reset"
11206            | "getpgrp" | "setpgrp" | "getppid"
11207            | "getpriority" | "setpriority"
11208            // ── socket ──────────────────────────────────────────────────
11209            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
11210            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
11211            | "getpeername" | "getsockname"
11212            // ── posix metadata ──────────────────────────────────────────
11213            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
11214            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
11215            | "getlogin"
11216            | "gethostbyname" | "gethostbyaddr" | "gethostent"
11217            | "getnetbyname" | "getnetent"
11218            | "getprotobyname" | "getprotoent"
11219            | "getservbyname" | "getservent"
11220            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
11221            | "endpwent" | "endgrent"
11222            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
11223            // ── control flow ────────────────────────────────────────────
11224            | "return" | "do" | "eval" | "require"
11225            | "my" | "our" | "local" | "use" | "no"
11226            | "sub" | "if" | "unless" | "while" | "until"
11227            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
11228            | "not" | "and" | "or"
11229            // ── quoting ─────────────────────────────────────────────────
11230            | "qw" | "qq" | "q"
11231            // ── phase blocks ────────────────────────────────────────────
11232            | "BEGIN" | "END"
11233        )
11234    }
11235
11236    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
11237    /// Used by `--compat` to reject extensions at parse time.
11238    fn stryke_extension_name(name: &str) -> Option<&str> {
11239        match name {
11240            // ── parallel ────────────────────────────────────────────────────
11241            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
11242            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
11243            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
11244            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
11245            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
11246            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
11247            | "pmaps" | "pflat_maps" | "pgreps"
11248            // ── functional / iterator ───────────────────────────────────────
11249            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "f" | "find_all" | "reduce" | "fold"
11250            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
11251            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "flatten" | "set"
11252            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
11253            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
11254            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
11255            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
11256            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
11257            | "zip_with" | "count_by" | "skip" | "first_or"
11258            // ── pipeline / string helpers ───────────────────────────────────
11259            | "input" | "lines" | "words" | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
11260            | "punctuation" | "punct"
11261            | "sentences" | "sents"
11262            | "paragraphs" | "paras" | "sections" | "sects"
11263            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
11264            | "trim" | "avg" | "stddev"
11265            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
11266            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
11267            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
11268            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
11269            | "to_html" | "to_markdown" | "to_table" | "xopen"
11270            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
11271            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
11272            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
11273            | "to_hash" | "to_set"
11274            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
11275            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
11276            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
11277            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
11278            | "inc" | "dec" | "elapsed"
11279            // ── filesystem extensions ───────────────────────────────────────
11280            | "files" | "filesf" | "fr" | "dirs" | "d" | "dr" | "sym_links"
11281            | "sockets" | "pipes" | "block_devices" | "char_devices"
11282            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
11283            | "copy" | "move" | "spurt" | "read_bytes" | "which"
11284            | "getcwd" | "touch" | "gethostname" | "uname"
11285            // ── data / network ──────────────────────────────────────────────
11286            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
11287            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
11288            | "par_fetch" | "par_csv_read" | "par_pipeline"
11289            | "json_encode" | "json_decode" | "json_jq"
11290            | "http_request" | "serve" | "ssh"
11291            | "html_parse" | "css_select" | "xml_parse" | "xpath"
11292            | "smtp_send"
11293            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
11294            | "net_public_ip" | "net_dns" | "net_reverse_dns"
11295            | "net_ping" | "net_port_open" | "net_ports_scan"
11296            | "net_latency" | "net_download" | "net_headers"
11297            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
11298            // ── git ─────────────────────────────────────────────────────────
11299            | "git_log" | "git_status" | "git_diff" | "git_branches"
11300            | "git_tags" | "git_blame" | "git_authors" | "git_files"
11301            | "git_show" | "git_root"
11302            // ── audio / media ───────────────────────────────────────────────
11303            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
11304            // ── pdf ─────────────────────────────────────────────────────────
11305            | "to_pdf" | "pdf_text" | "pdf_pages"
11306            // ── serialization (stryke-only encoders) ────────────────────────
11307            | "toml_encode" | "toml_decode"
11308            | "yaml_encode" | "yaml_decode"
11309            | "xml_encode" | "xml_decode"
11310            // ── crypto / encoding ───────────────────────────────────────────
11311            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
11312            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
11313            | "shake128" | "shake256"
11314            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
11315            | "uuid" | "crc32"
11316            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
11317            | "ripemd160" | "rmd160" | "md4"
11318            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
11319            | "murmur3" | "murmur3_32" | "murmur3_128"
11320            | "siphash" | "siphash_keyed"
11321            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
11322            | "poly1305" | "poly1305_mac"
11323            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
11324            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
11325            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
11326            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
11327            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
11328            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
11329            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
11330            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
11331            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
11332            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
11333            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
11334            | "secretbox" | "secretbox_seal" | "secretbox_open"
11335            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
11336            | "nacl_box_open" | "box_open"
11337            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
11338            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
11339            | "barcode_ean13" | "ean13" | "barcode_svg"
11340            | "argon2_hash" | "argon2" | "argon2_verify"
11341            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
11342            | "scrypt_hash" | "scrypt" | "scrypt_verify"
11343            | "pbkdf2" | "pbkdf2_derive"
11344            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
11345            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
11346            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
11347            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
11348            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
11349            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
11350            | "ecdsa_p256_verify" | "p256_verify"
11351            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
11352            | "ecdsa_p384_verify" | "p384_verify"
11353            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
11354            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
11355            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
11356            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
11357            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
11358            | "ed25519_verify" | "ed_verify"
11359            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
11360            | "base64_encode" | "base64_decode"
11361            | "hex_encode" | "hex_decode"
11362            | "url_encode" | "url_decode"
11363            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
11364            | "brotli" | "br" | "brotli_decode" | "ubr"
11365            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
11366            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
11367            | "lz4" | "lz4_decode" | "unlz4"
11368            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
11369            | "lzw" | "lzw_decode" | "unlzw"
11370            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
11371            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
11372            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
11373            // ── special math functions ────────────────────────────────────────
11374            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
11375            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
11376            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
11377            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
11378            | "gammaincc_reg" | "gamma_ur"
11379            // ── date / time ─────────────────────────────────────────────────
11380            | "datetime_utc" | "datetime_now_tz"
11381            | "datetime_format_tz" | "datetime_add_seconds"
11382            | "datetime_from_epoch"
11383            | "datetime_parse_rfc3339" | "datetime_parse_local"
11384            | "datetime_strftime"
11385            | "dateseq" | "dategrep" | "dateround" | "datesort"
11386            // ── jwt ─────────────────────────────────────────────────────────
11387            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
11388            // ── logging ─────────────────────────────────────────────────────
11389            | "log_info" | "log_warn" | "log_error"
11390            | "log_debug" | "log_trace" | "log_json" | "log_level"
11391            // ── concurrency / timing ────────────────────────────────────────
11392            | "async" | "spawn" | "trace" | "timer" | "bench"
11393            | "eval_timeout" | "retry" | "rate_limit" | "every"
11394            | "gen" | "watch"
11395            // ── testing framework ────────────────────────────────────────────
11396            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
11397            | "assert_true" | "assert_false"
11398            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
11399            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
11400            | "test_run"
11401            // ── system info ─────────────────────────────────────────────────
11402            | "mounts" | "du" | "du_tree" | "process_list"
11403            | "thread_count" | "pool_info" | "par_bench"
11404            // ── I/O extensions ──────────────────────────────────────────────
11405            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
11406            | "stdin"
11407            // ── internal ────────────────────────────────────────────────────
11408            | "__stryke_rust_compile"
11409            // ── short aliases ───────────────────────────────────────────────
11410            | "p" | "rev"
11411            // ── trivial numeric / predicate builtins ────────────────────────
11412            | "even" | "odd" | "zero" | "nonzero"
11413            | "positive" | "pos_n" | "negative" | "neg_n"
11414            | "sign" | "negate" | "double" | "triple" | "half"
11415            | "identity" | "id"
11416            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
11417            | "gcd" | "lcm" | "min2" | "max2"
11418            | "log2" | "log10" | "hypot"
11419            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
11420            | "pow2" | "abs_diff"
11421            | "factorial" | "fact" | "fibonacci" | "fib"
11422            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
11423            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
11424            | "median" | "mode_val" | "variance"
11425            // ── trivial string ops ──────────────────────────────────────────
11426            | "is_empty" | "is_blank" | "is_numeric"
11427            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
11428            | "is_space" | "is_whitespace"
11429            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
11430            | "capitalize" | "cap" | "swap_case" | "repeat"
11431            | "title_case" | "title" | "squish"
11432            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
11433            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
11434            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
11435            // ── trivial type predicates ─────────────────────────────────────
11436            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
11437            | "is_code" | "is_coderef" | "is_ref"
11438            | "is_undef" | "is_defined" | "is_def"
11439            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
11440            // ── hash helpers ────────────────────────────────────────────────
11441            | "invert" | "merge_hash"
11442            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
11443            // ── boolean combinators ─────────────────────────────────────────
11444            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
11445            // ── collection helpers (trivial) ────────────────────────────────
11446            | "riffle" | "intersperse" | "every_nth"
11447            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
11448            // ── base conversion ─────────────────────────────────────────────
11449            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
11450            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
11451            | "bits_count" | "popcount" | "leading_zeros" | "lz"
11452            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
11453            // ── bit ops ─────────────────────────────────────────────────────
11454            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
11455            | "shift_left" | "shl" | "shift_right" | "shr"
11456            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
11457            // ── unit conversions: temperature ───────────────────────────────
11458            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
11459            // ── unit conversions: distance ──────────────────────────────────
11460            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
11461            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
11462            | "yards_to_m" | "m_to_yards"
11463            // ── unit conversions: mass ──────────────────────────────────────
11464            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
11465            | "stone_to_kg" | "kg_to_stone"
11466            // ── unit conversions: digital ───────────────────────────────────
11467            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
11468            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
11469            | "kb_to_mb" | "mb_to_gb"
11470            | "bits_to_bytes" | "bytes_to_bits"
11471            // ── unit conversions: time ──────────────────────────────────────
11472            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
11473            | "seconds_to_hours" | "hours_to_seconds"
11474            | "seconds_to_days" | "days_to_seconds"
11475            | "minutes_to_hours" | "hours_to_minutes"
11476            | "hours_to_days" | "days_to_hours"
11477            // ── date helpers ────────────────────────────────────────────────
11478            | "is_leap_year" | "is_leap" | "days_in_month"
11479            | "month_name" | "month_short"
11480            | "weekday_name" | "weekday_short" | "quarter_of"
11481            // ── now / timestamp ─────────────────────────────────────────────
11482            | "now_ms" | "now_us" | "now_ns"
11483            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
11484            // ── color / ANSI ────────────────────────────────────────────────
11485            | "rgb_to_hex" | "hex_to_rgb"
11486            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
11487            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
11488            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
11489            | "strip_ansi"
11490            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
11491            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
11492            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
11493            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
11494            | "bright_magenta" | "bright_cyan" | "bright_white"
11495            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
11496            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
11497            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
11498            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
11499            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
11500            | "white_bold" | "bold_white"
11501            | "blink" | "rapid_blink" | "hidden" | "overline"
11502            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
11503            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
11504            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
11505            // ── network / validation ────────────────────────────────────────
11506            | "ipv4_to_int" | "int_to_ipv4"
11507            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
11508            // ── path helpers ────────────────────────────────────────────────
11509            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
11510            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
11511            // ── functional primitives ───────────────────────────────────────
11512            | "const_fn" | "always_true" | "always_false"
11513            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
11514            // ── more list helpers ───────────────────────────────────────────
11515            | "count_eq" | "count_ne" | "all_eq"
11516            | "all_distinct" | "all_unique" | "has_duplicates"
11517            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
11518            // ── string quote / escape ───────────────────────────────────────
11519            | "quote" | "single_quote" | "unquote"
11520            | "extract_between" | "ellipsis"
11521            // ── random ──────────────────────────────────────────────────────
11522            | "coin_flip" | "dice_roll"
11523            | "random_int" | "random_float" | "random_bool"
11524            | "random_choice" | "random_between"
11525            | "random_string" | "random_alpha" | "random_digit"
11526            // ── system introspection ────────────────────────────────────────
11527            | "os_name" | "os_arch" | "num_cpus"
11528            | "pid" | "ppid" | "uid" | "gid"
11529            | "username" | "home_dir" | "temp_dir"
11530            | "mem_total" | "mem_free" | "mem_used"
11531            | "swap_total" | "swap_free" | "swap_used"
11532            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
11533            | "load_avg" | "sys_uptime" | "page_size"
11534            | "os_version" | "os_family" | "endianness" | "pointer_width"
11535            | "proc_mem" | "rss"
11536            // ── collection more ─────────────────────────────────────────────
11537            | "transpose" | "unzip"
11538            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
11539            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
11540            // ── trig / math (batch 2) ───────────────────────────────────────
11541            | "tan" | "asin" | "acos" | "atan"
11542            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
11543            | "sqr" | "cube_fn"
11544            | "mod_op" | "ceil_div" | "floor_div"
11545            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
11546            | "degrees" | "radians"
11547            | "min_abs" | "max_abs"
11548            | "saturate" | "sat01" | "wrap_around"
11549            // ── string (batch 2) ────────────────────────────────────────────
11550            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
11551            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
11552            | "first_word" | "last_word"
11553            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
11554            | "lowercase" | "uppercase"
11555            | "pascal_case" | "pc_case"
11556            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
11557            | "is_palindrome" | "hamming_distance"
11558            | "longest_common_prefix" | "lcp"
11559            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
11560            | "replace_first" | "replace_all_str"
11561            | "contains_any" | "contains_all"
11562            | "starts_with_any" | "ends_with_any"
11563            // ── predicates (batch 2) ────────────────────────────────────────
11564            | "is_pair" | "is_triple"
11565            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
11566            | "is_empty_arr" | "is_empty_hash"
11567            | "is_subset" | "is_superset" | "is_permutation"
11568            // ── collection (batch 2) ────────────────────────────────────────
11569            | "first_eq" | "last_eq"
11570            | "index_of" | "last_index_of" | "positions_of"
11571            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
11572            | "distinct_count" | "longest" | "shortest"
11573            | "array_union" | "list_union"
11574            | "array_intersection" | "list_intersection"
11575            | "array_difference" | "list_difference"
11576            | "symmetric_diff" | "group_of_n" | "chunk_n"
11577            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
11578            // ── hash ops (batch 2) ──────────────────────────────────────────
11579            | "pick_keys" | "pick" | "omit_keys" | "omit"
11580            | "map_keys_fn" | "map_values_fn"
11581            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
11582            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
11583            // ── date (batch 2) ──────────────────────────────────────────────
11584            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
11585            // ── json helpers ────────────────────────────────────────────────
11586            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
11587            // ── process / env ───────────────────────────────────────────────
11588            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
11589            | "argc" | "script_name"
11590            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
11591            // ── id helpers ──────────────────────────────────────────────────
11592            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
11593            // ── url / email parts ───────────────────────────────────────────
11594            | "email_domain" | "email_local"
11595            | "url_host" | "url_path" | "url_query" | "url_scheme"
11596            // ── file stat / path ────────────────────────────────────────────
11597            | "file_size" | "fsize" | "file_mtime" | "mtime"
11598            | "file_atime" | "atime" | "file_ctime" | "ctime"
11599            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
11600            | "path_is_abs" | "path_is_rel"
11601            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
11602            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
11603            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
11604            | "reverse_list" | "list_reverse"
11605            | "without" | "without_nth" | "take_last" | "drop_last"
11606            | "pairwise" | "zipmap"
11607            | "format_bytes" | "human_bytes"
11608            | "format_duration" | "human_duration"
11609            | "format_number" | "group_number"
11610            | "format_percent" | "pad_number"
11611            | "spaceship" | "cmp_num" | "cmp_str"
11612            | "compare_versions" | "version_cmp"
11613            | "hash_insert" | "hash_update" | "hash_delete"
11614            | "matches_regex" | "re_match"
11615            | "count_regex_matches" | "regex_extract"
11616            | "regex_split_str" | "regex_replace_str"
11617            | "shuffle_chars" | "random_char" | "nth_word"
11618            | "head_lines" | "tail_lines" | "count_substring"
11619            | "is_valid_hex" | "hex_upper" | "hex_lower"
11620            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
11621            | "us_to_ns" | "ns_to_us"
11622            | "liters_to_gallons" | "gallons_to_liters"
11623            | "liters_to_ml" | "ml_to_liters"
11624            | "cups_to_ml" | "ml_to_cups"
11625            | "newtons_to_lbf" | "lbf_to_newtons"
11626            | "joules_to_cal" | "cal_to_joules"
11627            | "watts_to_hp" | "hp_to_watts"
11628            | "pascals_to_psi" | "psi_to_pascals"
11629            | "bar_to_pascals" | "pascals_to_bar"
11630            // ── algebraic match ─────────────────────────────────────────────
11631            | "match"
11632            // ── clojure stdlib (only names not matched above) ─────────────────
11633            | "fst" | "rest" | "rst" | "second" | "snd"
11634            | "last_clj" | "lastc" | "butlast" | "bl"
11635            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
11636            | "cons" | "conj"
11637            | "peek_clj" | "pkc" | "pop_clj" | "popc"
11638            | "some" | "not_any" | "not_every"
11639            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
11640            | "fnil" | "juxt"
11641            | "memoize" | "memo" | "curry" | "once"
11642            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
11643            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
11644            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
11645            | "reductions" | "rdcs"
11646            | "partition_by" | "pby" | "partition_all" | "pall"
11647            | "split_at" | "spat" | "split_with" | "spw"
11648            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
11649            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
11650            | "apply" | "appl"
11651            // ── python/ruby stdlib ───────────────────────────────────────────
11652            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
11653            | "zip_longest" | "zipl" | "combinations" | "comb" | "permutations" | "perm"
11654            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
11655            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
11656            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
11657            | "each_slice" | "eslice" | "each_cons" | "econs"
11658            | "one" | "none_match" | "nonem"
11659            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
11660            | "minmax" | "mmx" | "minmax_by" | "mmxb"
11661            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
11662            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
11663            | "sum_by" | "sumb" | "uniq_by" | "uqb"
11664            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
11665            | "step" | "upto" | "downto"
11666            // ── javascript array/object methods ─────────────────────────────
11667            | "find_last" | "fndl" | "find_last_index" | "fndli"
11668            | "at_index" | "ati" | "replace_at" | "repa"
11669            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
11670            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
11671            | "object_keys" | "okeys" | "object_values" | "ovals"
11672            | "object_entries" | "oents" | "object_from_entries" | "ofents"
11673            // ── haskell list functions ──────────────────────────────────────
11674            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
11675            | "nub" | "sort_on" | "srton"
11676            | "intersperse_val" | "isp" | "intercalate" | "ical"
11677            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
11678            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
11679            // ── rust iterator methods ───────────────────────────────────────
11680            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
11681            | "partition_either" | "peith" | "try_fold" | "tfld"
11682            | "map_while" | "mapw" | "inspect" | "insp"
11683            // ── ruby enumerable extras ──────────────────────────────────────
11684            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
11685            // ── go/general functional utilities ─────────────────────────────
11686            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
11687            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
11688            | "lines_from" | "lfrm" | "unlines" | "unlns"
11689            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
11690            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
11691            | "interpose" | "ipos" | "partition_n" | "partn"
11692            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
11693            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
11694            // ── additional missing stdlib functions ─────────────────────────
11695            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
11696            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
11697            | "each_with_object" | "ewo" | "reduce_right" | "redr"
11698            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
11699            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
11700            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
11701            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
11702            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
11703            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
11704            | "union_list" | "unionl" | "intersect_list" | "intl"
11705            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
11706            // ── Extended stdlib: Text Processing ─────────────────────────────
11707            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
11708            | "split_regex" | "splre" | "replace_regex" | "replre"
11709            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
11710            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
11711            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
11712            | "pluralize" | "plur" | "ordinalize" | "ordn"
11713            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
11714            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
11715            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
11716            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
11717            // ── Extended stdlib: Advanced Numeric ────────────────────────────
11718            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
11719            | "dot_product" | "dotp" | "cross_product" | "crossp"
11720            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
11721            | "distance" | "dist" | "manhattan_distance" | "mdist"
11722            | "covariance" | "cov" | "correlation" | "corr"
11723            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
11724            | "in_range" | "inrng" | "wrap_range" | "wrprng"
11725            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
11726            // ── Extended stdlib: Date/Time ───────────────────────────────────
11727            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
11728            | "diff_days" | "diffd" | "diff_hours" | "diffh"
11729            | "start_of_day" | "sod" | "end_of_day" | "eod"
11730            | "start_of_hour" | "soh" | "start_of_minute" | "som"
11731            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
11732            | "urle" | "urld"
11733            | "html_encode" | "htmle" | "html_decode" | "htmld"
11734            | "adler32" | "adl32" | "fnv1a" | "djb2"
11735            // ── Extended stdlib: Validation ──────────────────────────────────
11736            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
11737            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
11738            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
11739            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
11740            // ── Extended stdlib: Collection Advanced ─────────────────────────
11741            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
11742            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
11743            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
11744            | "partition_point" | "ppt" | "lower_bound" | "lbound"
11745            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
11746            // ── Extended stdlib: Matrix Operations ───────────────────────────
11747            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
11748            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
11749            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
11750            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
11751            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
11752            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
11753            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
11754            // ── Extended stdlib: Graph Algorithms ────────────────────────────
11755            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
11756            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
11757            | "connected_components_graph" | "ccgraph"
11758            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
11759            // ── Extended stdlib: Data Validation ─────────────────────────────
11760            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
11761            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
11762            | "is_hostname_valid" | "ishost"
11763            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
11764            | "is_iso_datetime" | "isisodtm"
11765            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
11766            // ── Extended stdlib: String Utilities Novel ──────────────────────
11767            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
11768            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
11769            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
11770            | "find_all_indices" | "fndalli"
11771            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
11772            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
11773            // ── Extended stdlib: Math Novel ──────────────────────────────────
11774            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
11775            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
11776            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
11777            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
11778            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
11779            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
11780            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
11781            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
11782            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
11783            | "longest_run" | "lrun" | "longest_increasing" | "linc"
11784            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
11785            | "majority_element" | "majority" | "kth_largest" | "kthl"
11786            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
11787            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
11788            // ── Extended stdlib batch 3: Set Operations ──────────────────────
11789            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
11790            | "overlap_coefficient" | "overlapcoef"
11791            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
11792            // ── Extended stdlib batch 3: Advanced String ─────────────────────
11793            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
11794            | "hamdist" | "jaro_similarity" | "jarosim"
11795            | "longest_common_substring" | "lcsub"
11796            | "longest_common_subsequence" | "lcseq"
11797            | "count_words" | "wcount" | "count_lines" | "lcount"
11798            | "count_chars" | "ccount" | "count_bytes" | "bcount"
11799            // ── Extended stdlib batch 3: More Math ───────────────────────────
11800            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
11801            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
11802            | "mobius" | "mob" | "is_squarefree" | "issqfr"
11803            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
11804            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
11805            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
11806            | "day_of_year" | "doy" | "week_of_year" | "woy"
11807            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
11808            | "age_in_years" | "ageyrs"
11809            // ── functional combinators ──────────────────────────────────────
11810
11811            | "when_true" | "when_false" | "if_else" | "clamp_fn"
11812            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
11813            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
11814            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
11815            | "coalesce" | "default_to" | "fallback"
11816            | "apply_list" | "zip_apply" | "scan"
11817            | "keep_if" | "reject_if" | "group_consecutive"
11818            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
11819
11820            // ── matrix / linear algebra ─────────────────────────────────────
11821
11822
11823            | "matrix_multiply" | "mat_mul"
11824            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
11825
11826
11827
11828            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
11829            | "linspace" | "arange"
11830            // ── more regex ──────────────────────────────────────────────────
11831            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
11832            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
11833            // ── more process / system ───────────────────────────────────────
11834            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
11835            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
11836            // ── data structure helpers ───────────────────────────────────────
11837            | "stack_new" | "queue_new" | "lru_new"
11838            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
11839            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
11840            // ── trivial numeric helpers (batch 4) ─────────────────────────────
11841            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
11842            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
11843            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
11844            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
11845            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
11846            // ── math / physics constants ──────────────────────────────────────
11847            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
11848            | "planck" | "speed_of_light" | "sqrt2"
11849            // ── physics formulas ──────────────────────────────────────────────
11850            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
11851            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
11852            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
11853            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
11854            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
11855            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
11856            // ── math functions ────────────────────────────────────────────────
11857            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
11858            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
11859            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
11860            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
11861            | "sigmoid" | "signum" | "square_root"
11862            // ── sequences ─────────────────────────────────────────────────────
11863            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
11864            | "squares_seq" | "triangular_seq"
11865            // ── string helpers (batch 4) ──────────────────────────────────────
11866            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
11867            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
11868            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
11869            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
11870            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
11871            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
11872            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
11873            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
11874            | "xor_strings"
11875            // ── list helpers (batch 4) ─────────────────────────────────────────
11876            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
11877            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
11878            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
11879            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
11880            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
11881            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
11882            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
11883            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
11884            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
11885            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
11886            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
11887            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
11888            | "wrap_index" | "digits_of"
11889            // ── predicates (batch 4) ──────────────────────────────────────────
11890            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
11891            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
11892            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
11893            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
11894            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
11895            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
11896            // ── counters (batch 4) ────────────────────────────────────────────
11897            | "count_digits" | "count_letters" | "count_lower" | "count_match"
11898            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
11899            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
11900            | "truthy_count" | "undef_count"
11901            // ── conversion / utility (batch 4) ────────────────────────────────
11902            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
11903            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
11904            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
11905            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
11906            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
11907            | "range_exclusive" | "range_inclusive"
11908            // ── math / numeric (uncategorized batch) ────────────────────────────
11909            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
11910            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
11911            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
11912            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
11913            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
11914            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
11915            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
11916            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
11917            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
11918            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
11919            | "tribonacci" | "weighted_mean" | "winsorize"
11920            // ── statistics (extended) ─────────────────────────────────────────
11921            | "chi_square_stat" | "describe" | "five_number_summary"
11922            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
11923            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
11924            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
11925            | "z_score" | "z_scores"
11926            // ── number theory / primes ──────────────────────────────────────────
11927            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
11928            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
11929            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
11930            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
11931            // ── geometry / physics ──────────────────────────────────────────────
11932            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
11933            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
11934            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
11935            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
11936            // ── geometry (extended) ───────────────────────────────────────────
11937            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
11938            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
11939            | "frustum_volume" | "haversine_distance" | "line_intersection"
11940            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
11941            | "reflect_point" | "scale_point" | "sector_area"
11942            | "torus_surface" | "torus_volume" | "translate_point"
11943            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
11944            // ── constants ───────────────────────────────────────────────────────
11945            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
11946            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
11947            | "sol" | "tau"
11948            // ── finance ─────────────────────────────────────────────────────────
11949            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
11950            // ── finance (extended) ────────────────────────────────────────────
11951            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
11952            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
11953            | "discounted_payback" | "duration" | "irr"
11954            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
11955            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
11956            | "wacc" | "xirr"
11957            // ── string processing (uncategorized batch) ─────────────────────────
11958            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
11959            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
11960            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
11961            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
11962            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
11963            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
11964            // ── encoding / phonetics ────────────────────────────────────────────
11965            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
11966            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
11967            | "to_emoji_num"
11968            // ── roman numerals ──────────────────────────────────────────────────
11969            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
11970            // ── base / gray code ────────────────────────────────────────────────
11971            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
11972            // ── color operations ────────────────────────────────────────────────
11973            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
11974            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
11975            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
11976            | "rgb_to_hsl" | "rgb_to_hsv"
11977            // ── matrix operations (uncategorized batch) ─────────────────────────
11978            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
11979            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
11980            | "matrix_transpose"
11981            // ── array / list operations (uncategorized batch) ───────────────────
11982            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
11983            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
11984            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
11985            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
11986            | "zero_crossings"
11987            // ── DSP / signal (extended) ───────────────────────────────────────
11988            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
11989            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
11990            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
11991            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
11992            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
11993            // ── validation predicates (uncategorized batch) ─────────────────────
11994            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
11995            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
11996            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
11997            // ── algorithms / puzzles ────────────────────────────────────────────
11998            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
11999            | "sierpinski" | "tower_of_hanoi" | "truth_table"
12000            // ── misc / utility ──────────────────────────────────────────────────
12001            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
12002            // ── math formulas ───────────────────────────────────────────────────
12003            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
12004            | "geometric_series" | "stirling_approx"
12005            | "double_factorial" | "rising_factorial" | "falling_factorial"
12006            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
12007            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
12008            | "map_range"
12009            // ── physics formulas ────────────────────────────────────────────────
12010            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
12011            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
12012            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
12013            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
12014            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
12015            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
12016            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
12017            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
12018            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
12019            | "projectile_range" | "projectile_max_height" | "projectile_time"
12020            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
12021            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
12022            | "lens_power" | "thin_lens" | "magnification_lens"
12023            // ── math constants ──────────────────────────────────────────────────
12024            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
12025            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
12026            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
12027            // ── physics constants ───────────────────────────────────────────────
12028            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
12029            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
12030            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
12031            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
12032            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
12033            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
12034            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
12035            // ── linear algebra (extended) ──────────────────────────────────
12036            | "matrix_solve" | "msolve" | "solve"
12037            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
12038            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
12039            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
12040            | "matrix_pinv" | "mpinv" | "pinv"
12041            | "matrix_cholesky" | "mchol" | "cholesky"
12042            | "matrix_det_general" | "mdetg" | "det"
12043            // ── statistics tests (extended) ────────────────────────────────
12044            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
12045            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
12046            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
12047            | "confidence_interval" | "ci"
12048            // ── distributions (extended) ──────────────────────────────────
12049            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
12050            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
12051            | "t_pdf" | "tpdf" | "student_pdf"
12052            | "f_pdf" | "fpdf" | "fisher_pdf"
12053            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
12054            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
12055            | "pareto_pdf" | "paretopdf"
12056            // ── interpolation & curve fitting ─────────────────────────────
12057            | "lagrange_interp" | "lagrange" | "linterp"
12058            | "cubic_spline" | "cspline" | "spline"
12059            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
12060            // ── numerical integration & differentiation ───────────────────
12061            | "trapz" | "trapezoid" | "simpson" | "simps"
12062            | "numerical_diff" | "numdiff" | "diff_array"
12063            | "cumtrapz" | "cumulative_trapz"
12064            // ── optimization / root finding ────────────────────────────────
12065            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
12066            | "golden_section" | "golden" | "gss"
12067            // ── ODE solvers ───────────────────────────────────────────────
12068            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
12069            // ── graph algorithms (extended) ────────────────────────────────
12070            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
12071            | "floyd_warshall" | "floydwarshall" | "apsp"
12072            | "prim_mst" | "mst" | "prim"
12073            // ── trig extensions ───────────────────────────────────────────
12074            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
12075            // ── ML activation functions ───────────────────────────────────
12076            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
12077            | "silu" | "swish" | "mish" | "softplus"
12078            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
12079            // ── special functions ─────────────────────────────────────────
12080            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
12081            | "lambert_w" | "lambertw" | "productlog"
12082            // ── number theory (extended) ──────────────────────────────────
12083            | "mod_exp" | "modexp" | "powmod"
12084            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
12085            | "miller_rabin" | "millerrabin" | "is_probable_prime"
12086            // ── combinatorics (extended) ──────────────────────────────────
12087            | "derangements" | "stirling2" | "stirling_second"
12088            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
12089            // ── physics (new) ─────────────────────────────────────────────
12090            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
12091            // ── financial greeks & risk ───────────────────────────────────
12092            | "bs_delta" | "bsdelta" | "option_delta"
12093            | "bs_gamma" | "bsgamma" | "option_gamma"
12094            | "bs_vega" | "bsvega" | "option_vega"
12095            | "bs_theta" | "bstheta" | "option_theta"
12096            | "bs_rho" | "bsrho" | "option_rho"
12097            | "bond_duration" | "mac_duration"
12098            // ── DSP extensions ────────────────────────────────────────────
12099            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
12100            // ── encoding extensions ───────────────────────────────────────
12101            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
12102            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
12103            // ── R base: distributions ─────────────────────────────────────
12104            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
12105            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
12106            // ── R base: matrix ops ────────────────────────────────────────
12107            | "rbind" | "cbind"
12108            | "row_sums" | "rowSums" | "col_sums" | "colSums"
12109            | "row_means" | "rowMeans" | "col_means" | "colMeans"
12110            | "outer_product" | "outer" | "crossprod" | "tcrossprod"
12111            | "nrow" | "ncol" | "prop_table" | "proptable"
12112            // ── R base: vector ops ────────────────────────────────────────
12113            | "cummax" | "cummin" | "scale_vec" | "scale"
12114            | "which_fn" | "tabulate"
12115            | "duplicated" | "duped" | "rev_vec"
12116            | "seq_fn" | "rep_fn" | "rep"
12117            | "cut_bins" | "cut" | "find_interval" | "findInterval"
12118            | "ecdf_fn" | "ecdf" | "density_est" | "density"
12119            | "embed_ts" | "embed"
12120            // ── R base: stats tests ───────────────────────────────────────
12121            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
12122            | "wilcox_test" | "wilcox" | "mann_whitney"
12123            | "prop_test" | "proptest" | "binom_test" | "binomtest"
12124            // ── R base: apply / functional ────────────────────────────────
12125            | "sapply" | "tapply" | "do_call" | "docall"
12126            // ── R base: ML / clustering ───────────────────────────────────
12127            | "kmeans" | "prcomp" | "pca"
12128            // ── R base: random generators ─────────────────────────────────
12129            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
12130            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
12131            | "rweibull" | "rlnorm" | "rcauchy"
12132            // ── R base: quantile functions ────────────────────────────────
12133            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
12134            // ── R base: additional CDFs ───────────────────────────────────
12135            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
12136            // ── R base: additional PMFs ───────────────────────────────────
12137            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
12138            // ── R base: smoothing / interpolation ─────────────────────────
12139            | "lowess" | "loess" | "approx_fn" | "approx"
12140            // ── R base: linear models ─────────────────────────────────────
12141            | "lm_fit" | "lm"
12142            // ── R base: remaining quantiles ───────────────────────────────
12143            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
12144            | "qbinom" | "qpois"
12145            // ── R base: time series ───────────────────────────────────────
12146            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
12147            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
12148            // ── R base: regression diagnostics ────────────────────────────
12149            | "predict_lm" | "predict" | "confint_lm" | "confint"
12150            // ── R base: multivariate stats ────────────────────────────────
12151            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
12152            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
12153            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
12154            // ── SVG plotting ──────────────────────────────────────────────
12155            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
12156            | "plot_svg" | "hist_svg" | "histogram_svg"
12157            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
12158            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
12159            | "donut_svg" | "donut" | "area_svg" | "area_chart"
12160            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
12161            | "candlestick_svg" | "candlestick" | "ohlc"
12162            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
12163            | "stacked_bar_svg" | "stacked_bar"
12164            | "wordcloud_svg" | "wordcloud" | "wcloud"
12165            | "treemap_svg" | "treemap"
12166            | "pvw"
12167            // ── Cyberpunk terminal art ────────────────────────────────
12168            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
12169            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
12170            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
12171            => Some(name),
12172            _ => None,
12173        }
12174    }
12175
12176    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
12177    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
12178    /// Also accepts a bare function name: `psort my_cmp, @list`.
12179    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
12180        if matches!(self.peek(), Token::LBrace) {
12181            return self.parse_block();
12182        }
12183        let line = self.peek_line();
12184        // Bare sub name: `psort my_cmp, @list`
12185        if let Token::Ident(ref name) = self.peek().clone() {
12186            if matches!(
12187                self.peek_at(1),
12188                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12189            ) {
12190                let name = name.clone();
12191                self.advance();
12192                let body = Expr {
12193                    kind: ExprKind::FuncCall {
12194                        name,
12195                        args: vec![
12196                            Expr {
12197                                kind: ExprKind::ScalarVar("a".to_string()),
12198                                line,
12199                            },
12200                            Expr {
12201                                kind: ExprKind::ScalarVar("b".to_string()),
12202                                line,
12203                            },
12204                        ],
12205                    },
12206                    line,
12207                };
12208                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12209            }
12210        }
12211        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
12212        let expr = self.parse_assign_expr_stop_at_pipe()?;
12213        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12214    }
12215
12216    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
12217    fn parse_fan_optional_progress(
12218        &mut self,
12219        which: &'static str,
12220    ) -> PerlResult<Option<Box<Expr>>> {
12221        let line = self.peek_line();
12222        if self.eat(&Token::Comma) {
12223            match self.peek() {
12224                Token::Ident(ref kw)
12225                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
12226                {
12227                    self.advance();
12228                    self.expect(&Token::FatArrow)?;
12229                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
12230                }
12231                _ => {
12232                    return Err(self.syntax_err(
12233                        format!("{which}: expected `progress => EXPR` after comma"),
12234                        line,
12235                    ));
12236                }
12237            }
12238        }
12239        if let Token::Ident(ref kw) = self.peek().clone() {
12240            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12241                self.advance();
12242                self.expect(&Token::FatArrow)?;
12243                return Ok(Some(Box::new(self.parse_assign_expr()?)));
12244            }
12245        }
12246        Ok(None)
12247    }
12248
12249    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
12250    /// (for `pmap_chunked`, `psort`, etc.).
12251    ///
12252    /// Paren-less — individual parts parse through
12253    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
12254    /// the enclosing pipe-forward loop (left-associative chaining).
12255    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
12256        // On the RHS of `|>`, list-taking builtins may be written bare with no
12257        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
12258        // When the next token is a list-terminator, yield an empty placeholder
12259        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
12260        // desugar time, so the placeholder is never evaluated.
12261        if self.in_pipe_rhs()
12262            && matches!(
12263                self.peek(),
12264                Token::Semicolon
12265                    | Token::RBrace
12266                    | Token::RParen
12267                    | Token::Eof
12268                    | Token::PipeForward
12269                    | Token::Comma
12270            )
12271        {
12272            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
12273        }
12274        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
12275        loop {
12276            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12277                break;
12278            }
12279            if matches!(
12280                self.peek(),
12281                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12282            ) {
12283                break;
12284            }
12285            if self.peek_is_postfix_stmt_modifier_keyword() {
12286                break;
12287            }
12288            if let Token::Ident(ref kw) = self.peek().clone() {
12289                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12290                    self.advance();
12291                    self.expect(&Token::FatArrow)?;
12292                    let prog = self.parse_assign_expr_stop_at_pipe()?;
12293                    return Ok((merge_expr_list(parts), Some(prog)));
12294                }
12295            }
12296            parts.push(self.parse_assign_expr_stop_at_pipe()?);
12297        }
12298        Ok((merge_expr_list(parts), None))
12299    }
12300
12301    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
12302        if matches!(self.peek(), Token::LParen) {
12303            self.advance();
12304            let expr = self.parse_expression()?;
12305            self.expect(&Token::RParen)?;
12306            Ok(expr)
12307        } else {
12308            self.parse_assign_expr_stop_at_pipe()
12309        }
12310    }
12311
12312    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
12313        // Default to `$_` when the next token cannot start an argument expression
12314        // because it has lower precedence than a named unary operator. Perl 5
12315        // named unary precedence sits above ternary / comparison / logical / bitwise
12316        // / assignment / list ops; everything below should terminate the implicit
12317        // argument and let the surrounding expression continue.
12318        // See `perldoc perlop` ("Named Unary Operators").
12319        if matches!(
12320            self.peek(),
12321            // Statement / list / call boundaries
12322            Token::Semicolon
12323                | Token::RBrace
12324                | Token::RParen
12325                | Token::RBracket
12326                | Token::Eof
12327                | Token::Comma
12328                | Token::FatArrow
12329                | Token::PipeForward
12330            // Ternary `? :`
12331                | Token::Question
12332                | Token::Colon
12333            // Comparison / equality (numeric + string)
12334                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
12335                | Token::NumLe | Token::NumGe | Token::Spaceship
12336                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
12337                | Token::StrLe | Token::StrGe | Token::StrCmp
12338            // Logical (symbolic and word forms) + defined-or
12339                | Token::LogAnd | Token::LogOr | Token::LogNot
12340                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
12341                | Token::DefinedOr
12342            // Range (lower precedence than named unary)
12343                | Token::Range | Token::RangeExclusive
12344            // Assignment (any compound form)
12345                | Token::Assign | Token::PlusAssign | Token::MinusAssign
12346                | Token::MulAssign | Token::DivAssign | Token::ModAssign
12347                | Token::PowAssign | Token::DotAssign | Token::AndAssign
12348                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
12349                | Token::ShiftLeftAssign | Token::ShiftRightAssign
12350                | Token::BitAndAssign | Token::BitOrAssign
12351        ) {
12352            return Ok(Expr {
12353                kind: ExprKind::ScalarVar("_".into()),
12354                line: self.peek_line(),
12355            });
12356        }
12357        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
12358        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
12359        // Perl accepts both `length` and `length()` as `length($_)`.
12360        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
12361            let line = self.peek_line();
12362            self.advance(); // (
12363            self.advance(); // )
12364            return Ok(Expr {
12365                kind: ExprKind::ScalarVar("_".into()),
12366                line,
12367            });
12368        }
12369        self.parse_one_arg()
12370    }
12371
12372    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
12373    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
12374        let line = self.prev_line(); // line where shift/pop keyword was
12375        if matches!(self.peek(), Token::LParen) {
12376            self.advance();
12377            if matches!(self.peek(), Token::RParen) {
12378                self.advance();
12379                return Ok(Expr {
12380                    kind: ExprKind::ArrayVar("_".into()),
12381                    line: self.peek_line(),
12382                });
12383            }
12384            let expr = self.parse_expression()?;
12385            self.expect(&Token::RParen)?;
12386            return Ok(expr);
12387        }
12388        // Implicit semicolon: if next token is on a different line, don't consume it
12389        if matches!(
12390            self.peek(),
12391            Token::Semicolon
12392                | Token::RBrace
12393                | Token::RParen
12394                | Token::Eof
12395                | Token::Comma
12396                | Token::PipeForward
12397        ) || self.peek_line() > line
12398        {
12399            Ok(Expr {
12400                kind: ExprKind::ArrayVar("_".into()),
12401                line,
12402            })
12403        } else {
12404            self.parse_assign_expr()
12405        }
12406    }
12407
12408    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
12409        if matches!(self.peek(), Token::LParen) {
12410            self.advance();
12411            let args = self.parse_arg_list()?;
12412            self.expect(&Token::RParen)?;
12413            Ok(args)
12414        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
12415            // In thread context, don't consume barewords as arguments
12416            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
12417            Ok(vec![])
12418        } else {
12419            self.parse_list_until_terminator()
12420        }
12421    }
12422
12423    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
12424    /// should be treated as an auto-quoted string (hash key), not a function call.
12425    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
12426    #[inline]
12427    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
12428        if matches!(self.peek(), Token::FatArrow) {
12429            Some(Expr {
12430                kind: ExprKind::String(name.to_string()),
12431                line,
12432            })
12433        } else {
12434            None
12435        }
12436    }
12437
12438    /// Parse a hash subscript key inside `{…}`.
12439    ///
12440    /// Perl auto-quotes a single bareword before `}`, even for keywords:
12441    /// `$h{print}`, `$r->{f}` etc. all yield the string key.
12442    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
12443        let line = self.peek_line();
12444        if let Token::Ident(ref k) = self.peek().clone() {
12445            if matches!(self.peek_at(1), Token::RBrace) {
12446                let s = k.clone();
12447                self.advance();
12448                return Ok(Expr {
12449                    kind: ExprKind::String(s),
12450                    line,
12451                });
12452            }
12453        }
12454        self.parse_expression()
12455    }
12456
12457    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
12458    #[inline]
12459    fn peek_is_glob_par_progress_kw(&self) -> bool {
12460        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
12461            && matches!(self.peek_at(1), Token::FatArrow)
12462    }
12463
12464    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
12465    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
12466        let mut args = Vec::new();
12467        loop {
12468            if matches!(self.peek(), Token::RParen | Token::Eof) {
12469                break;
12470            }
12471            if self.peek_is_glob_par_progress_kw() {
12472                break;
12473            }
12474            args.push(self.parse_assign_expr()?);
12475            match self.peek() {
12476                Token::RParen => break,
12477                Token::Comma => {
12478                    self.advance();
12479                    if matches!(self.peek(), Token::RParen) {
12480                        break;
12481                    }
12482                    if self.peek_is_glob_par_progress_kw() {
12483                        break;
12484                    }
12485                }
12486                _ => {
12487                    return Err(self.syntax_err(
12488                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
12489                        self.peek_line(),
12490                    ));
12491                }
12492            }
12493        }
12494        Ok(args)
12495    }
12496
12497    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
12498    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
12499        let mut args = Vec::new();
12500        loop {
12501            if matches!(
12502                self.peek(),
12503                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12504            ) {
12505                break;
12506            }
12507            if self.peek_is_postfix_stmt_modifier_keyword() {
12508                break;
12509            }
12510            if self.peek_is_glob_par_progress_kw() {
12511                break;
12512            }
12513            args.push(self.parse_assign_expr()?);
12514            if !self.eat(&Token::Comma) {
12515                break;
12516            }
12517            if self.peek_is_glob_par_progress_kw() {
12518                break;
12519            }
12520        }
12521        Ok(args)
12522    }
12523
12524    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
12525    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
12526        if matches!(self.peek(), Token::LParen) {
12527            self.advance();
12528            let args = self.parse_pattern_list_until_rparen_or_progress()?;
12529            let progress = if self.peek_is_glob_par_progress_kw() {
12530                self.advance();
12531                self.expect(&Token::FatArrow)?;
12532                Some(Box::new(self.parse_assign_expr()?))
12533            } else {
12534                None
12535            };
12536            self.expect(&Token::RParen)?;
12537            Ok((args, progress))
12538        } else {
12539            let args = self.parse_pattern_list_glob_par_bare()?;
12540            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
12541            let progress = if self.peek_is_glob_par_progress_kw() {
12542                self.advance();
12543                self.expect(&Token::FatArrow)?;
12544                Some(Box::new(self.parse_assign_expr()?))
12545            } else {
12546                None
12547            };
12548            Ok((args, progress))
12549        }
12550    }
12551
12552    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
12553        let mut args = Vec::new();
12554        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
12555        // so shadow any outer paren-less-arg suppression from
12556        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
12557        let saved_no_pf = self.no_pipe_forward_depth;
12558        self.no_pipe_forward_depth = 0;
12559        while !matches!(
12560            self.peek(),
12561            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
12562        ) {
12563            let arg = match self.parse_assign_expr() {
12564                Ok(e) => e,
12565                Err(err) => {
12566                    self.no_pipe_forward_depth = saved_no_pf;
12567                    return Err(err);
12568                }
12569            };
12570            args.push(arg);
12571            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12572                break;
12573            }
12574        }
12575        self.no_pipe_forward_depth = saved_no_pf;
12576        Ok(args)
12577    }
12578
12579    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
12580    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
12581    /// no-arg method call; we must not consume that `+` as the start of a first argument.
12582    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
12583        let mut args = Vec::new();
12584        let call_line = self.prev_line();
12585        loop {
12586            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
12587            // hash argument to `next` (paren-less method call has no args here).
12588            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
12589                break;
12590            }
12591            if matches!(
12592                self.peek(),
12593                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12594            ) {
12595                break;
12596            }
12597            if let Token::Ident(ref kw) = self.peek().clone() {
12598                if matches!(
12599                    kw.as_str(),
12600                    "if" | "unless" | "while" | "until" | "for" | "foreach"
12601                ) {
12602                    break;
12603                }
12604            }
12605            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
12606            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
12607            if args.is_empty()
12608                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
12609            {
12610                break;
12611            }
12612            // Implicit semicolon: if no args collected yet and next token is on a different
12613            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
12614            if args.is_empty() && self.peek_line() > call_line {
12615                break;
12616            }
12617            args.push(self.parse_assign_expr()?);
12618            if !self.eat(&Token::Comma) {
12619                break;
12620            }
12621        }
12622        Ok(args)
12623    }
12624
12625    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
12626    /// the whole `->meth` expression).
12627    fn peek_method_arg_infix_terminator(&self) -> bool {
12628        matches!(
12629            self.peek(),
12630            Token::Plus
12631                | Token::Minus
12632                | Token::Star
12633                | Token::Slash
12634                | Token::Percent
12635                | Token::Power
12636                | Token::Dot
12637                | Token::X
12638                | Token::NumEq
12639                | Token::NumNe
12640                | Token::NumLt
12641                | Token::NumGt
12642                | Token::NumLe
12643                | Token::NumGe
12644                | Token::Spaceship
12645                | Token::StrEq
12646                | Token::StrNe
12647                | Token::StrLt
12648                | Token::StrGt
12649                | Token::StrLe
12650                | Token::StrGe
12651                | Token::StrCmp
12652                | Token::LogAnd
12653                | Token::LogOr
12654                | Token::LogAndWord
12655                | Token::LogOrWord
12656                | Token::DefinedOr
12657                | Token::BitAnd
12658                | Token::BitOr
12659                | Token::BitXor
12660                | Token::ShiftLeft
12661                | Token::ShiftRight
12662                | Token::Range
12663                | Token::RangeExclusive
12664                | Token::BindMatch
12665                | Token::BindNotMatch
12666                | Token::Arrow
12667                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
12668                | Token::Question
12669                | Token::Colon
12670        )
12671    }
12672
12673    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
12674        let mut args = Vec::new();
12675        // Line of the last consumed token (the keyword / function name that
12676        // triggered this arg parse).  Used for implicit-semicolon: if no args
12677        // have been parsed yet and the next token is on a *different* line,
12678        // treat the newline as a statement boundary and stop.
12679        let call_line = self.prev_line();
12680        loop {
12681            if matches!(
12682                self.peek(),
12683                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12684            ) {
12685                break;
12686            }
12687            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
12688            if let Token::Ident(ref kw) = self.peek().clone() {
12689                if matches!(
12690                    kw.as_str(),
12691                    "if" | "unless" | "while" | "until" | "for" | "foreach"
12692                ) {
12693                    break;
12694                }
12695            }
12696            // Implicit semicolons: if no args have been collected yet and the
12697            // next token is on a different line from the call keyword, treat
12698            // the newline as a statement boundary.  This prevents paren-less
12699            // calls (`say`, `print`, user subs) from greedily swallowing the
12700            // *next* statement when the author omitted a semicolon.
12701            // After a comma continuation, multi-line arg lists still work.
12702            if args.is_empty() && self.peek_line() > call_line {
12703                break;
12704            }
12705            // Paren-less builtin args: `|>` terminates the whole call list, so
12706            // individual args must not absorb a following `|>`.
12707            args.push(self.parse_assign_expr_stop_at_pipe()?);
12708            if !self.eat(&Token::Comma) {
12709                break;
12710            }
12711        }
12712        Ok(args)
12713    }
12714
12715    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
12716        let mut pairs = Vec::new();
12717        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
12718            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
12719            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
12720            let line = self.peek_line();
12721            let key = if let Token::Ident(ref name) = self.peek().clone() {
12722                if matches!(self.peek_at(1), Token::FatArrow) {
12723                    self.advance();
12724                    Expr {
12725                        kind: ExprKind::String(name.clone()),
12726                        line,
12727                    }
12728                } else {
12729                    self.parse_assign_expr()?
12730                }
12731            } else {
12732                self.parse_assign_expr()?
12733            };
12734            // If the key expression is a hash/array variable and is followed by `}` or `,`
12735            // with no `=>`, treat the whole thing as a hash-from-expression construction.
12736            // This handles `{ %a }`, `{ %a, key => val }`, etc.
12737            if matches!(self.peek(), Token::RBrace | Token::Comma)
12738                && matches!(
12739                    key.kind,
12740                    ExprKind::HashVar(_)
12741                        | ExprKind::Deref {
12742                            kind: Sigil::Hash,
12743                            ..
12744                        }
12745                )
12746            {
12747                // Synthesize a pair whose key/value is spread from the hash expression.
12748                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
12749                // The evaluator will flatten this.
12750                let sentinel_key = Expr {
12751                    kind: ExprKind::String("__HASH_SPREAD__".into()),
12752                    line,
12753                };
12754                pairs.push((sentinel_key, key));
12755                self.eat(&Token::Comma);
12756                continue;
12757            }
12758            // Expect => or , after key
12759            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
12760                let val = self.parse_assign_expr()?;
12761                pairs.push((key, val));
12762                self.eat(&Token::Comma);
12763            } else {
12764                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
12765            }
12766        }
12767        self.expect(&Token::RBrace)?;
12768        Ok(pairs)
12769    }
12770
12771    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
12772    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
12773    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
12774    /// navigates. Caller expects and consumes `term` itself.
12775    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
12776        let mut pairs = Vec::new();
12777        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
12778            && !matches!(self.peek(), Token::Eof)
12779        {
12780            let line = self.peek_line();
12781            let key = if let Token::Ident(ref name) = self.peek().clone() {
12782                if matches!(self.peek_at(1), Token::FatArrow) {
12783                    self.advance();
12784                    Expr {
12785                        kind: ExprKind::String(name.clone()),
12786                        line,
12787                    }
12788                } else {
12789                    self.parse_assign_expr()?
12790                }
12791            } else {
12792                self.parse_assign_expr()?
12793            };
12794            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
12795                let val = self.parse_assign_expr()?;
12796                pairs.push((key, val));
12797                self.eat(&Token::Comma);
12798            } else {
12799                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
12800            }
12801        }
12802        Ok(pairs)
12803    }
12804
12805    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
12806    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
12807    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
12808    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
12809    /// Each step wraps the current expression in an `ArrowDeref`.
12810    fn interp_chain_subscripts(
12811        &self,
12812        chars: &[char],
12813        i: &mut usize,
12814        mut base: Expr,
12815        line: usize,
12816    ) -> Expr {
12817        loop {
12818            // Optional `->` connector
12819            let (after, requires_subscript) =
12820                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
12821                    (*i + 2, true)
12822                } else {
12823                    (*i, false)
12824                };
12825            if after >= chars.len() {
12826                break;
12827            }
12828            match chars[after] {
12829                '[' => {
12830                    *i = after + 1;
12831                    let mut idx_str = String::new();
12832                    while *i < chars.len() && chars[*i] != ']' {
12833                        idx_str.push(chars[*i]);
12834                        *i += 1;
12835                    }
12836                    if *i < chars.len() {
12837                        *i += 1;
12838                    }
12839                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
12840                        Expr {
12841                            kind: ExprKind::ScalarVar(rest.to_string()),
12842                            line,
12843                        }
12844                    } else if let Ok(n) = idx_str.parse::<i64>() {
12845                        Expr {
12846                            kind: ExprKind::Integer(n),
12847                            line,
12848                        }
12849                    } else {
12850                        Expr {
12851                            kind: ExprKind::String(idx_str),
12852                            line,
12853                        }
12854                    };
12855                    base = Expr {
12856                        kind: ExprKind::ArrowDeref {
12857                            expr: Box::new(base),
12858                            index: Box::new(idx_expr),
12859                            kind: DerefKind::Array,
12860                        },
12861                        line,
12862                    };
12863                }
12864                '{' => {
12865                    *i = after + 1;
12866                    let mut key = String::new();
12867                    let mut depth = 1usize;
12868                    while *i < chars.len() && depth > 0 {
12869                        if chars[*i] == '{' {
12870                            depth += 1;
12871                        } else if chars[*i] == '}' {
12872                            depth -= 1;
12873                            if depth == 0 {
12874                                break;
12875                            }
12876                        }
12877                        key.push(chars[*i]);
12878                        *i += 1;
12879                    }
12880                    if *i < chars.len() {
12881                        *i += 1;
12882                    }
12883                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
12884                        Expr {
12885                            kind: ExprKind::ScalarVar(rest.to_string()),
12886                            line,
12887                        }
12888                    } else {
12889                        Expr {
12890                            kind: ExprKind::String(key),
12891                            line,
12892                        }
12893                    };
12894                    base = Expr {
12895                        kind: ExprKind::ArrowDeref {
12896                            expr: Box::new(base),
12897                            index: Box::new(key_expr),
12898                            kind: DerefKind::Hash,
12899                        },
12900                        line,
12901                    };
12902                }
12903                _ => {
12904                    if requires_subscript {
12905                        // `->method()` etc — not interpolated, leave for literal output.
12906                    }
12907                    break;
12908                }
12909            }
12910        }
12911        base
12912    }
12913
12914    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
12915        // Parse $var and @var inside double-quoted strings
12916        let mut parts = Vec::new();
12917        let mut literal = String::new();
12918        let chars: Vec<char> = s.chars().collect();
12919        let mut i = 0;
12920
12921        'istr: while i < chars.len() {
12922            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
12923                literal.push('$');
12924                i += 1;
12925                continue;
12926            }
12927            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
12928            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
12929                literal.push('\\');
12930                i += 1;
12931                // i now points at '$' — fall through to $ handling below
12932            }
12933            if chars[i] == '$' && i + 1 < chars.len() {
12934                if !literal.is_empty() {
12935                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
12936                }
12937                i += 1; // past `$`
12938                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
12939                while i < chars.len() && chars[i].is_whitespace() {
12940                    i += 1;
12941                }
12942                if i >= chars.len() {
12943                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
12944                }
12945                // `$#name` — last index of `@name` (Perl `$#array`).
12946                if chars[i] == '#' {
12947                    i += 1;
12948                    let mut sname = String::from("#");
12949                    while i < chars.len()
12950                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
12951                    {
12952                        sname.push(chars[i]);
12953                        i += 1;
12954                    }
12955                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
12956                        sname.push_str("::");
12957                        i += 2;
12958                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
12959                            sname.push(chars[i]);
12960                            i += 1;
12961                        }
12962                    }
12963                    parts.push(StringPart::ScalarVar(sname));
12964                    continue;
12965                }
12966                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
12967                // between) and the second `$` is not followed by a word character or digit (`$$x`
12968                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
12969                if chars[i] == '$' {
12970                    let next_c = chars.get(i + 1).copied();
12971                    let is_pid = match next_c {
12972                        None => true,
12973                        Some(c)
12974                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
12975                        {
12976                            true
12977                        }
12978                        _ => false,
12979                    };
12980                    if is_pid {
12981                        parts.push(StringPart::ScalarVar("$$".to_string()));
12982                        i += 1; // consume second `$`
12983                        continue;
12984                    }
12985                    i += 1; // skip second `$` — same as a single `$` before the identifier
12986                }
12987                if chars[i] == '{' {
12988                    // `${…}` — braced variable OR expression interpolation.
12989                    //   `${name}`              → ScalarVar(name)        (Perl standard)
12990                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
12991                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
12992                    // stryke's prior `#{expr}` form remains supported elsewhere.
12993                    i += 1;
12994                    let mut inner = String::new();
12995                    let mut depth = 1usize;
12996                    while i < chars.len() && depth > 0 {
12997                        match chars[i] {
12998                            '{' => depth += 1,
12999                            '}' => {
13000                                depth -= 1;
13001                                if depth == 0 {
13002                                    break;
13003                                }
13004                            }
13005                            _ => {}
13006                        }
13007                        inner.push(chars[i]);
13008                        i += 1;
13009                    }
13010                    if i < chars.len() {
13011                        i += 1; // skip closing }
13012                    }
13013
13014                    // Distinguish "name" from "expression". If trimmed inner starts with
13015                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
13016                    // expression and emit a scalar deref. Otherwise, plain variable name.
13017                    let trimmed = inner.trim();
13018                    let is_expr = trimmed.starts_with('$')
13019                        || trimmed.starts_with('\\')
13020                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
13021                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
13022                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
13023                    let mut base: Expr = if is_expr {
13024                        // Re-parse the inner content as a Perl expression. Wrap in
13025                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
13026                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
13027                        match parse_expression_from_str(trimmed, "<interp>") {
13028                            Ok(e) => Expr {
13029                                kind: ExprKind::Deref {
13030                                    expr: Box::new(e),
13031                                    kind: Sigil::Scalar,
13032                                },
13033                                line,
13034                            },
13035                            Err(_) => Expr {
13036                                kind: ExprKind::ScalarVar(inner.clone()),
13037                                line,
13038                            },
13039                        }
13040                    } else {
13041                        // Treat as a plain (possibly qualified) variable name.
13042                        Expr {
13043                            kind: ExprKind::ScalarVar(inner),
13044                            line,
13045                        }
13046                    };
13047
13048                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
13049                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
13050                    // chains thereafter.
13051                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13052                    parts.push(StringPart::Expr(base));
13053                } else if chars[i] == '^' {
13054                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
13055                    let mut name = String::from("^");
13056                    i += 1;
13057                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13058                        name.push(chars[i]);
13059                        i += 1;
13060                    }
13061                    if i < chars.len() && chars[i] == '{' {
13062                        i += 1; // skip {
13063                        let mut key = String::new();
13064                        let mut depth = 1;
13065                        while i < chars.len() && depth > 0 {
13066                            if chars[i] == '{' {
13067                                depth += 1;
13068                            } else if chars[i] == '}' {
13069                                depth -= 1;
13070                                if depth == 0 {
13071                                    break;
13072                                }
13073                            }
13074                            key.push(chars[i]);
13075                            i += 1;
13076                        }
13077                        if i < chars.len() {
13078                            i += 1;
13079                        }
13080                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
13081                            Expr {
13082                                kind: ExprKind::ScalarVar(rest.to_string()),
13083                                line,
13084                            }
13085                        } else {
13086                            Expr {
13087                                kind: ExprKind::String(key),
13088                                line,
13089                            }
13090                        };
13091                        parts.push(StringPart::Expr(Expr {
13092                            kind: ExprKind::HashElement {
13093                                hash: name,
13094                                key: Box::new(key_expr),
13095                            },
13096                            line,
13097                        }));
13098                    } else if i < chars.len() && chars[i] == '[' {
13099                        i += 1;
13100                        let mut idx_str = String::new();
13101                        while i < chars.len() && chars[i] != ']' {
13102                            idx_str.push(chars[i]);
13103                            i += 1;
13104                        }
13105                        if i < chars.len() {
13106                            i += 1;
13107                        }
13108                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
13109                            Expr {
13110                                kind: ExprKind::ScalarVar(rest.to_string()),
13111                                line,
13112                            }
13113                        } else if let Ok(n) = idx_str.parse::<i64>() {
13114                            Expr {
13115                                kind: ExprKind::Integer(n),
13116                                line,
13117                            }
13118                        } else {
13119                            Expr {
13120                                kind: ExprKind::String(idx_str),
13121                                line,
13122                            }
13123                        };
13124                        parts.push(StringPart::Expr(Expr {
13125                            kind: ExprKind::ArrayElement {
13126                                array: name,
13127                                index: Box::new(idx_expr),
13128                            },
13129                            line,
13130                        }));
13131                    } else {
13132                        parts.push(StringPart::ScalarVar(name));
13133                    }
13134                } else if chars[i].is_alphabetic() || chars[i] == '_' {
13135                    let mut name = String::new();
13136                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13137                        name.push(chars[i]);
13138                        i += 1;
13139                    }
13140                    // `$_<`, `$_<<`, … — outer topic (stryke extension); only for bare `_`.
13141                    if name == "_" {
13142                        while i < chars.len() && chars[i] == '<' {
13143                            name.push('<');
13144                            i += 1;
13145                        }
13146                    }
13147                    // Build the base expression, then thread arrow-deref chains
13148                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
13149                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
13150                    // correctly inside double-quoted strings (Perl convention).
13151                    let mut base = if i < chars.len() && chars[i] == '{' {
13152                        // $hash{key}
13153                        i += 1; // skip {
13154                        let mut key = String::new();
13155                        let mut depth = 1;
13156                        while i < chars.len() && depth > 0 {
13157                            if chars[i] == '{' {
13158                                depth += 1;
13159                            } else if chars[i] == '}' {
13160                                depth -= 1;
13161                                if depth == 0 {
13162                                    break;
13163                                }
13164                            }
13165                            key.push(chars[i]);
13166                            i += 1;
13167                        }
13168                        if i < chars.len() {
13169                            i += 1;
13170                        } // skip }
13171                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
13172                            Expr {
13173                                kind: ExprKind::ScalarVar(rest.to_string()),
13174                                line,
13175                            }
13176                        } else {
13177                            Expr {
13178                                kind: ExprKind::String(key),
13179                                line,
13180                            }
13181                        };
13182                        Expr {
13183                            kind: ExprKind::HashElement {
13184                                hash: name,
13185                                key: Box::new(key_expr),
13186                            },
13187                            line,
13188                        }
13189                    } else if i < chars.len() && chars[i] == '[' {
13190                        // $array[idx]
13191                        i += 1;
13192                        let mut idx_str = String::new();
13193                        while i < chars.len() && chars[i] != ']' {
13194                            idx_str.push(chars[i]);
13195                            i += 1;
13196                        }
13197                        if i < chars.len() {
13198                            i += 1;
13199                        }
13200                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
13201                            Expr {
13202                                kind: ExprKind::ScalarVar(rest.to_string()),
13203                                line,
13204                            }
13205                        } else if let Ok(n) = idx_str.parse::<i64>() {
13206                            Expr {
13207                                kind: ExprKind::Integer(n),
13208                                line,
13209                            }
13210                        } else {
13211                            Expr {
13212                                kind: ExprKind::String(idx_str),
13213                                line,
13214                            }
13215                        };
13216                        Expr {
13217                            kind: ExprKind::ArrayElement {
13218                                array: name,
13219                                index: Box::new(idx_expr),
13220                            },
13221                            line,
13222                        }
13223                    } else {
13224                        // Bare $name — defer to the chain-extension loop below.
13225                        Expr {
13226                            kind: ExprKind::ScalarVar(name),
13227                            line,
13228                        }
13229                    };
13230
13231                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
13232                    // implies `->` between consecutive subscripts (`$m[1][2]`
13233                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
13234                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13235                    parts.push(StringPart::Expr(base));
13236                } else if chars[i].is_ascii_digit() {
13237                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
13238                    if chars[i] == '0' {
13239                        i += 1;
13240                        if i < chars.len() && chars[i].is_ascii_digit() {
13241                            return Err(self.syntax_err(
13242                                "Numeric variables with more than one digit may not start with '0'",
13243                                line,
13244                            ));
13245                        }
13246                        parts.push(StringPart::ScalarVar("0".into()));
13247                    } else {
13248                        let start = i;
13249                        while i < chars.len() && chars[i].is_ascii_digit() {
13250                            i += 1;
13251                        }
13252                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
13253                    }
13254                } else {
13255                    let c = chars[i];
13256                    let probe = c.to_string();
13257                    if Interpreter::is_special_scalar_name_for_get(&probe)
13258                        || matches!(c, '\'' | '`')
13259                    {
13260                        i += 1;
13261                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
13262                        if i < chars.len() && chars[i] == '{' {
13263                            i += 1; // skip {
13264                            let mut key = String::new();
13265                            let mut depth = 1;
13266                            while i < chars.len() && depth > 0 {
13267                                if chars[i] == '{' {
13268                                    depth += 1;
13269                                } else if chars[i] == '}' {
13270                                    depth -= 1;
13271                                    if depth == 0 {
13272                                        break;
13273                                    }
13274                                }
13275                                key.push(chars[i]);
13276                                i += 1;
13277                            }
13278                            if i < chars.len() {
13279                                i += 1;
13280                            } // skip }
13281                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
13282                                Expr {
13283                                    kind: ExprKind::ScalarVar(rest.to_string()),
13284                                    line,
13285                                }
13286                            } else {
13287                                Expr {
13288                                    kind: ExprKind::String(key),
13289                                    line,
13290                                }
13291                            };
13292                            let mut base = Expr {
13293                                kind: ExprKind::HashElement {
13294                                    hash: probe,
13295                                    key: Box::new(key_expr),
13296                                },
13297                                line,
13298                            };
13299                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13300                            parts.push(StringPart::Expr(base));
13301                        } else {
13302                            // Check for arrow deref chain: `$@->{key}`, etc.
13303                            let mut base = Expr {
13304                                kind: ExprKind::ScalarVar(probe),
13305                                line,
13306                            };
13307                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13308                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
13309                                // No chain extension — use the simpler ScalarVar part
13310                                if let ExprKind::ScalarVar(name) = base.kind {
13311                                    parts.push(StringPart::ScalarVar(name));
13312                                }
13313                            } else {
13314                                parts.push(StringPart::Expr(base));
13315                            }
13316                        }
13317                    } else {
13318                        literal.push('$');
13319                        literal.push(c);
13320                        i += 1;
13321                    }
13322                }
13323            } else if chars[i] == '@' && i + 1 < chars.len() {
13324                let next = chars[i + 1];
13325                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
13326                if next == '$' {
13327                    if !literal.is_empty() {
13328                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13329                    }
13330                    i += 1; // past `@`
13331                    debug_assert_eq!(chars[i], '$');
13332                    i += 1; // past `$`
13333                    while i < chars.len() && chars[i].is_whitespace() {
13334                        i += 1;
13335                    }
13336                    if i >= chars.len() {
13337                        return Err(self.syntax_err(
13338                            "Expected variable or block after `@$` in double-quoted string",
13339                            line,
13340                        ));
13341                    }
13342                    let inner_expr = if chars[i] == '{' {
13343                        i += 1;
13344                        let start = i;
13345                        let mut depth = 1usize;
13346                        while i < chars.len() && depth > 0 {
13347                            match chars[i] {
13348                                '{' => depth += 1,
13349                                '}' => {
13350                                    depth -= 1;
13351                                    if depth == 0 {
13352                                        break;
13353                                    }
13354                                }
13355                                _ => {}
13356                            }
13357                            i += 1;
13358                        }
13359                        if depth != 0 {
13360                            return Err(self.syntax_err(
13361                                "Unterminated `${ ... }` after `@` in double-quoted string",
13362                                line,
13363                            ));
13364                        }
13365                        let inner: String = chars[start..i].iter().collect();
13366                        i += 1; // closing `}`
13367                        parse_expression_from_str(inner.trim(), "-e")?
13368                    } else {
13369                        let mut name = String::new();
13370                        if chars[i] == '^' {
13371                            name.push('^');
13372                            i += 1;
13373                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
13374                            {
13375                                name.push(chars[i]);
13376                                i += 1;
13377                            }
13378                        } else {
13379                            while i < chars.len()
13380                                && (chars[i].is_alphanumeric()
13381                                    || chars[i] == '_'
13382                                    || chars[i] == ':')
13383                            {
13384                                name.push(chars[i]);
13385                                i += 1;
13386                            }
13387                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
13388                                name.push_str("::");
13389                                i += 2;
13390                                while i < chars.len()
13391                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
13392                                {
13393                                    name.push(chars[i]);
13394                                    i += 1;
13395                                }
13396                            }
13397                        }
13398                        if name.is_empty() {
13399                            return Err(self.syntax_err(
13400                                "Expected identifier after `@$` in double-quoted string",
13401                                line,
13402                            ));
13403                        }
13404                        Expr {
13405                            kind: ExprKind::ScalarVar(name),
13406                            line,
13407                        }
13408                    };
13409                    parts.push(StringPart::Expr(Expr {
13410                        kind: ExprKind::Deref {
13411                            expr: Box::new(inner_expr),
13412                            kind: Sigil::Array,
13413                        },
13414                        line,
13415                    }));
13416                    continue 'istr;
13417                }
13418                if next == '{' {
13419                    if !literal.is_empty() {
13420                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13421                    }
13422                    i += 2; // `@{`
13423                    let start = i;
13424                    let mut depth = 1usize;
13425                    while i < chars.len() && depth > 0 {
13426                        match chars[i] {
13427                            '{' => depth += 1,
13428                            '}' => {
13429                                depth -= 1;
13430                                if depth == 0 {
13431                                    break;
13432                                }
13433                            }
13434                            _ => {}
13435                        }
13436                        i += 1;
13437                    }
13438                    if depth != 0 {
13439                        return Err(
13440                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
13441                        );
13442                    }
13443                    let inner: String = chars[start..i].iter().collect();
13444                    i += 1; // closing `}`
13445                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
13446                    parts.push(StringPart::Expr(Expr {
13447                        kind: ExprKind::Deref {
13448                            expr: Box::new(inner_expr),
13449                            kind: Sigil::Array,
13450                        },
13451                        line,
13452                    }));
13453                    continue 'istr;
13454                }
13455                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
13456                    literal.push(chars[i]);
13457                    i += 1;
13458                } else {
13459                    if !literal.is_empty() {
13460                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13461                    }
13462                    i += 1;
13463                    let mut name = String::new();
13464                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
13465                        name.push(chars[i]);
13466                        i += 1;
13467                    } else {
13468                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13469                            name.push(chars[i]);
13470                            i += 1;
13471                        }
13472                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
13473                            name.push_str("::");
13474                            i += 2;
13475                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
13476                            {
13477                                name.push(chars[i]);
13478                                i += 1;
13479                            }
13480                        }
13481                    }
13482                    if i < chars.len() && chars[i] == '[' {
13483                        i += 1;
13484                        let start_inner = i;
13485                        let mut depth = 1usize;
13486                        while i < chars.len() && depth > 0 {
13487                            match chars[i] {
13488                                '[' => depth += 1,
13489                                ']' => depth -= 1,
13490                                _ => {}
13491                            }
13492                            if depth == 0 {
13493                                let inner: String = chars[start_inner..i].iter().collect();
13494                                i += 1; // closing ]
13495                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
13496                                parts.push(StringPart::Expr(Expr {
13497                                    kind: ExprKind::ArraySlice {
13498                                        array: name.clone(),
13499                                        indices,
13500                                    },
13501                                    line,
13502                                }));
13503                                continue 'istr;
13504                            }
13505                            i += 1;
13506                        }
13507                        return Err(self.syntax_err(
13508                            "Unterminated [ in array slice inside quoted string",
13509                            line,
13510                        ));
13511                    }
13512                    parts.push(StringPart::ArrayVar(name));
13513                }
13514            } else if chars[i] == '#'
13515                && i + 1 < chars.len()
13516                && chars[i + 1] == '{'
13517                && !crate::compat_mode()
13518            {
13519                // #{expr} — Ruby-style expression interpolation (stryke extension).
13520                if !literal.is_empty() {
13521                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13522                }
13523                i += 2; // skip `#{`
13524                let mut inner = String::new();
13525                let mut depth = 1usize;
13526                while i < chars.len() && depth > 0 {
13527                    match chars[i] {
13528                        '{' => depth += 1,
13529                        '}' => {
13530                            depth -= 1;
13531                            if depth == 0 {
13532                                break;
13533                            }
13534                        }
13535                        _ => {}
13536                    }
13537                    inner.push(chars[i]);
13538                    i += 1;
13539                }
13540                if i < chars.len() {
13541                    i += 1; // skip closing `}`
13542                }
13543                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
13544                parts.push(StringPart::Expr(expr));
13545            } else {
13546                literal.push(chars[i]);
13547                i += 1;
13548            }
13549        }
13550        if !literal.is_empty() {
13551            parts.push(StringPart::Literal(literal));
13552        }
13553
13554        if parts.len() == 1 {
13555            if let StringPart::Literal(s) = &parts[0] {
13556                return Ok(Expr {
13557                    kind: ExprKind::String(s.clone()),
13558                    line,
13559                });
13560            }
13561        }
13562        if parts.is_empty() {
13563            return Ok(Expr {
13564                kind: ExprKind::String(String::new()),
13565                line,
13566            });
13567        }
13568
13569        Ok(Expr {
13570            kind: ExprKind::InterpolatedString(parts),
13571            line,
13572        })
13573    }
13574
13575    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
13576        match &e.kind {
13577            ExprKind::String(s) => Ok(s.clone()),
13578            _ => Err(self.syntax_err(
13579                "overload key must be a string literal (e.g. '\"\"' or '+')",
13580                e.line,
13581            )),
13582        }
13583    }
13584
13585    fn expr_to_overload_sub(&self, e: &Expr) -> PerlResult<String> {
13586        match &e.kind {
13587            ExprKind::String(s) => Ok(s.clone()),
13588            ExprKind::Integer(n) => Ok(n.to_string()),
13589            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
13590            _ => Err(self.syntax_err(
13591                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
13592                e.line,
13593            )),
13594        }
13595    }
13596}
13597
13598fn merge_expr_list(parts: Vec<Expr>) -> Expr {
13599    if parts.len() == 1 {
13600        parts.into_iter().next().unwrap()
13601    } else {
13602        let line = parts.first().map(|e| e.line).unwrap_or(0);
13603        Expr {
13604            kind: ExprKind::List(parts),
13605            line,
13606        }
13607    }
13608}
13609
13610/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
13611pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
13612    let mut lexer = Lexer::new_with_file(s, file);
13613    let tokens = lexer.tokenize()?;
13614    let mut parser = Parser::new_with_file(tokens, file);
13615    let e = parser.parse_expression()?;
13616    if !parser.at_eof() {
13617        return Err(parser.syntax_err(
13618            "Extra tokens in embedded string expression",
13619            parser.peek_line(),
13620        ));
13621    }
13622    Ok(e)
13623}
13624
13625/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
13626pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
13627    let mut lexer = Lexer::new_with_file(s, file);
13628    let tokens = lexer.tokenize()?;
13629    let mut parser = Parser::new_with_file(tokens, file);
13630    let stmts = parser.parse_statements()?;
13631    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
13632    let inner = Expr {
13633        kind: ExprKind::CodeRef {
13634            params: vec![],
13635            body: stmts,
13636        },
13637        line: inner_line,
13638    };
13639    Ok(Expr {
13640        kind: ExprKind::Do(Box::new(inner)),
13641        line,
13642    })
13643}
13644
13645/// Comma-separated expressions on a `format` value line (below a picture line).
13646/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
13647pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
13648    let mut lexer = Lexer::new_with_file(s, file);
13649    let tokens = lexer.tokenize()?;
13650    let mut parser = Parser::new_with_file(tokens, file);
13651    parser.parse_arg_list()
13652}
13653
13654pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
13655    let trimmed = line.trim();
13656    if trimmed.is_empty() {
13657        return Ok(vec![]);
13658    }
13659    let mut lexer = Lexer::new(trimmed);
13660    let tokens = lexer.tokenize()?;
13661    let mut parser = Parser::new(tokens);
13662    let mut exprs = Vec::new();
13663    loop {
13664        if parser.at_eof() {
13665            break;
13666        }
13667        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
13668        exprs.push(parser.parse_assign_expr()?);
13669        if parser.eat(&Token::Comma) {
13670            continue;
13671        }
13672        if !parser.at_eof() {
13673            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
13674        }
13675        break;
13676    }
13677    Ok(exprs)
13678}
13679
13680#[cfg(test)]
13681mod tests {
13682    use super::*;
13683
13684    fn parse_ok(code: &str) -> Program {
13685        let mut lexer = Lexer::new(code);
13686        let tokens = lexer.tokenize().expect("tokenize");
13687        let mut parser = Parser::new(tokens);
13688        parser.parse_program().expect("parse")
13689    }
13690
13691    fn parse_err(code: &str) -> String {
13692        let mut lexer = Lexer::new(code);
13693        let tokens = match lexer.tokenize() {
13694            Ok(t) => t,
13695            Err(e) => return e.message,
13696        };
13697        let mut parser = Parser::new(tokens);
13698        parser.parse_program().unwrap_err().message
13699    }
13700
13701    #[test]
13702    fn parse_empty_program() {
13703        let p = parse_ok("");
13704        assert!(p.statements.is_empty());
13705    }
13706
13707    #[test]
13708    fn parse_semicolons_only() {
13709        let p = parse_ok(";;");
13710        assert!(p.statements.len() <= 3);
13711    }
13712
13713    #[test]
13714    fn parse_simple_scalar_assignment() {
13715        let p = parse_ok("$x = 1");
13716        assert_eq!(p.statements.len(), 1);
13717    }
13718
13719    #[test]
13720    fn parse_simple_array_assignment() {
13721        let p = parse_ok("@arr = (1, 2, 3)");
13722        assert_eq!(p.statements.len(), 1);
13723    }
13724
13725    #[test]
13726    fn parse_simple_hash_assignment() {
13727        let p = parse_ok("%h = (a => 1, b => 2)");
13728        assert_eq!(p.statements.len(), 1);
13729    }
13730
13731    #[test]
13732    fn parse_subroutine_decl() {
13733        let p = parse_ok("sub foo { 1 }");
13734        assert_eq!(p.statements.len(), 1);
13735        match &p.statements[0].kind {
13736            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
13737            _ => panic!("expected SubDecl"),
13738        }
13739    }
13740
13741    #[test]
13742    fn parse_subroutine_with_prototype() {
13743        let p = parse_ok("sub foo ($$) { 1 }");
13744        assert_eq!(p.statements.len(), 1);
13745        match &p.statements[0].kind {
13746            StmtKind::SubDecl { prototype, .. } => {
13747                assert!(prototype.is_some());
13748            }
13749            _ => panic!("expected SubDecl"),
13750        }
13751    }
13752
13753    #[test]
13754    fn parse_anonymous_sub() {
13755        let p = parse_ok("my $f = sub { 1 }");
13756        assert_eq!(p.statements.len(), 1);
13757    }
13758
13759    #[test]
13760    fn parse_if_statement() {
13761        let p = parse_ok("if (1) { 2 }");
13762        assert_eq!(p.statements.len(), 1);
13763        matches!(&p.statements[0].kind, StmtKind::If { .. });
13764    }
13765
13766    #[test]
13767    fn parse_if_elsif_else() {
13768        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
13769        assert_eq!(p.statements.len(), 1);
13770    }
13771
13772    #[test]
13773    fn parse_unless_statement() {
13774        let p = parse_ok("unless (0) { 1 }");
13775        assert_eq!(p.statements.len(), 1);
13776    }
13777
13778    #[test]
13779    fn parse_while_loop() {
13780        let p = parse_ok("while ($x) { $x-- }");
13781        assert_eq!(p.statements.len(), 1);
13782    }
13783
13784    #[test]
13785    fn parse_until_loop() {
13786        let p = parse_ok("until ($x) { $x++ }");
13787        assert_eq!(p.statements.len(), 1);
13788    }
13789
13790    #[test]
13791    fn parse_for_c_style() {
13792        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
13793        assert_eq!(p.statements.len(), 1);
13794    }
13795
13796    #[test]
13797    fn parse_foreach_loop() {
13798        let p = parse_ok("foreach my $x (@arr) { 1 }");
13799        assert_eq!(p.statements.len(), 1);
13800    }
13801
13802    #[test]
13803    fn parse_loop_with_label() {
13804        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
13805        assert_eq!(p.statements.len(), 1);
13806        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
13807    }
13808
13809    #[test]
13810    fn parse_begin_block() {
13811        let p = parse_ok("BEGIN { 1 }");
13812        assert_eq!(p.statements.len(), 1);
13813        matches!(&p.statements[0].kind, StmtKind::Begin(_));
13814    }
13815
13816    #[test]
13817    fn parse_end_block() {
13818        let p = parse_ok("END { 1 }");
13819        assert_eq!(p.statements.len(), 1);
13820        matches!(&p.statements[0].kind, StmtKind::End(_));
13821    }
13822
13823    #[test]
13824    fn parse_package_statement() {
13825        let p = parse_ok("package Foo::Bar");
13826        assert_eq!(p.statements.len(), 1);
13827        match &p.statements[0].kind {
13828            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
13829            _ => panic!("expected Package"),
13830        }
13831    }
13832
13833    #[test]
13834    fn parse_use_statement() {
13835        let p = parse_ok("use strict");
13836        assert_eq!(p.statements.len(), 1);
13837    }
13838
13839    #[test]
13840    fn parse_no_statement() {
13841        let p = parse_ok("no warnings");
13842        assert_eq!(p.statements.len(), 1);
13843    }
13844
13845    #[test]
13846    fn parse_require_bareword() {
13847        let p = parse_ok("require Foo::Bar");
13848        assert_eq!(p.statements.len(), 1);
13849    }
13850
13851    #[test]
13852    fn parse_require_string() {
13853        let p = parse_ok(r#"require "foo.pl""#);
13854        assert_eq!(p.statements.len(), 1);
13855    }
13856
13857    #[test]
13858    fn parse_eval_block() {
13859        let p = parse_ok("eval { 1 }");
13860        assert_eq!(p.statements.len(), 1);
13861    }
13862
13863    #[test]
13864    fn parse_eval_string() {
13865        let p = parse_ok(r#"eval "1 + 2""#);
13866        assert_eq!(p.statements.len(), 1);
13867    }
13868
13869    #[test]
13870    fn parse_qw_word_list() {
13871        let p = parse_ok("my @a = qw(foo bar baz)");
13872        assert_eq!(p.statements.len(), 1);
13873    }
13874
13875    #[test]
13876    fn parse_q_string() {
13877        let p = parse_ok("my $s = q{hello}");
13878        assert_eq!(p.statements.len(), 1);
13879    }
13880
13881    #[test]
13882    fn parse_qq_string() {
13883        let p = parse_ok(r#"my $s = qq(hello $x)"#);
13884        assert_eq!(p.statements.len(), 1);
13885    }
13886
13887    #[test]
13888    fn parse_regex_match() {
13889        let p = parse_ok(r#"$x =~ /foo/"#);
13890        assert_eq!(p.statements.len(), 1);
13891    }
13892
13893    #[test]
13894    fn parse_regex_substitution() {
13895        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
13896        assert_eq!(p.statements.len(), 1);
13897    }
13898
13899    #[test]
13900    fn parse_transliterate() {
13901        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
13902        assert_eq!(p.statements.len(), 1);
13903    }
13904
13905    #[test]
13906    fn parse_ternary_operator() {
13907        let p = parse_ok("my $x = $a ? 1 : 2");
13908        assert_eq!(p.statements.len(), 1);
13909    }
13910
13911    #[test]
13912    fn parse_arrow_method_call() {
13913        let p = parse_ok("$obj->method()");
13914        assert_eq!(p.statements.len(), 1);
13915    }
13916
13917    #[test]
13918    fn parse_arrow_deref_hash() {
13919        let p = parse_ok("$r->{key}");
13920        assert_eq!(p.statements.len(), 1);
13921    }
13922
13923    #[test]
13924    fn parse_arrow_deref_array() {
13925        let p = parse_ok("$r->[0]");
13926        assert_eq!(p.statements.len(), 1);
13927    }
13928
13929    #[test]
13930    fn parse_chained_arrow_deref() {
13931        let p = parse_ok("$r->{a}[0]{b}");
13932        assert_eq!(p.statements.len(), 1);
13933    }
13934
13935    #[test]
13936    fn parse_my_multiple_vars() {
13937        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
13938        assert_eq!(p.statements.len(), 1);
13939    }
13940
13941    #[test]
13942    fn parse_our_scalar() {
13943        let p = parse_ok("our $VERSION = '1.0'");
13944        assert_eq!(p.statements.len(), 1);
13945    }
13946
13947    #[test]
13948    fn parse_local_scalar() {
13949        let p = parse_ok("local $/ = undef");
13950        assert_eq!(p.statements.len(), 1);
13951    }
13952
13953    #[test]
13954    fn parse_state_variable() {
13955        let p = parse_ok("sub counter { state $n = 0; $n++ }");
13956        assert_eq!(p.statements.len(), 1);
13957    }
13958
13959    #[test]
13960    fn parse_postfix_if() {
13961        let p = parse_ok("print 1 if $x");
13962        assert_eq!(p.statements.len(), 1);
13963    }
13964
13965    #[test]
13966    fn parse_postfix_unless() {
13967        let p = parse_ok("die 'error' unless $ok");
13968        assert_eq!(p.statements.len(), 1);
13969    }
13970
13971    #[test]
13972    fn parse_postfix_while() {
13973        let p = parse_ok("$x++ while $x < 10");
13974        assert_eq!(p.statements.len(), 1);
13975    }
13976
13977    #[test]
13978    fn parse_postfix_for() {
13979        let p = parse_ok("print for @arr");
13980        assert_eq!(p.statements.len(), 1);
13981    }
13982
13983    #[test]
13984    fn parse_last_next_redo() {
13985        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
13986        assert_eq!(p.statements.len(), 1);
13987    }
13988
13989    #[test]
13990    fn parse_return_statement() {
13991        let p = parse_ok("sub foo { return 42 }");
13992        assert_eq!(p.statements.len(), 1);
13993    }
13994
13995    #[test]
13996    fn parse_wantarray() {
13997        let p = parse_ok("sub foo { wantarray ? @a : $a }");
13998        assert_eq!(p.statements.len(), 1);
13999    }
14000
14001    #[test]
14002    fn parse_caller_builtin() {
14003        let p = parse_ok("my @c = caller");
14004        assert_eq!(p.statements.len(), 1);
14005    }
14006
14007    #[test]
14008    fn parse_ref_to_array() {
14009        let p = parse_ok("my $r = \\@arr");
14010        assert_eq!(p.statements.len(), 1);
14011    }
14012
14013    #[test]
14014    fn parse_ref_to_hash() {
14015        let p = parse_ok("my $r = \\%hash");
14016        assert_eq!(p.statements.len(), 1);
14017    }
14018
14019    #[test]
14020    fn parse_ref_to_scalar() {
14021        let p = parse_ok("my $r = \\$x");
14022        assert_eq!(p.statements.len(), 1);
14023    }
14024
14025    #[test]
14026    fn parse_deref_scalar() {
14027        let p = parse_ok("my $v = $$r");
14028        assert_eq!(p.statements.len(), 1);
14029    }
14030
14031    #[test]
14032    fn parse_deref_array() {
14033        let p = parse_ok("my @a = @$r");
14034        assert_eq!(p.statements.len(), 1);
14035    }
14036
14037    #[test]
14038    fn parse_deref_hash() {
14039        let p = parse_ok("my %h = %$r");
14040        assert_eq!(p.statements.len(), 1);
14041    }
14042
14043    #[test]
14044    fn parse_blessed_ref() {
14045        let p = parse_ok("bless $r, 'Foo'");
14046        assert_eq!(p.statements.len(), 1);
14047    }
14048
14049    #[test]
14050    fn parse_heredoc_basic() {
14051        let p = parse_ok("my $s = <<END;\nfoo\nEND");
14052        assert_eq!(p.statements.len(), 1);
14053    }
14054
14055    #[test]
14056    fn parse_heredoc_quoted() {
14057        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
14058        assert_eq!(p.statements.len(), 1);
14059    }
14060
14061    #[test]
14062    fn parse_do_block() {
14063        let p = parse_ok("my $x = do { 1 + 2 }");
14064        assert_eq!(p.statements.len(), 1);
14065    }
14066
14067    #[test]
14068    fn parse_do_file() {
14069        let p = parse_ok(r#"do "foo.pl""#);
14070        assert_eq!(p.statements.len(), 1);
14071    }
14072
14073    #[test]
14074    fn parse_map_expression() {
14075        let p = parse_ok("my @b = map { $_ * 2 } @a");
14076        assert_eq!(p.statements.len(), 1);
14077    }
14078
14079    #[test]
14080    fn parse_grep_expression() {
14081        let p = parse_ok("my @b = grep { $_ > 0 } @a");
14082        assert_eq!(p.statements.len(), 1);
14083    }
14084
14085    #[test]
14086    fn parse_sort_expression() {
14087        let p = parse_ok("my @b = sort { $a <=> $b } @a");
14088        assert_eq!(p.statements.len(), 1);
14089    }
14090
14091    #[test]
14092    fn parse_pipe_forward() {
14093        let p = parse_ok("@a |> map { $_ * 2 }");
14094        assert_eq!(p.statements.len(), 1);
14095    }
14096
14097    #[test]
14098    fn parse_expression_from_str_simple() {
14099        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
14100        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
14101    }
14102
14103    #[test]
14104    fn parse_expression_from_str_extra_tokens_error() {
14105        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
14106        assert!(err.message.contains("Extra tokens"));
14107    }
14108
14109    #[test]
14110    fn parse_slice_indices_from_str_basic() {
14111        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
14112        assert_eq!(indices.len(), 3);
14113    }
14114
14115    #[test]
14116    fn parse_format_value_line_empty() {
14117        let exprs = parse_format_value_line("").unwrap();
14118        assert!(exprs.is_empty());
14119    }
14120
14121    #[test]
14122    fn parse_format_value_line_single() {
14123        let exprs = parse_format_value_line("$x").unwrap();
14124        assert_eq!(exprs.len(), 1);
14125    }
14126
14127    #[test]
14128    fn parse_format_value_line_multiple() {
14129        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
14130        assert_eq!(exprs.len(), 3);
14131    }
14132
14133    #[test]
14134    fn parse_unclosed_brace_error() {
14135        let err = parse_err("sub foo {");
14136        assert!(!err.is_empty());
14137    }
14138
14139    #[test]
14140    fn parse_unclosed_paren_error() {
14141        let err = parse_err("print (1, 2");
14142        assert!(!err.is_empty());
14143    }
14144
14145    #[test]
14146    fn parse_invalid_statement_error() {
14147        let err = parse_err("???");
14148        assert!(!err.is_empty());
14149    }
14150
14151    #[test]
14152    fn merge_expr_list_single() {
14153        let e = Expr {
14154            kind: ExprKind::Integer(1),
14155            line: 1,
14156        };
14157        let merged = merge_expr_list(vec![e.clone()]);
14158        matches!(merged.kind, ExprKind::Integer(1));
14159    }
14160
14161    #[test]
14162    fn merge_expr_list_multiple() {
14163        let e1 = Expr {
14164            kind: ExprKind::Integer(1),
14165            line: 1,
14166        };
14167        let e2 = Expr {
14168            kind: ExprKind::Integer(2),
14169            line: 1,
14170        };
14171        let merged = merge_expr_list(vec![e1, e2]);
14172        matches!(merged.kind, ExprKind::List(_));
14173    }
14174}