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    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
123    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
124    /// misparse `b : c` as a range.
125    suppress_colon_range: u32,
126    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
127    /// instead of thread-first (prepend). Set by `->>` thread macro.
128    thread_last_mode: bool,
129}
130
131impl Parser {
132    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
133        Self::new_with_file(tokens, "-e")
134    }
135
136    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
137        Self {
138            tokens,
139            pos: 0,
140            next_rate_limit_slot: 0,
141            suppress_indirect_paren_call: 0,
142            pipe_rhs_depth: 0,
143            no_pipe_forward_depth: 0,
144            suppress_scalar_hash_brace: 0,
145            next_desugar_tmp: 0,
146            error_file: file.into(),
147            declared_subs: std::collections::HashSet::new(),
148            suppress_parenless_call: 0,
149            suppress_slash_as_div: 0,
150            suppress_m_regex: 0,
151            suppress_colon_range: 0,
152            thread_last_mode: false,
153        }
154    }
155
156    fn alloc_desugar_tmp(&mut self) -> u32 {
157        let n = self.next_desugar_tmp;
158        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
159        n
160    }
161
162    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
163    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
164    /// placeholder list instead of erroring on a missing operand.
165    #[inline]
166    fn in_pipe_rhs(&self) -> bool {
167        self.pipe_rhs_depth > 0
168    }
169
170    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
171    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
172    fn pipe_supplies_slurped_list_operand(&self) -> bool {
173        self.in_pipe_rhs()
174            && (matches!(
175                self.peek(),
176                Token::Semicolon
177                    | Token::RBrace
178                    | Token::RParen
179                    | Token::Eof
180                    | Token::Comma
181                    | Token::PipeForward
182            ) || self.peek_line() > self.prev_line())
183    }
184
185    /// Empty placeholder list used as a stand-in for the list operand of
186    /// list-taking builtins when they appear on the RHS of `|>`.
187    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
188    /// value at desugar time, so the placeholder is never evaluated.
189    #[inline]
190    fn pipe_placeholder_list(&self, line: usize) -> Expr {
191        Expr {
192            kind: ExprKind::List(vec![]),
193            line,
194        }
195    }
196
197    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
198    ///
199    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
200    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
201    /// element instead of stringifying the bareword.  Non-bareword expressions
202    /// pass through unchanged.
203    ///
204    /// Also injects `$_` into known builtins that were parsed with zero
205    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
206    /// topic variable instead of being no-ops.
207    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
208        let line = expr.line;
209        let topic = || Expr {
210            kind: ExprKind::ScalarVar("_".into()),
211            line,
212        };
213        match expr.kind {
214            ExprKind::Bareword(ref name) => Expr {
215                kind: ExprKind::FuncCall {
216                    name: name.clone(),
217                    args: vec![topic()],
218                },
219                line,
220            },
221            // Builtins that take Vec<Expr> args — inject $_ when empty.
222            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
223                kind: ExprKind::Unlink(vec![topic()]),
224                line,
225            },
226            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
227                kind: ExprKind::Chmod(vec![topic()]),
228                line,
229            },
230            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
231            ExprKind::Stat(_) => expr,
232            ExprKind::Lstat(_) => expr,
233            ExprKind::Readlink(_) => expr,
234            // rev with empty list should use $_
235            ExprKind::Rev(ref inner) => {
236                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
237                    Expr {
238                        kind: ExprKind::Rev(Box::new(topic())),
239                        line,
240                    }
241                } else {
242                    expr
243                }
244            }
245            _ => expr,
246        }
247    }
248
249    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
250    /// duration, so any trailing `|>` is left to the enclosing parser instead
251    /// of being absorbed into this sub-expression. Used by paren-less arg
252    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
253    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
254    /// left-associatively instead of letting `head`'s first arg swallow the
255    /// outer `|>`. The counter is restored on both success and error paths.
256    fn parse_assign_expr_stop_at_pipe(&mut self) -> PerlResult<Expr> {
257        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
258        let r = self.parse_assign_expr();
259        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
260        r
261    }
262
263    fn syntax_err(&self, message: impl Into<String>, line: usize) -> PerlError {
264        PerlError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
265    }
266
267    fn alloc_rate_limit_slot(&mut self) -> u32 {
268        let s = self.next_rate_limit_slot;
269        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
270        s
271    }
272
273    fn peek(&self) -> &Token {
274        self.tokens
275            .get(self.pos)
276            .map(|(t, _)| t)
277            .unwrap_or(&Token::Eof)
278    }
279
280    fn peek_line(&self) -> usize {
281        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
282    }
283
284    fn peek_at(&self, offset: usize) -> &Token {
285        self.tokens
286            .get(self.pos + offset)
287            .map(|(t, _)| t)
288            .unwrap_or(&Token::Eof)
289    }
290
291    fn advance(&mut self) -> (Token, usize) {
292        let tok = self
293            .tokens
294            .get(self.pos)
295            .cloned()
296            .unwrap_or((Token::Eof, 0));
297        self.pos += 1;
298        tok
299    }
300
301    /// Line number of the most recently consumed token (the token at `pos - 1`).
302    fn prev_line(&self) -> usize {
303        if self.pos > 0 {
304            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
305        } else {
306            0
307        }
308    }
309
310    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
311    /// Heuristics (assuming current token is `{`):
312    /// - `{ bareword =>` → hashref
313    /// - `{ "string" =>` → hashref
314    /// - `{ $var =>` → hashref
315    /// - `{ 0 =>` → hashref (numeric key)
316    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
317    /// - `{ }` (empty) → hashref
318    fn looks_like_hashref(&self) -> bool {
319        debug_assert!(matches!(self.peek(), Token::LBrace));
320        let tok1 = self.peek_at(1);
321        let tok2 = self.peek_at(2);
322        match tok1 {
323            Token::RBrace => true,
324            Token::Ident(_)
325            | Token::SingleString(_)
326            | Token::DoubleString(_)
327            | Token::ScalarVar(_)
328            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
329            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
330            _ => false,
331        }
332    }
333
334    fn expect(&mut self, expected: &Token) -> PerlResult<usize> {
335        let (tok, line) = self.advance();
336        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
337            Ok(line)
338        } else {
339            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
340        }
341    }
342
343    fn eat(&mut self, expected: &Token) -> bool {
344        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
345            self.advance();
346            true
347        } else {
348            false
349        }
350    }
351
352    fn at_eof(&self) -> bool {
353        matches!(self.peek(), Token::Eof)
354    }
355
356    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
357    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
358        matches!(
359            tok,
360            Token::RParen
361                | Token::Semicolon
362                | Token::Comma
363                | Token::RBrace
364                | Token::Eof
365                | Token::LogAnd
366                | Token::LogOr
367                | Token::LogAndWord
368                | Token::LogOrWord
369                | Token::PipeForward
370        )
371    }
372
373    /// True when the next token is a statement-starting keyword on a *different*
374    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
375    /// import lists when semicolons are omitted (stryke extension).
376    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
377        // Semicolons-optional is a stryke extension; in compat mode, require them.
378        if crate::compat_mode() {
379            return false;
380        }
381        if self.peek_line() == stmt_line {
382            return false;
383        }
384        matches!(
385            self.peek(),
386            Token::Ident(ref kw) if matches!(kw.as_str(),
387                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
388                | "if" | "unless" | "while" | "until" | "for" | "foreach"
389                | "return" | "last" | "next" | "redo" | "package" | "require"
390                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
391            )
392        )
393    }
394
395    /// True when the next token is on a different line from `stmt_line` and could
396    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
397    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
398    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
399        if crate::compat_mode() {
400            return false;
401        }
402        if self.peek_line() == stmt_line {
403            return false;
404        }
405        matches!(
406            self.peek(),
407            Token::ScalarVar(_)
408                | Token::DerefScalarVar(_)
409                | Token::ArrayVar(_)
410                | Token::HashVar(_)
411                | Token::LBrace
412        ) || self.next_is_new_stmt_keyword(stmt_line)
413    }
414
415    // ── Top level ──
416
417    pub fn parse_program(&mut self) -> PerlResult<Program> {
418        let statements = self.parse_statements()?;
419        Ok(Program { statements })
420    }
421
422    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
423    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
424        let mut statements = Vec::new();
425        while !self.at_eof() {
426            if matches!(self.peek(), Token::Semicolon) {
427                let line = self.peek_line();
428                self.advance();
429                statements.push(Statement {
430                    label: None,
431                    kind: StmtKind::Empty,
432                    line,
433                });
434                continue;
435            }
436            statements.push(self.parse_statement()?);
437        }
438        Ok(statements)
439    }
440
441    // ── Statements ──
442
443    fn parse_statement(&mut self) -> PerlResult<Statement> {
444        let line = self.peek_line();
445
446        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
447        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
448        let label = match self.peek().clone() {
449            Token::Ident(_) => {
450                if matches!(self.peek_at(1), Token::Colon)
451                    && !matches!(self.peek_at(2), Token::Colon)
452                {
453                    let (tok, _) = self.advance();
454                    let l = match tok {
455                        Token::Ident(l) => l,
456                        _ => unreachable!(),
457                    };
458                    self.advance(); // ':'
459                    Some(l)
460                } else {
461                    None
462                }
463            }
464            _ => None,
465        };
466
467        let mut stmt = match self.peek().clone() {
468            Token::FormatDecl { .. } => {
469                let tok_line = self.peek_line();
470                let (tok, _) = self.advance();
471                match tok {
472                    Token::FormatDecl { name, lines } => Statement {
473                        label: label.clone(),
474                        kind: StmtKind::FormatDecl { name, lines },
475                        line: tok_line,
476                    },
477                    _ => unreachable!(),
478                }
479            }
480            Token::Ident(ref kw) => match kw.as_str() {
481                "if" => self.parse_if()?,
482                "unless" => self.parse_unless()?,
483                "while" => {
484                    let mut s = self.parse_while()?;
485                    if let StmtKind::While {
486                        label: ref mut lbl, ..
487                    } = s.kind
488                    {
489                        *lbl = label.clone();
490                    }
491                    s
492                }
493                "until" => {
494                    let mut s = self.parse_until()?;
495                    if let StmtKind::Until {
496                        label: ref mut lbl, ..
497                    } = s.kind
498                    {
499                        *lbl = label.clone();
500                    }
501                    s
502                }
503                "for" => {
504                    let mut s = self.parse_for_or_foreach()?;
505                    match s.kind {
506                        StmtKind::For {
507                            label: ref mut lbl, ..
508                        }
509                        | StmtKind::Foreach {
510                            label: ref mut lbl, ..
511                        } => *lbl = label.clone(),
512                        _ => {}
513                    }
514                    s
515                }
516                "foreach" => {
517                    let mut s = self.parse_foreach()?;
518                    if let StmtKind::Foreach {
519                        label: ref mut lbl, ..
520                    } = s.kind
521                    {
522                        *lbl = label.clone();
523                    }
524                    s
525                }
526                "sub" => {
527                    if !crate::compat_mode() {
528                        return Err(self.syntax_err(
529                            "stryke uses `fn` instead of `sub` (this is not Perl 5)",
530                            self.peek_line(),
531                        ));
532                    }
533                    self.parse_sub_decl(true)?
534                }
535                "fn" => self.parse_sub_decl(false)?,
536                "struct" => {
537                    if crate::compat_mode() {
538                        return Err(self.syntax_err(
539                            "`struct` is a stryke extension (disabled by --compat)",
540                            self.peek_line(),
541                        ));
542                    }
543                    self.parse_struct_decl()?
544                }
545                "enum" => {
546                    if crate::compat_mode() {
547                        return Err(self.syntax_err(
548                            "`enum` is a stryke extension (disabled by --compat)",
549                            self.peek_line(),
550                        ));
551                    }
552                    self.parse_enum_decl()?
553                }
554                "class" => {
555                    if crate::compat_mode() {
556                        // TODO: parse Perl 5.38 class syntax with :isa()
557                        return Err(self.syntax_err(
558                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
559                            self.peek_line(),
560                        ));
561                    }
562                    self.parse_class_decl(false, false)?
563                }
564                "abstract" => {
565                    self.advance(); // abstract
566                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
567                        return Err(self.syntax_err(
568                            "`abstract` must be followed by `class`",
569                            self.peek_line(),
570                        ));
571                    }
572                    self.parse_class_decl(true, false)?
573                }
574                "final" => {
575                    self.advance(); // final
576                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
577                        return Err(self
578                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
579                    }
580                    self.parse_class_decl(false, true)?
581                }
582                "trait" => {
583                    if crate::compat_mode() {
584                        return Err(self.syntax_err(
585                            "`trait` is a stryke extension (disabled by --compat)",
586                            self.peek_line(),
587                        ));
588                    }
589                    self.parse_trait_decl()?
590                }
591                "my" => self.parse_my_our_local("my", false)?,
592                "state" => self.parse_my_our_local("state", false)?,
593                "mysync" => {
594                    if crate::compat_mode() {
595                        return Err(self.syntax_err(
596                            "`mysync` is a stryke extension (disabled by --compat)",
597                            self.peek_line(),
598                        ));
599                    }
600                    self.parse_my_our_local("mysync", false)?
601                }
602                "frozen" | "const" => {
603                    let leading = kw.as_str().to_string();
604                    if crate::compat_mode() {
605                        return Err(self.syntax_err(
606                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
607                            self.peek_line(),
608                        ));
609                    }
610                    // `frozen my $x = val;` / `const my $x = val;` — the
611                    // two spellings are interchangeable (`const` is the
612                    // more-familiar name for new users). Expects `my`
613                    // to follow.
614                    self.advance(); // consume "frozen"/"const"
615                    if let Token::Ident(ref kw) = self.peek().clone() {
616                        if kw == "my" {
617                            let mut stmt = self.parse_my_our_local("my", false)?;
618                            if let StmtKind::My(ref mut decls) = stmt.kind {
619                                for decl in decls.iter_mut() {
620                                    decl.frozen = true;
621                                }
622                            }
623                            stmt
624                        } else {
625                            return Err(self.syntax_err(
626                                format!("Expected 'my' after '{leading}'"),
627                                self.peek_line(),
628                            ));
629                        }
630                    } else {
631                        return Err(self.syntax_err(
632                            format!("Expected 'my' after '{leading}'"),
633                            self.peek_line(),
634                        ));
635                    }
636                }
637                "typed" => {
638                    if crate::compat_mode() {
639                        return Err(self.syntax_err(
640                            "`typed` is a stryke extension (disabled by --compat)",
641                            self.peek_line(),
642                        ));
643                    }
644                    self.advance();
645                    if let Token::Ident(ref kw) = self.peek().clone() {
646                        if kw == "my" {
647                            self.parse_my_our_local("my", true)?
648                        } else {
649                            return Err(
650                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
651                            );
652                        }
653                    } else {
654                        return Err(
655                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
656                        );
657                    }
658                }
659                "our" => self.parse_my_our_local("our", false)?,
660                "local" => self.parse_my_our_local("local", false)?,
661                "package" => self.parse_package()?,
662                "use" => self.parse_use()?,
663                "no" => self.parse_no()?,
664                "return" => self.parse_return()?,
665                "last" => {
666                    self.advance();
667                    let lbl = if let Token::Ident(ref s) = self.peek() {
668                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
669                            let (Token::Ident(l), _) = self.advance() else {
670                                unreachable!()
671                            };
672                            Some(l)
673                        } else {
674                            None
675                        }
676                    } else {
677                        None
678                    };
679                    let stmt = Statement {
680                        label: None,
681                        kind: StmtKind::Last(lbl.or(label.clone())),
682                        line,
683                    };
684                    self.parse_stmt_postfix_modifier(stmt)?
685                }
686                "next" => {
687                    self.advance();
688                    let lbl = if let Token::Ident(ref s) = self.peek() {
689                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
690                            let (Token::Ident(l), _) = self.advance() else {
691                                unreachable!()
692                            };
693                            Some(l)
694                        } else {
695                            None
696                        }
697                    } else {
698                        None
699                    };
700                    let stmt = Statement {
701                        label: None,
702                        kind: StmtKind::Next(lbl.or(label.clone())),
703                        line,
704                    };
705                    self.parse_stmt_postfix_modifier(stmt)?
706                }
707                "redo" => {
708                    self.advance();
709                    self.eat(&Token::Semicolon);
710                    Statement {
711                        label: None,
712                        kind: StmtKind::Redo(label.clone()),
713                        line,
714                    }
715                }
716                "BEGIN" => {
717                    self.advance();
718                    let block = self.parse_block()?;
719                    Statement {
720                        label: None,
721                        kind: StmtKind::Begin(block),
722                        line,
723                    }
724                }
725                "END" => {
726                    self.advance();
727                    let block = self.parse_block()?;
728                    Statement {
729                        label: None,
730                        kind: StmtKind::End(block),
731                        line,
732                    }
733                }
734                "UNITCHECK" => {
735                    self.advance();
736                    let block = self.parse_block()?;
737                    Statement {
738                        label: None,
739                        kind: StmtKind::UnitCheck(block),
740                        line,
741                    }
742                }
743                "CHECK" => {
744                    self.advance();
745                    let block = self.parse_block()?;
746                    Statement {
747                        label: None,
748                        kind: StmtKind::Check(block),
749                        line,
750                    }
751                }
752                "INIT" => {
753                    self.advance();
754                    let block = self.parse_block()?;
755                    Statement {
756                        label: None,
757                        kind: StmtKind::Init(block),
758                        line,
759                    }
760                }
761                "goto" => {
762                    self.advance();
763                    let target = self.parse_expression()?;
764                    let stmt = Statement {
765                        label: None,
766                        kind: StmtKind::Goto {
767                            target: Box::new(target),
768                        },
769                        line,
770                    };
771                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
772                    self.parse_stmt_postfix_modifier(stmt)?
773                }
774                "continue" => {
775                    self.advance();
776                    let block = self.parse_block()?;
777                    Statement {
778                        label: None,
779                        kind: StmtKind::Continue(block),
780                        line,
781                    }
782                }
783                "try" => self.parse_try_catch()?,
784                "defer" => self.parse_defer_stmt()?,
785                "tie" => self.parse_tie_stmt()?,
786                "given" => self.parse_given()?,
787                "when" => self.parse_when_stmt()?,
788                "default" => self.parse_default_stmt()?,
789                "eval_timeout" => self.parse_eval_timeout()?,
790                "do" => {
791                    if matches!(self.peek_at(1), Token::LBrace) {
792                        self.advance();
793                        let body = self.parse_block()?;
794                        if let Token::Ident(ref w) = self.peek().clone() {
795                            if w == "while" {
796                                self.advance();
797                                self.expect(&Token::LParen)?;
798                                let mut condition = self.parse_expression()?;
799                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
800                                self.expect(&Token::RParen)?;
801                                self.eat(&Token::Semicolon);
802                                Statement {
803                                    label: label.clone(),
804                                    kind: StmtKind::DoWhile { body, condition },
805                                    line,
806                                }
807                            } else {
808                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
809                                let inner = Expr {
810                                    kind: ExprKind::CodeRef {
811                                        params: vec![],
812                                        body,
813                                    },
814                                    line: inner_line,
815                                };
816                                let expr = Expr {
817                                    kind: ExprKind::Do(Box::new(inner)),
818                                    line,
819                                };
820                                let stmt = Statement {
821                                    label: label.clone(),
822                                    kind: StmtKind::Expression(expr),
823                                    line,
824                                };
825                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
826                                self.parse_stmt_postfix_modifier(stmt)?
827                            }
828                        } else {
829                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
830                            let inner = Expr {
831                                kind: ExprKind::CodeRef {
832                                    params: vec![],
833                                    body,
834                                },
835                                line: inner_line,
836                            };
837                            let expr = Expr {
838                                kind: ExprKind::Do(Box::new(inner)),
839                                line,
840                            };
841                            let stmt = Statement {
842                                label: label.clone(),
843                                kind: StmtKind::Expression(expr),
844                                line,
845                            };
846                            self.parse_stmt_postfix_modifier(stmt)?
847                        }
848                    } else {
849                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
850                            let stmt = self.maybe_postfix_modifier(expr)?;
851                            self.parse_stmt_postfix_modifier(stmt)?
852                        } else {
853                            let expr = self.parse_expression()?;
854                            let stmt = self.maybe_postfix_modifier(expr)?;
855                            self.parse_stmt_postfix_modifier(stmt)?
856                        }
857                    }
858                }
859                _ => {
860                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
861                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
862                        let stmt = self.maybe_postfix_modifier(expr)?;
863                        self.parse_stmt_postfix_modifier(stmt)?
864                    } else {
865                        let expr = self.parse_expression()?;
866                        let stmt = self.maybe_postfix_modifier(expr)?;
867                        self.parse_stmt_postfix_modifier(stmt)?
868                    }
869                }
870            },
871            Token::LBrace => {
872                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
873                // If it looks like a hashref, parse as expression; otherwise parse as block.
874                if self.looks_like_hashref() {
875                    let expr = self.parse_expression()?;
876                    let stmt = self.maybe_postfix_modifier(expr)?;
877                    self.parse_stmt_postfix_modifier(stmt)?
878                } else {
879                    let block = self.parse_block()?;
880                    let stmt = Statement {
881                        label: None,
882                        kind: StmtKind::Block(block),
883                        line,
884                    };
885                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
886                    self.parse_stmt_postfix_modifier(stmt)?
887                }
888            }
889            _ => {
890                let expr = self.parse_expression()?;
891                let stmt = self.maybe_postfix_modifier(expr)?;
892                self.parse_stmt_postfix_modifier(stmt)?
893            }
894        };
895
896        stmt.label = label;
897        Ok(stmt)
898    }
899
900    /// Handle postfix if/unless on statement-level keywords like last/next.
901    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
902        let line = stmt.line;
903        // Implicit semicolon: a modifier keyword on a new line is a new
904        // statement, not a postfix modifier.  This prevents semicolon-less
905        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
906        // as `my $x = "val" if ($x) { ... }`.
907        if self.peek_line() > self.prev_line() {
908            self.eat(&Token::Semicolon);
909            return Ok(stmt);
910        }
911        if let Token::Ident(ref kw) = self.peek().clone() {
912            match kw.as_str() {
913                "if" => {
914                    self.advance();
915                    let mut cond = self.parse_expression()?;
916                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
917                    self.eat(&Token::Semicolon);
918                    return Ok(Statement {
919                        label: None,
920                        kind: StmtKind::If {
921                            condition: cond,
922                            body: vec![stmt],
923                            elsifs: vec![],
924                            else_block: None,
925                        },
926                        line,
927                    });
928                }
929                "unless" => {
930                    self.advance();
931                    let mut cond = self.parse_expression()?;
932                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
933                    self.eat(&Token::Semicolon);
934                    return Ok(Statement {
935                        label: None,
936                        kind: StmtKind::Unless {
937                            condition: cond,
938                            body: vec![stmt],
939                            else_block: None,
940                        },
941                        line,
942                    });
943                }
944                "while" | "until" | "for" | "foreach" => {
945                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
946                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
947                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
948                        let out = self.maybe_postfix_modifier(expr)?;
949                        self.eat(&Token::Semicolon);
950                        return Ok(out);
951                    }
952                    return Err(self.syntax_err(
953                        format!("postfix `{}` is not supported on this statement form", kw),
954                        self.peek_line(),
955                    ));
956                }
957                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
958                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
959                    let line = stmt.line;
960                    let block = self.stmt_into_parallel_block(stmt)?;
961                    let which = kw.as_str();
962                    self.advance();
963                    self.eat(&Token::Comma);
964                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
965                    self.eat(&Token::Semicolon);
966                    let list = Box::new(list);
967                    let progress = progress.map(Box::new);
968                    let kind = match which {
969                        "pmap" => ExprKind::PMapExpr {
970                            block,
971                            list,
972                            progress,
973                            flat_outputs: false,
974                            on_cluster: None,
975                            stream: false,
976                        },
977                        "pflat_map" => ExprKind::PMapExpr {
978                            block,
979                            list,
980                            progress,
981                            flat_outputs: true,
982                            on_cluster: None,
983                            stream: false,
984                        },
985                        "pgrep" => ExprKind::PGrepExpr {
986                            block,
987                            list,
988                            progress,
989                            stream: false,
990                        },
991                        "pfor" => ExprKind::PForExpr {
992                            block,
993                            list,
994                            progress,
995                        },
996                        "preduce" => ExprKind::PReduceExpr {
997                            block,
998                            list,
999                            progress,
1000                        },
1001                        "pcache" => ExprKind::PcacheExpr {
1002                            block,
1003                            list,
1004                            progress,
1005                        },
1006                        _ => unreachable!(),
1007                    };
1008                    return Ok(Statement {
1009                        label: None,
1010                        kind: StmtKind::Expression(Expr { kind, line }),
1011                        line,
1012                    });
1013                }
1014                _ => {}
1015            }
1016        }
1017        self.eat(&Token::Semicolon);
1018        Ok(stmt)
1019    }
1020
1021    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1022    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1023    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
1024        let line = stmt.line;
1025        match stmt.kind {
1026            StmtKind::Block(block) => Ok(block),
1027            StmtKind::Expression(expr) => {
1028                if let ExprKind::Do(ref inner) = expr.kind {
1029                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1030                        return Ok(body.clone());
1031                    }
1032                }
1033                Ok(vec![Statement {
1034                    label: None,
1035                    kind: StmtKind::Expression(expr),
1036                    line,
1037                }])
1038            }
1039            _ => Err(self.syntax_err(
1040                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1041                line,
1042            )),
1043        }
1044    }
1045
1046    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1047    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`](ExprKind::Do)([`CodeRef`](ExprKind::CodeRef))).
1048    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1049        match stmt.kind {
1050            StmtKind::Expression(expr) => Some(expr),
1051            StmtKind::Block(block) => {
1052                let line = stmt.line;
1053                let inner = Expr {
1054                    kind: ExprKind::CodeRef {
1055                        params: vec![],
1056                        body: block,
1057                    },
1058                    line,
1059                };
1060                Some(Expr {
1061                    kind: ExprKind::Do(Box::new(inner)),
1062                    line,
1063                })
1064            }
1065            _ => None,
1066        }
1067    }
1068
1069    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1070    /// (same set as [`parse_list_until_terminator`]).
1071    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1072        matches!(
1073            self.peek(),
1074            Token::Ident(ref kw)
1075                if matches!(
1076                    kw.as_str(),
1077                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1078                )
1079        )
1080    }
1081
1082    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1083        let line = expr.line;
1084        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1085        if self.peek_line() > self.prev_line() {
1086            return Ok(Statement {
1087                label: None,
1088                kind: StmtKind::Expression(expr),
1089                line,
1090            });
1091        }
1092        match self.peek() {
1093            Token::Ident(ref kw) => match kw.as_str() {
1094                "if" => {
1095                    self.advance();
1096                    let cond = self.parse_expression()?;
1097                    Ok(Statement {
1098                        label: None,
1099                        kind: StmtKind::Expression(Expr {
1100                            kind: ExprKind::PostfixIf {
1101                                expr: Box::new(expr),
1102                                condition: Box::new(cond),
1103                            },
1104                            line,
1105                        }),
1106                        line,
1107                    })
1108                }
1109                "unless" => {
1110                    self.advance();
1111                    let cond = self.parse_expression()?;
1112                    Ok(Statement {
1113                        label: None,
1114                        kind: StmtKind::Expression(Expr {
1115                            kind: ExprKind::PostfixUnless {
1116                                expr: Box::new(expr),
1117                                condition: Box::new(cond),
1118                            },
1119                            line,
1120                        }),
1121                        line,
1122                    })
1123                }
1124                "while" => {
1125                    self.advance();
1126                    let cond = self.parse_expression()?;
1127                    Ok(Statement {
1128                        label: None,
1129                        kind: StmtKind::Expression(Expr {
1130                            kind: ExprKind::PostfixWhile {
1131                                expr: Box::new(expr),
1132                                condition: Box::new(cond),
1133                            },
1134                            line,
1135                        }),
1136                        line,
1137                    })
1138                }
1139                "until" => {
1140                    self.advance();
1141                    let cond = self.parse_expression()?;
1142                    Ok(Statement {
1143                        label: None,
1144                        kind: StmtKind::Expression(Expr {
1145                            kind: ExprKind::PostfixUntil {
1146                                expr: Box::new(expr),
1147                                condition: Box::new(cond),
1148                            },
1149                            line,
1150                        }),
1151                        line,
1152                    })
1153                }
1154                "for" | "foreach" => {
1155                    self.advance();
1156                    let list = self.parse_expression()?;
1157                    Ok(Statement {
1158                        label: None,
1159                        kind: StmtKind::Expression(Expr {
1160                            kind: ExprKind::PostfixForeach {
1161                                expr: Box::new(expr),
1162                                list: Box::new(list),
1163                            },
1164                            line,
1165                        }),
1166                        line,
1167                    })
1168                }
1169                _ => Ok(Statement {
1170                    label: None,
1171                    kind: StmtKind::Expression(expr),
1172                    line,
1173                }),
1174            },
1175            _ => Ok(Statement {
1176                label: None,
1177                kind: StmtKind::Expression(expr),
1178                line,
1179            }),
1180        }
1181    }
1182
1183    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1184    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1185        let saved = self.pos;
1186        let line = self.peek_line();
1187        let mut name = match self.peek() {
1188            Token::Ident(n) => n.clone(),
1189            _ => return None,
1190        };
1191        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1192        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1193            return None;
1194        }
1195        self.advance();
1196        while self.eat(&Token::PackageSep) {
1197            match self.advance() {
1198                (Token::Ident(part), _) => {
1199                    name = format!("{}::{}", name, part);
1200                }
1201                _ => {
1202                    self.pos = saved;
1203                    return None;
1204                }
1205            }
1206        }
1207        match self.peek() {
1208            Token::Semicolon | Token::RBrace => Some(Expr {
1209                kind: ExprKind::FuncCall { name, args: vec![] },
1210                line,
1211            }),
1212            _ => {
1213                self.pos = saved;
1214                None
1215            }
1216        }
1217    }
1218
1219    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1220    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1221        !matches!(
1222            name,
1223            "__FILE__"
1224                | "__LINE__"
1225                | "abs"
1226                | "async"
1227                | "spawn"
1228                | "atan2"
1229                | "await"
1230                | "barrier"
1231                | "bless"
1232                | "caller"
1233                | "capture"
1234                | "cat"
1235                | "chdir"
1236                | "chmod"
1237                | "chomp"
1238                | "chop"
1239                | "chr"
1240                | "chown"
1241                | "closedir"
1242                | "close"
1243                | "collect"
1244                | "cos"
1245                | "crypt"
1246                | "defined"
1247                | "dec"
1248                | "delete"
1249                | "die"
1250                | "deque"
1251                | "do"
1252                | "each"
1253                | "eof"
1254                | "fore"
1255                | "eval"
1256                | "exec"
1257                | "exists"
1258                | "exit"
1259                | "exp"
1260                | "fan"
1261                | "fan_cap"
1262                | "fc"
1263                | "fetch_url"
1264                | "d"
1265                | "dirs"
1266                | "dr"
1267                | "f"
1268                | "fi"
1269                | "files"
1270                | "filesf"
1271                | "filter"
1272                | "fr"
1273                | "getcwd"
1274                | "glob_par"
1275                | "par_sed"
1276                | "glob"
1277                | "grep"
1278                | "greps"
1279                | "heap"
1280                | "hex"
1281                | "inc"
1282                | "index"
1283                | "int"
1284                | "join"
1285                | "keys"
1286                | "lcfirst"
1287                | "lc"
1288                | "length"
1289                | "link"
1290                | "log"
1291                | "lstat"
1292                | "map"
1293                | "flat_map"
1294                | "maps"
1295                | "flat_maps"
1296                | "flatten"
1297                | "frequencies"
1298                | "freq"
1299                | "interleave"
1300                | "ddump"
1301                | "stringify"
1302                | "str"
1303                | "s"
1304                | "input"
1305                | "lines"
1306                | "words"
1307                | "chars"
1308                | "digits"
1309                | "letters"
1310                | "letters_uc"
1311                | "letters_lc"
1312                | "punctuation"
1313                | "sentences"
1314                | "paragraphs"
1315                | "sections"
1316                | "numbers"
1317                | "graphemes"
1318                | "columns"
1319                | "trim"
1320                | "avg"
1321                | "top"
1322                | "pager"
1323                | "pg"
1324                | "less"
1325                | "count_by"
1326                | "to_file"
1327                | "to_json"
1328                | "to_csv"
1329                | "grep_v"
1330                | "select_keys"
1331                | "pluck"
1332                | "clamp"
1333                | "normalize"
1334                | "stddev"
1335                | "squared"
1336                | "square"
1337                | "cubed"
1338                | "cube"
1339                | "expt"
1340                | "pow"
1341                | "pw"
1342                | "snake_case"
1343                | "camel_case"
1344                | "kebab_case"
1345                | "to_toml"
1346                | "to_yaml"
1347                | "to_xml"
1348                | "to_html"
1349                | "to_markdown"
1350                | "xopen"
1351                | "clip"
1352                | "paste"
1353                | "to_table"
1354                | "sparkline"
1355                | "bar_chart"
1356                | "flame"
1357                | "set"
1358                | "list_count"
1359                | "list_size"
1360                | "count"
1361                | "size"
1362                | "cnt"
1363                | "len"
1364                | "all"
1365                | "any"
1366                | "none"
1367                | "take_while"
1368                | "drop_while"
1369                | "skip_while"
1370                | "skip"
1371                | "first_or"
1372                | "tap"
1373                | "peek"
1374                | "partition"
1375                | "min_by"
1376                | "max_by"
1377                | "zip_with"
1378                | "group_by"
1379                | "chunk_by"
1380                | "with_index"
1381                | "puniq"
1382                | "pfirst"
1383                | "pany"
1384                | "uniq"
1385                | "distinct"
1386                | "shuffle"
1387                | "shuffled"
1388                | "chunked"
1389                | "windowed"
1390                | "match"
1391                | "mkdir"
1392                | "every"
1393                | "gen"
1394                | "oct"
1395                | "open"
1396                | "p"
1397                | "opendir"
1398                | "ord"
1399                | "par_lines"
1400                | "par_walk"
1401                | "pipe"
1402                | "pipes"
1403                | "block_devices"
1404                | "char_devices"
1405                | "exe"
1406                | "executables"
1407                | "rate_limit"
1408                | "retry"
1409                | "pcache"
1410                | "pchannel"
1411                | "pfor"
1412                | "pgrep"
1413                | "pgreps"
1414                | "pipeline"
1415                | "pmap_chunked"
1416                | "pmap_reduce"
1417                | "pmap_on"
1418                | "pflat_map_on"
1419                | "pmap"
1420                | "pmaps"
1421                | "pflat_map"
1422                | "pflat_maps"
1423                | "pop"
1424                | "pos"
1425                | "ppool"
1426                | "preduce_init"
1427                | "preduce"
1428                | "pselect"
1429                | "printf"
1430                | "print"
1431                | "pr"
1432                | "psort"
1433                | "push"
1434                | "pwatch"
1435                | "rand"
1436                | "readdir"
1437                | "readlink"
1438                | "reduce"
1439                | "fold"
1440                | "inject"
1441                | "first"
1442                | "detect"
1443                | "find"
1444                | "find_all"
1445                | "ref"
1446                | "rename"
1447                | "require"
1448                | "rev"
1449                | "reverse"
1450                | "reversed"
1451                | "rewinddir"
1452                | "rindex"
1453                | "rmdir"
1454                | "rm"
1455                | "say"
1456                | "scalar"
1457                | "seekdir"
1458                | "shift"
1459                | "sin"
1460                | "slurp"
1461                | "sockets"
1462                | "sort"
1463                | "splice"
1464                | "split"
1465                | "sprintf"
1466                | "sqrt"
1467                | "srand"
1468                | "stat"
1469                | "study"
1470                | "substr"
1471                | "symlink"
1472                | "sym_links"
1473                | "system"
1474                | "telldir"
1475                | "timer"
1476                | "trace"
1477                | "ucfirst"
1478                | "uc"
1479                | "undef"
1480                | "umask"
1481                | "unlink"
1482                | "unshift"
1483                | "utime"
1484                | "values"
1485                | "wantarray"
1486                | "warn"
1487                | "watch"
1488                | "yield"
1489                | "sub"
1490        )
1491    }
1492
1493    fn parse_block(&mut self) -> PerlResult<Block> {
1494        self.expect(&Token::LBrace)?;
1495        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1496        // parses its own input instead of using `$_[0]` placeholder.
1497        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1498        self.pipe_rhs_depth = 0;
1499        let mut stmts = Vec::new();
1500        // `{ |$a, $b| body }` — Ruby-style block params.
1501        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1502        // or `my $p = $_N` for positional N≥3.
1503        if let Some(param_stmts) = self.try_parse_block_params()? {
1504            stmts.extend(param_stmts);
1505        }
1506        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1507            if self.eat(&Token::Semicolon) {
1508                continue;
1509            }
1510            stmts.push(self.parse_statement()?);
1511        }
1512        self.expect(&Token::RBrace)?;
1513        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1514        Self::default_topic_for_sole_bareword(&mut stmts);
1515        Ok(stmts)
1516    }
1517
1518    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1519    /// Returns `None` if the leading `|` is not block-param syntax.
1520    /// When successful, returns `my $var = <implicit>` assignment statements
1521    /// that alias the block's positional arguments.
1522    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1523        if !matches!(self.peek(), Token::BitOr) {
1524            return Ok(None);
1525        }
1526        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1527        let mut i = 1; // skip the opening `|`
1528        loop {
1529            match self.peek_at(i) {
1530                Token::ScalarVar(_) => i += 1,
1531                _ => return Ok(None), // not `|$var...|`
1532            }
1533            match self.peek_at(i) {
1534                Token::BitOr => break,  // closing `|`
1535                Token::Comma => i += 1, // more params
1536                _ => return Ok(None),   // not block params
1537            }
1538        }
1539        // Confirmed — consume and build assignments.
1540        let line = self.peek_line();
1541        self.advance(); // eat opening `|`
1542        let mut names = Vec::new();
1543        loop {
1544            if let Token::ScalarVar(ref name) = self.peek().clone() {
1545                names.push(name.clone());
1546                self.advance();
1547            }
1548            if self.eat(&Token::BitOr) {
1549                break;
1550            }
1551            self.expect(&Token::Comma)?;
1552        }
1553        // Generate `my $name = <source>` for each param.
1554        // 1 param  → source is `$_` (map/grep/each/for topic)
1555        // 2 params → sources are `$a`, `$b` (sort/reduce)
1556        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1557        let sources: Vec<&str> = match names.len() {
1558            1 => vec!["_"],
1559            2 => vec!["a", "b"],
1560            n => {
1561                // Can't return borrowed from a generated vec, handle below.
1562                let _ = n;
1563                vec![] // sentinel — handled in the else branch
1564            }
1565        };
1566        let mut stmts = Vec::with_capacity(names.len());
1567        if !sources.is_empty() {
1568            for (name, src) in names.iter().zip(sources.iter()) {
1569                stmts.push(Statement {
1570                    label: None,
1571                    kind: StmtKind::My(vec![VarDecl {
1572                        sigil: Sigil::Scalar,
1573                        name: name.clone(),
1574                        initializer: Some(Expr {
1575                            kind: ExprKind::ScalarVar(src.to_string()),
1576                            line,
1577                        }),
1578                        frozen: false,
1579                        type_annotation: None,
1580                    }]),
1581                    line,
1582                });
1583            }
1584        } else {
1585            // N≥3: positional `$_`, `$_1`, `$_2`, …
1586            for (idx, name) in names.iter().enumerate() {
1587                let src = if idx == 0 {
1588                    "_".to_string()
1589                } else {
1590                    format!("_{idx}")
1591                };
1592                stmts.push(Statement {
1593                    label: None,
1594                    kind: StmtKind::My(vec![VarDecl {
1595                        sigil: Sigil::Scalar,
1596                        name: name.clone(),
1597                        initializer: Some(Expr {
1598                            kind: ExprKind::ScalarVar(src),
1599                            line,
1600                        }),
1601                        frozen: false,
1602                        type_annotation: None,
1603                    }]),
1604                    line,
1605                });
1606            }
1607        }
1608        Ok(Some(stmts))
1609    }
1610
1611    /// Block shorthand: when the body is literally one bare builtin call
1612    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1613    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1614    ///
1615    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1616    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1617    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1618    /// empty args and return the wrong value. This rewrite levels the
1619    /// playing field at parse time — no per-builtin handling needed.
1620    ///
1621    /// Narrow by design: fires only when the block has *exactly one*
1622    /// expression statement whose sole content is a known-bareword call
1623    /// with zero args. Multi-statement blocks and blocks with any other
1624    /// content are untouched.
1625    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1626        let [only] = stmts else { return };
1627        let StmtKind::Expression(ref mut expr) = only.kind else {
1628            return;
1629        };
1630        let topic_line = expr.line;
1631        let topic_arg = || Expr {
1632            kind: ExprKind::ScalarVar("_".to_string()),
1633            line: topic_line,
1634        };
1635        match expr.kind {
1636            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1637            ExprKind::FuncCall {
1638                ref name,
1639                ref mut args,
1640            } if args.is_empty()
1641                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1642            {
1643                args.push(topic_arg());
1644            }
1645            // Lone bareword (the parser sometimes keeps a bareword as a
1646            // `Bareword` node instead of a zero-arg `FuncCall` —
1647            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1648            ExprKind::Bareword(ref name)
1649                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1650            {
1651                let n = name.clone();
1652                expr.kind = ExprKind::FuncCall {
1653                    name: n,
1654                    args: vec![topic_arg()],
1655                };
1656            }
1657            _ => {}
1658        }
1659    }
1660
1661    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1662    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
1663    /// handles specially by emitting Op::DeferBlock.
1664    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1665        let line = self.peek_line();
1666        self.advance(); // defer
1667        let body = self.parse_block()?;
1668        self.eat(&Token::Semicolon);
1669        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
1670        let coderef = Expr {
1671            kind: ExprKind::CodeRef {
1672                params: vec![],
1673                body,
1674            },
1675            line,
1676        };
1677        Ok(Statement {
1678            label: None,
1679            kind: StmtKind::Expression(Expr {
1680                kind: ExprKind::FuncCall {
1681                    name: "defer__internal".to_string(),
1682                    args: vec![coderef],
1683                },
1684                line,
1685            }),
1686            line,
1687        })
1688    }
1689
1690    /// `try { } catch ($err) { }` with optional `finally { }`
1691    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
1692        let line = self.peek_line();
1693        self.advance(); // try
1694        let try_block = self.parse_block()?;
1695        match self.peek() {
1696            Token::Ident(ref k) if k == "catch" => {
1697                self.advance();
1698            }
1699            _ => {
1700                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
1701            }
1702        }
1703        self.expect(&Token::LParen)?;
1704        let catch_var = self.parse_scalar_var_name()?;
1705        self.expect(&Token::RParen)?;
1706        let catch_block = self.parse_block()?;
1707        let finally_block = match self.peek() {
1708            Token::Ident(ref k) if k == "finally" => {
1709                self.advance();
1710                Some(self.parse_block()?)
1711            }
1712            _ => None,
1713        };
1714        self.eat(&Token::Semicolon);
1715        Ok(Statement {
1716            label: None,
1717            kind: StmtKind::TryCatch {
1718                try_block,
1719                catch_var,
1720                catch_block,
1721                finally_block,
1722            },
1723            line,
1724        })
1725    }
1726
1727    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
1728    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
1729    ///
1730    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
1731    ///
1732    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
1733    /// is not parsed from tokens — using `parse_unary()` there lets the first
1734    /// bareword greedily consume the next token as its arg, which misparses
1735    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
1736    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
1737    /// token through the stage loop, and wrap the resulting chain in a
1738    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
1739    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
1740    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> PerlResult<Expr> {
1741        // Set thread-last mode for pipe_forward_apply calls within this macro
1742        let saved_thread_last = self.thread_last_mode;
1743        self.thread_last_mode = thread_last;
1744
1745        let pipe_rhs_wrap = self.in_pipe_rhs();
1746        let mut result = if pipe_rhs_wrap {
1747            Expr {
1748                kind: ExprKind::ArrayElement {
1749                    array: "_".to_string(),
1750                    index: Box::new(Expr {
1751                        kind: ExprKind::Integer(0),
1752                        line: _line,
1753                    }),
1754                },
1755                line: _line,
1756            }
1757        } else {
1758            // Suppress paren-less function calls so `t Color::Red p` parses
1759            // the enum variant without consuming `p` as an argument.
1760            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
1761            let expr = self.parse_thread_input();
1762            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
1763            expr?
1764        };
1765
1766        // Track line where the last stage ended (initially the input expression's line).
1767        let mut last_stage_end_line = self.prev_line();
1768
1769        // Parse stages until we hit a statement terminator
1770        loop {
1771            // Newline termination: if the next token is on a different line than where
1772            // the previous stage ended, the thread macro terminates. This allows
1773            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
1774            // without requiring a semicolon.
1775            if self.peek_line() > last_stage_end_line {
1776                break;
1777            }
1778
1779            // Check for terminators - |> ends thread and allows piping the result.
1780            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
1781            // cannot be stages, so they implicitly terminate the thread macro.
1782            match self.peek() {
1783                Token::Semicolon
1784                | Token::RBrace
1785                | Token::RParen
1786                | Token::RBracket
1787                | Token::PipeForward
1788                | Token::Eof
1789                | Token::ScalarVar(_)
1790                | Token::ArrayVar(_)
1791                | Token::HashVar(_)
1792                | Token::Comma => break,
1793                Token::Ident(ref kw)
1794                    if matches!(
1795                        kw.as_str(),
1796                        "my" | "our"
1797                            | "local"
1798                            | "state"
1799                            | "if"
1800                            | "unless"
1801                            | "while"
1802                            | "until"
1803                            | "for"
1804                            | "foreach"
1805                            | "return"
1806                            | "last"
1807                            | "next"
1808                            | "redo"
1809                    ) =>
1810                {
1811                    break
1812                }
1813                _ => {}
1814            }
1815
1816            let stage_line = self.peek_line();
1817
1818            // Parse a stage and apply it to result via pipe
1819            match self.peek().clone() {
1820                // `>{ block }` — standalone anonymous block (sugar for fn { })
1821                Token::ArrowBrace => {
1822                    self.advance(); // consume `>{`
1823                    let mut stmts = Vec::new();
1824                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1825                        if self.eat(&Token::Semicolon) {
1826                            continue;
1827                        }
1828                        stmts.push(self.parse_statement()?);
1829                    }
1830                    self.expect(&Token::RBrace)?;
1831                    let code_ref = Expr {
1832                        kind: ExprKind::CodeRef {
1833                            params: vec![],
1834                            body: stmts,
1835                        },
1836                        line: stage_line,
1837                    };
1838                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1839                }
1840                // `fn { block }` — only valid in compat mode
1841                Token::Ident(ref name) if name == "sub" => {
1842                    if !crate::compat_mode() {
1843                        return Err(self.syntax_err(
1844                            "stryke uses `fn {}` instead of `fn {}` (this is not Perl 5)",
1845                            stage_line,
1846                        ));
1847                    }
1848                    self.advance(); // consume `sub`
1849                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
1850                    let body = self.parse_block()?;
1851                    let code_ref = Expr {
1852                        kind: ExprKind::CodeRef { params, body },
1853                        line: stage_line,
1854                    };
1855                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1856                }
1857                // `fn { block }` — stryke anonymous function
1858                Token::Ident(ref name) if name == "fn" => {
1859                    self.advance(); // consume `fn`
1860                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
1861                    let body = self.parse_block()?;
1862                    let code_ref = Expr {
1863                        kind: ExprKind::CodeRef { params, body },
1864                        line: stage_line,
1865                    };
1866                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1867                }
1868                // `ident` possibly followed by block
1869                Token::Ident(ref name) => {
1870                    let func_name = name.clone();
1871                    self.advance();
1872
1873                    // Handle s/// and tr/// encoded tokens
1874                    if func_name.starts_with('\x00') {
1875                        let parts: Vec<&str> = func_name.split('\x00').collect();
1876                        if parts.len() >= 4 && parts[1] == "s" {
1877                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1878                            let stage = Expr {
1879                                kind: ExprKind::Substitution {
1880                                    expr: Box::new(result.clone()),
1881                                    pattern: parts[2].to_string(),
1882                                    replacement: parts[3].to_string(),
1883                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1884                                    delim,
1885                                },
1886                                line: stage_line,
1887                            };
1888                            result = stage;
1889                            last_stage_end_line = self.prev_line();
1890                            continue;
1891                        }
1892                        if parts.len() >= 4 && parts[1] == "tr" {
1893                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1894                            let stage = Expr {
1895                                kind: ExprKind::Transliterate {
1896                                    expr: Box::new(result.clone()),
1897                                    from: parts[2].to_string(),
1898                                    to: parts[3].to_string(),
1899                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1900                                    delim,
1901                                },
1902                                line: stage_line,
1903                            };
1904                            result = stage;
1905                            last_stage_end_line = self.prev_line();
1906                            continue;
1907                        }
1908                        return Err(
1909                            self.syntax_err("Unexpected encoded token in thread", stage_line)
1910                        );
1911                    }
1912
1913                    // `map +{ ... }` — hashref expression form (not a code block).
1914                    // The `+` disambiguates: `+{` is always a hashref constructor.
1915                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
1916                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
1917                    if matches!(self.peek(), Token::Plus)
1918                        && matches!(self.peek_at(1), Token::LBrace)
1919                    {
1920                        self.advance(); // consume `+`
1921                        self.expect(&Token::LBrace)?;
1922                        // try_parse_hash_ref consumes the closing `}`
1923                        let pairs = self.try_parse_hash_ref()?;
1924                        let hashref_expr = Expr {
1925                            kind: ExprKind::HashRef(pairs),
1926                            line: stage_line,
1927                        };
1928                        let flatten_array_refs =
1929                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
1930                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
1931                        // Placeholder list — pipe_forward_apply replaces it with `result`.
1932                        let placeholder = Expr {
1933                            kind: ExprKind::Undef,
1934                            line: stage_line,
1935                        };
1936                        let map_node = Expr {
1937                            kind: ExprKind::MapExprComma {
1938                                expr: Box::new(hashref_expr),
1939                                list: Box::new(placeholder),
1940                                flatten_array_refs,
1941                                stream,
1942                            },
1943                            line: stage_line,
1944                        };
1945                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
1946                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
1947                    } else if func_name == "pmap_chunked" {
1948                        let chunk_size = self.parse_assign_expr()?;
1949                        let block = self.parse_block_or_bareword_block()?;
1950                        let placeholder = self.pipe_placeholder_list(stage_line);
1951                        let stage = Expr {
1952                            kind: ExprKind::PMapChunkedExpr {
1953                                chunk_size: Box::new(chunk_size),
1954                                block,
1955                                list: Box::new(placeholder),
1956                                progress: None,
1957                            },
1958                            line: stage_line,
1959                        };
1960                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1961                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
1962                    } else if func_name == "preduce_init" {
1963                        let init = self.parse_assign_expr()?;
1964                        let block = self.parse_block_or_bareword_block()?;
1965                        let placeholder = self.pipe_placeholder_list(stage_line);
1966                        let stage = Expr {
1967                            kind: ExprKind::PReduceInitExpr {
1968                                init: Box::new(init),
1969                                block,
1970                                list: Box::new(placeholder),
1971                                progress: None,
1972                            },
1973                            line: stage_line,
1974                        };
1975                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1976                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
1977                    } else if func_name == "pmap_reduce" {
1978                        let map_block = self.parse_block_or_bareword_block()?;
1979                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
1980                            self.parse_block()?
1981                        } else {
1982                            self.expect(&Token::Comma)?;
1983                            self.parse_block_or_bareword_cmp_block()?
1984                        };
1985                        let placeholder = self.pipe_placeholder_list(stage_line);
1986                        let stage = Expr {
1987                            kind: ExprKind::PMapReduceExpr {
1988                                map_block,
1989                                reduce_block,
1990                                list: Box::new(placeholder),
1991                                progress: None,
1992                            },
1993                            line: stage_line,
1994                        };
1995                        result = self.pipe_forward_apply(result, stage, stage_line)?;
1996                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
1997                    } else if matches!(self.peek(), Token::LBrace) {
1998                        // Parse as a block-taking builtin
1999                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2000                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2001                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2002                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2003                    } else if matches!(self.peek(), Token::LParen) {
2004                        // Special handling for join(sep) and split(pattern) in thread context.
2005                        // These take the threaded list/string as their data argument, not as $_.
2006                        if func_name == "join" {
2007                            self.advance(); // consume `(`
2008                            let separator = self.parse_assign_expr()?;
2009                            self.expect(&Token::RParen)?;
2010                            let placeholder = self.pipe_placeholder_list(stage_line);
2011                            let stage = Expr {
2012                                kind: ExprKind::JoinExpr {
2013                                    separator: Box::new(separator),
2014                                    list: Box::new(placeholder),
2015                                },
2016                                line: stage_line,
2017                            };
2018                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2019                        } else if func_name == "split" {
2020                            self.advance(); // consume `(`
2021                            let pattern = self.parse_assign_expr()?;
2022                            let limit = if self.eat(&Token::Comma) {
2023                                Some(Box::new(self.parse_assign_expr()?))
2024                            } else {
2025                                None
2026                            };
2027                            self.expect(&Token::RParen)?;
2028                            let placeholder = Expr {
2029                                kind: ExprKind::ScalarVar("_".to_string()),
2030                                line: stage_line,
2031                            };
2032                            let stage = Expr {
2033                                kind: ExprKind::SplitExpr {
2034                                    pattern: Box::new(pattern),
2035                                    string: Box::new(placeholder),
2036                                    limit,
2037                                },
2038                                line: stage_line,
2039                            };
2040                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2041                        } else {
2042                            // `name($_-bearing-args)` — parse explicit args, require at
2043                            // least one `$_` placeholder, then wrap as a `>{...}` block
2044                            // so the threaded value binds to `$_` at any position.
2045                            // Examples:
2046                            //   t 10 add2($_, 5) p      → add2(10, 5)
2047                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2048                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2049                            // To pass the threaded value as a sole arg, use bare form:
2050                            //   t 10 add2 p   (not `add2()`)
2051                            self.advance(); // consume `(`
2052                            let mut call_args = Vec::new();
2053                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2054                                call_args.push(self.parse_assign_expr()?);
2055                                if !self.eat(&Token::Comma) {
2056                                    break;
2057                                }
2058                            }
2059                            self.expect(&Token::RParen)?;
2060                            // If no `$_` placeholder, auto-inject threaded value.
2061                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2062                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2063                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2064                                let topic = Expr {
2065                                    kind: ExprKind::ScalarVar("_".to_string()),
2066                                    line: stage_line,
2067                                };
2068                                if self.thread_last_mode {
2069                                    call_args.push(topic);
2070                                } else {
2071                                    call_args.insert(0, topic);
2072                                }
2073                            }
2074                            let call_expr = Expr {
2075                                kind: ExprKind::FuncCall {
2076                                    name: func_name.clone(),
2077                                    args: call_args,
2078                                },
2079                                line: stage_line,
2080                            };
2081                            let code_ref = Expr {
2082                                kind: ExprKind::CodeRef {
2083                                    params: vec![],
2084                                    body: vec![Statement {
2085                                        label: None,
2086                                        kind: StmtKind::Expression(call_expr),
2087                                        line: stage_line,
2088                                    }],
2089                                },
2090                                line: stage_line,
2091                            };
2092                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2093                        }
2094                    } else {
2095                        // Bare function name — handle unary builtins specially
2096                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2097                    }
2098                }
2099                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2100                Token::Regex(ref pattern, ref flags, delim) => {
2101                    let pattern = pattern.clone();
2102                    let flags = flags.clone();
2103                    self.advance();
2104                    result =
2105                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2106                }
2107                // Handle `/` that was lexed as Slash (division) because it followed a term.
2108                // In thread stage context, `/pattern/` should be a regex filter.
2109                Token::Slash => {
2110                    self.advance(); // consume opening /
2111
2112                    // Special case: if next token is Ident("m") or similar followed by Regex,
2113                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2114                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2115                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2116                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2117                            && matches!(self.peek_at(1), Token::Regex(..))
2118                        {
2119                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2120                            self.advance(); // consume the ident
2121                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2122                                            // extract what would have been the closing `/` situation.
2123                                            // Actually, the lexer consumed everything. Let's just use the ident
2124                                            // as the pattern and expect a closing slash.
2125                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2126                                self.peek().clone()
2127                            {
2128                                // The misparsed regex ate our closing `/`.
2129                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2130                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2131                                // interprets as m// regex start, reads until next `/` (none) -> error.
2132                                // So we shouldn't reach here if there was an error.
2133                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2134                                // This is getting complicated. Let me try a different approach.
2135                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2136                                // Skip for now and fall through to manual parsing.
2137                                let _ = (misparsed_pattern, misparsed_flags);
2138                            }
2139                        }
2140                    }
2141
2142                    // Manually parse the regex pattern from tokens until we hit another Slash
2143                    let mut pattern = String::new();
2144                    loop {
2145                        match self.peek().clone() {
2146                            Token::Slash => {
2147                                self.advance(); // consume closing /
2148                                break;
2149                            }
2150                            Token::Eof | Token::Semicolon | Token::Newline => {
2151                                return Err(self
2152                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2153                            }
2154                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2155                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2156                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2157                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2158                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2159                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2160                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2161                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2162                                // This is a lexer bug workaround.
2163                                if pattern.is_empty()
2164                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2165                                {
2166                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2167                                    // and lexer misparsed. The Regex token is garbage.
2168                                    // Just use the ident as pattern and ignore this Regex.
2169                                    // But we already advanced past the ident...
2170                                    // This is messy. Let me try a cleaner approach.
2171                                    let _ = (inner_pattern, inner_flags, delim);
2172                                }
2173                                // For now, error out - this case is too complex
2174                                return Err(self.syntax_err(
2175                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2176                                    stage_line,
2177                                ));
2178                            }
2179                            Token::Ident(ref s) => {
2180                                pattern.push_str(s);
2181                                self.advance();
2182                            }
2183                            Token::Integer(n) => {
2184                                pattern.push_str(&n.to_string());
2185                                self.advance();
2186                            }
2187                            Token::ScalarVar(ref v) => {
2188                                pattern.push('$');
2189                                pattern.push_str(v);
2190                                self.advance();
2191                            }
2192                            Token::Dot => {
2193                                pattern.push('.');
2194                                self.advance();
2195                            }
2196                            Token::Star => {
2197                                pattern.push('*');
2198                                self.advance();
2199                            }
2200                            Token::Plus => {
2201                                pattern.push('+');
2202                                self.advance();
2203                            }
2204                            Token::Question => {
2205                                pattern.push('?');
2206                                self.advance();
2207                            }
2208                            Token::LParen => {
2209                                pattern.push('(');
2210                                self.advance();
2211                            }
2212                            Token::RParen => {
2213                                pattern.push(')');
2214                                self.advance();
2215                            }
2216                            Token::LBracket => {
2217                                pattern.push('[');
2218                                self.advance();
2219                            }
2220                            Token::RBracket => {
2221                                pattern.push(']');
2222                                self.advance();
2223                            }
2224                            Token::Backslash => {
2225                                pattern.push('\\');
2226                                self.advance();
2227                            }
2228                            Token::BitOr => {
2229                                pattern.push('|');
2230                                self.advance();
2231                            }
2232                            Token::Power => {
2233                                pattern.push_str("**");
2234                                self.advance();
2235                            }
2236                            Token::BitXor => {
2237                                pattern.push('^');
2238                                self.advance();
2239                            }
2240                            Token::Minus => {
2241                                pattern.push('-');
2242                                self.advance();
2243                            }
2244                            _ => {
2245                                return Err(self.syntax_err(
2246                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2247                                    stage_line,
2248                                ));
2249                            }
2250                        }
2251                    }
2252                    // Parse optional flags (sequence of letters after closing /)
2253                    // Be careful: single letters like 'e' could be regex flags OR thread
2254                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2255                    let mut flags = String::new();
2256                    if let Token::Ident(ref s) = self.peek().clone() {
2257                        let is_flag_only =
2258                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2259                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2260                        if is_flag_only && !followed_by_brace {
2261                            flags.push_str(s);
2262                            self.advance();
2263                        }
2264                    }
2265                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2266                }
2267                tok => {
2268                    return Err(self.syntax_err(
2269                        format!(
2270                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2271                            tok
2272                        ),
2273                        stage_line,
2274                    ));
2275                }
2276            };
2277            last_stage_end_line = self.prev_line();
2278        }
2279
2280        // Restore thread-last mode
2281        self.thread_last_mode = saved_thread_last;
2282
2283        if pipe_rhs_wrap {
2284            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2285            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2286            let body_line = result.line;
2287            return Ok(Expr {
2288                kind: ExprKind::CodeRef {
2289                    params: vec![],
2290                    body: vec![Statement {
2291                        label: None,
2292                        kind: StmtKind::Expression(result),
2293                        line: body_line,
2294                    }],
2295                },
2296                line: _line,
2297            });
2298        }
2299        Ok(result)
2300    }
2301
2302    /// Build a grep filter stage from a regex pattern for the thread macro.
2303    fn thread_regex_grep_stage(
2304        &self,
2305        list: Expr,
2306        pattern: String,
2307        flags: String,
2308        delim: char,
2309        line: usize,
2310    ) -> Expr {
2311        let topic = Expr {
2312            kind: ExprKind::ScalarVar("_".to_string()),
2313            line,
2314        };
2315        let match_expr = Expr {
2316            kind: ExprKind::Match {
2317                expr: Box::new(topic),
2318                pattern,
2319                flags,
2320                scalar_g: false,
2321                delim,
2322            },
2323            line,
2324        };
2325        let block = vec![Statement {
2326            label: None,
2327            kind: StmtKind::Expression(match_expr),
2328            line,
2329        }];
2330        Expr {
2331            kind: ExprKind::GrepExpr {
2332                block,
2333                list: Box::new(list),
2334                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2335            },
2336            line,
2337        }
2338    }
2339
2340    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2341    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2342    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2343    /// must appear in the args, otherwise the threaded value is silently dropped.
2344    ///
2345    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2346    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2347    /// per-variant walker that would need to be updated whenever new `ExprKind`
2348    /// variants are added (and would silently miss any it forgot to handle).
2349    /// Parse-time perf is non-critical and the AST is small at this scope.
2350    fn expr_contains_topic_var(e: &Expr) -> bool {
2351        format!("{:?}", e).contains("ScalarVar(\"_\")")
2352    }
2353
2354    /// Apply a bare function name in thread context, handling unary builtins specially.
2355    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2356        let kind = match name {
2357            // String functions
2358            "uc" => ExprKind::Uc(Box::new(arg)),
2359            "lc" => ExprKind::Lc(Box::new(arg)),
2360            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2361            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2362            "fc" => ExprKind::Fc(Box::new(arg)),
2363            "chomp" => ExprKind::Chomp(Box::new(arg)),
2364            "chop" => ExprKind::Chop(Box::new(arg)),
2365            "length" => ExprKind::Length(Box::new(arg)),
2366            "len" | "cnt" => ExprKind::FuncCall {
2367                name: "count".to_string(),
2368                args: vec![arg],
2369            },
2370            "quotemeta" | "qm" => ExprKind::FuncCall {
2371                name: "quotemeta".to_string(),
2372                args: vec![arg],
2373            },
2374            // Numeric functions
2375            "abs" => ExprKind::Abs(Box::new(arg)),
2376            "int" => ExprKind::Int(Box::new(arg)),
2377            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2378            "sin" => ExprKind::Sin(Box::new(arg)),
2379            "cos" => ExprKind::Cos(Box::new(arg)),
2380            "exp" => ExprKind::Exp(Box::new(arg)),
2381            "log" => ExprKind::Log(Box::new(arg)),
2382            "hex" => ExprKind::Hex(Box::new(arg)),
2383            "oct" => ExprKind::Oct(Box::new(arg)),
2384            "chr" => ExprKind::Chr(Box::new(arg)),
2385            "ord" => ExprKind::Ord(Box::new(arg)),
2386            // Type/ref functions
2387            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2388            "ref" => ExprKind::Ref(Box::new(arg)),
2389            "scalar" => ExprKind::ScalarContext(Box::new(arg)),
2390            // Array/hash functions
2391            "keys" => ExprKind::Keys(Box::new(arg)),
2392            "values" => ExprKind::Values(Box::new(arg)),
2393            "each" => ExprKind::Each(Box::new(arg)),
2394            "pop" => ExprKind::Pop(Box::new(arg)),
2395            "shift" => ExprKind::Shift(Box::new(arg)),
2396            "reverse" => {
2397                if !crate::compat_mode() {
2398                    return Err(self.syntax_err(
2399                        "stryke uses `rev` instead of `reverse` (this is not Perl 5)",
2400                        line,
2401                    ));
2402                }
2403                ExprKind::ReverseExpr(Box::new(arg))
2404            }
2405            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2406            "sort" | "so" => ExprKind::SortExpr {
2407                cmp: None,
2408                list: Box::new(arg),
2409            },
2410            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2411                name: "uniq".to_string(),
2412                args: vec![arg],
2413            },
2414            "trim" | "tm" => ExprKind::FuncCall {
2415                name: "trim".to_string(),
2416                args: vec![arg],
2417            },
2418            "flatten" | "fl" => ExprKind::FuncCall {
2419                name: "flatten".to_string(),
2420                args: vec![arg],
2421            },
2422            "compact" | "cpt" => ExprKind::FuncCall {
2423                name: "compact".to_string(),
2424                args: vec![arg],
2425            },
2426            "shuffle" | "shuf" => ExprKind::FuncCall {
2427                name: "shuffle".to_string(),
2428                args: vec![arg],
2429            },
2430            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2431                name: "frequencies".to_string(),
2432                args: vec![arg],
2433            },
2434            "dedup" | "dup" => ExprKind::FuncCall {
2435                name: "dedup".to_string(),
2436                args: vec![arg],
2437            },
2438            "enumerate" | "en" => ExprKind::FuncCall {
2439                name: "enumerate".to_string(),
2440                args: vec![arg],
2441            },
2442            "lines" | "ln" => ExprKind::FuncCall {
2443                name: "lines".to_string(),
2444                args: vec![arg],
2445            },
2446            "words" | "wd" => ExprKind::FuncCall {
2447                name: "words".to_string(),
2448                args: vec![arg],
2449            },
2450            "chars" | "ch" => ExprKind::FuncCall {
2451                name: "chars".to_string(),
2452                args: vec![arg],
2453            },
2454            "digits" | "dg" => ExprKind::FuncCall {
2455                name: "digits".to_string(),
2456                args: vec![arg],
2457            },
2458            "letters" | "lts" => ExprKind::FuncCall {
2459                name: "letters".to_string(),
2460                args: vec![arg],
2461            },
2462            "letters_uc" => ExprKind::FuncCall {
2463                name: "letters_uc".to_string(),
2464                args: vec![arg],
2465            },
2466            "letters_lc" => ExprKind::FuncCall {
2467                name: "letters_lc".to_string(),
2468                args: vec![arg],
2469            },
2470            "punctuation" | "punct" => ExprKind::FuncCall {
2471                name: "punctuation".to_string(),
2472                args: vec![arg],
2473            },
2474            "sentences" | "sents" => ExprKind::FuncCall {
2475                name: "sentences".to_string(),
2476                args: vec![arg],
2477            },
2478            "paragraphs" | "paras" => ExprKind::FuncCall {
2479                name: "paragraphs".to_string(),
2480                args: vec![arg],
2481            },
2482            "sections" | "sects" => ExprKind::FuncCall {
2483                name: "sections".to_string(),
2484                args: vec![arg],
2485            },
2486            "numbers" | "nums" => ExprKind::FuncCall {
2487                name: "numbers".to_string(),
2488                args: vec![arg],
2489            },
2490            "graphemes" | "grs" => ExprKind::FuncCall {
2491                name: "graphemes".to_string(),
2492                args: vec![arg],
2493            },
2494            "columns" | "cols" => ExprKind::FuncCall {
2495                name: "columns".to_string(),
2496                args: vec![arg],
2497            },
2498            // File functions
2499            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2500            "chdir" => ExprKind::Chdir(Box::new(arg)),
2501            "stat" => ExprKind::Stat(Box::new(arg)),
2502            "lstat" => ExprKind::Lstat(Box::new(arg)),
2503            "readlink" => ExprKind::Readlink(Box::new(arg)),
2504            "readdir" => ExprKind::Readdir(Box::new(arg)),
2505            "close" => ExprKind::Close(Box::new(arg)),
2506            "basename" | "bn" => ExprKind::FuncCall {
2507                name: "basename".to_string(),
2508                args: vec![arg],
2509            },
2510            "dirname" | "dn" => ExprKind::FuncCall {
2511                name: "dirname".to_string(),
2512                args: vec![arg],
2513            },
2514            "realpath" | "rp" => ExprKind::FuncCall {
2515                name: "realpath".to_string(),
2516                args: vec![arg],
2517            },
2518            "which" | "wh" => ExprKind::FuncCall {
2519                name: "which".to_string(),
2520                args: vec![arg],
2521            },
2522            // Other
2523            "eval" => ExprKind::Eval(Box::new(arg)),
2524            "require" => ExprKind::Require(Box::new(arg)),
2525            "study" => ExprKind::Study(Box::new(arg)),
2526            // Case conversion
2527            "snake_case" | "sc" => ExprKind::FuncCall {
2528                name: "snake_case".to_string(),
2529                args: vec![arg],
2530            },
2531            "camel_case" | "cc" => ExprKind::FuncCall {
2532                name: "camel_case".to_string(),
2533                args: vec![arg],
2534            },
2535            "kebab_case" | "kc" => ExprKind::FuncCall {
2536                name: "kebab_case".to_string(),
2537                args: vec![arg],
2538            },
2539            // Serialization
2540            "to_json" | "tj" => ExprKind::FuncCall {
2541                name: "to_json".to_string(),
2542                args: vec![arg],
2543            },
2544            "to_yaml" | "ty" => ExprKind::FuncCall {
2545                name: "to_yaml".to_string(),
2546                args: vec![arg],
2547            },
2548            "to_toml" | "tt" => ExprKind::FuncCall {
2549                name: "to_toml".to_string(),
2550                args: vec![arg],
2551            },
2552            "to_csv" | "tc" => ExprKind::FuncCall {
2553                name: "to_csv".to_string(),
2554                args: vec![arg],
2555            },
2556            "to_xml" | "tx" => ExprKind::FuncCall {
2557                name: "to_xml".to_string(),
2558                args: vec![arg],
2559            },
2560            "to_html" | "th" => ExprKind::FuncCall {
2561                name: "to_html".to_string(),
2562                args: vec![arg],
2563            },
2564            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2565                name: "to_markdown".to_string(),
2566                args: vec![arg],
2567            },
2568            "xopen" | "xo" => ExprKind::FuncCall {
2569                name: "xopen".to_string(),
2570                args: vec![arg],
2571            },
2572            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2573                name: "clip".to_string(),
2574                args: vec![arg],
2575            },
2576            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2577                name: "to_table".to_string(),
2578                args: vec![arg],
2579            },
2580            "sparkline" | "spark" => ExprKind::FuncCall {
2581                name: "sparkline".to_string(),
2582                args: vec![arg],
2583            },
2584            "bar_chart" | "bars" => ExprKind::FuncCall {
2585                name: "bar_chart".to_string(),
2586                args: vec![arg],
2587            },
2588            "flame" | "flamechart" => ExprKind::FuncCall {
2589                name: "flame".to_string(),
2590                args: vec![arg],
2591            },
2592            "ddump" | "dd" => ExprKind::FuncCall {
2593                name: "ddump".to_string(),
2594                args: vec![arg],
2595            },
2596            "say" => {
2597                if !crate::compat_mode() {
2598                    return Err(self.syntax_err(
2599                        "stryke uses `p` instead of `say` (this is not Perl 5)",
2600                        line,
2601                    ));
2602                }
2603                ExprKind::Say {
2604                    handle: None,
2605                    args: vec![arg],
2606                }
2607            }
2608            "p" => ExprKind::Say {
2609                handle: None,
2610                args: vec![arg],
2611            },
2612            "print" => ExprKind::Print {
2613                handle: None,
2614                args: vec![arg],
2615            },
2616            "warn" => ExprKind::Warn(vec![arg]),
2617            "die" => ExprKind::Die(vec![arg]),
2618            "stringify" | "str" => ExprKind::FuncCall {
2619                name: "stringify".to_string(),
2620                args: vec![arg],
2621            },
2622            "json_decode" | "jd" => ExprKind::FuncCall {
2623                name: "json_decode".to_string(),
2624                args: vec![arg],
2625            },
2626            "yaml_decode" | "yd" => ExprKind::FuncCall {
2627                name: "yaml_decode".to_string(),
2628                args: vec![arg],
2629            },
2630            "toml_decode" | "td" => ExprKind::FuncCall {
2631                name: "toml_decode".to_string(),
2632                args: vec![arg],
2633            },
2634            "xml_decode" | "xd" => ExprKind::FuncCall {
2635                name: "xml_decode".to_string(),
2636                args: vec![arg],
2637            },
2638            "json_encode" | "je" => ExprKind::FuncCall {
2639                name: "json_encode".to_string(),
2640                args: vec![arg],
2641            },
2642            "yaml_encode" | "ye" => ExprKind::FuncCall {
2643                name: "yaml_encode".to_string(),
2644                args: vec![arg],
2645            },
2646            "toml_encode" | "te" => ExprKind::FuncCall {
2647                name: "toml_encode".to_string(),
2648                args: vec![arg],
2649            },
2650            "xml_encode" | "xe" => ExprKind::FuncCall {
2651                name: "xml_encode".to_string(),
2652                args: vec![arg],
2653            },
2654            // Encoding
2655            "base64_encode" | "b64e" => ExprKind::FuncCall {
2656                name: "base64_encode".to_string(),
2657                args: vec![arg],
2658            },
2659            "base64_decode" | "b64d" => ExprKind::FuncCall {
2660                name: "base64_decode".to_string(),
2661                args: vec![arg],
2662            },
2663            "hex_encode" | "hxe" => ExprKind::FuncCall {
2664                name: "hex_encode".to_string(),
2665                args: vec![arg],
2666            },
2667            "hex_decode" | "hxd" => ExprKind::FuncCall {
2668                name: "hex_decode".to_string(),
2669                args: vec![arg],
2670            },
2671            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
2672                name: "url_encode".to_string(),
2673                args: vec![arg],
2674            },
2675            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
2676                name: "url_decode".to_string(),
2677                args: vec![arg],
2678            },
2679            "gzip" | "gz" => ExprKind::FuncCall {
2680                name: "gzip".to_string(),
2681                args: vec![arg],
2682            },
2683            "gunzip" | "ugz" => ExprKind::FuncCall {
2684                name: "gunzip".to_string(),
2685                args: vec![arg],
2686            },
2687            "zstd" | "zst" => ExprKind::FuncCall {
2688                name: "zstd".to_string(),
2689                args: vec![arg],
2690            },
2691            "zstd_decode" | "uzst" => ExprKind::FuncCall {
2692                name: "zstd_decode".to_string(),
2693                args: vec![arg],
2694            },
2695            // Crypto
2696            "sha256" | "s256" => ExprKind::FuncCall {
2697                name: "sha256".to_string(),
2698                args: vec![arg],
2699            },
2700            "sha1" | "s1" => ExprKind::FuncCall {
2701                name: "sha1".to_string(),
2702                args: vec![arg],
2703            },
2704            "md5" | "m5" => ExprKind::FuncCall {
2705                name: "md5".to_string(),
2706                args: vec![arg],
2707            },
2708            "uuid" | "uid" => ExprKind::FuncCall {
2709                name: "uuid".to_string(),
2710                args: vec![arg],
2711            },
2712            // Datetime
2713            "datetime_utc" | "utc" => ExprKind::FuncCall {
2714                name: "datetime_utc".to_string(),
2715                args: vec![arg],
2716            },
2717            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
2718            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
2719            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
2720                block: vec![Statement {
2721                    label: None,
2722                    kind: StmtKind::Expression(Expr {
2723                        kind: ExprKind::Say {
2724                            handle: None,
2725                            args: vec![Expr {
2726                                kind: ExprKind::ScalarVar("_".into()),
2727                                line,
2728                            }],
2729                        },
2730                        line,
2731                    }),
2732                    line,
2733                }],
2734                list: Box::new(arg),
2735            },
2736            // Default: generic function call
2737            _ => ExprKind::FuncCall {
2738                name: name.to_string(),
2739                args: vec![arg],
2740            },
2741        };
2742        Ok(Expr { kind, line })
2743    }
2744
2745    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
2746    /// In thread context, we only parse the block - the list comes from the piped result.
2747    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
2748        let block = self.parse_block()?;
2749        // Use a placeholder for the list - pipe_forward_apply will replace it
2750        let placeholder = self.pipe_placeholder_list(line);
2751
2752        match name {
2753            "map" | "flat_map" | "maps" | "flat_maps" => {
2754                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
2755                let stream = matches!(name, "maps" | "flat_maps");
2756                Ok(Expr {
2757                    kind: ExprKind::MapExpr {
2758                        block,
2759                        list: Box::new(placeholder),
2760                        flatten_array_refs,
2761                        stream,
2762                    },
2763                    line,
2764                })
2765            }
2766            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
2767                let keyword = match name {
2768                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
2769                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
2770                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
2771                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
2772                    _ => unreachable!(),
2773                };
2774                Ok(Expr {
2775                    kind: ExprKind::GrepExpr {
2776                        block,
2777                        list: Box::new(placeholder),
2778                        keyword,
2779                    },
2780                    line,
2781                })
2782            }
2783            "sort" | "so" => Ok(Expr {
2784                kind: ExprKind::SortExpr {
2785                    cmp: Some(SortComparator::Block(block)),
2786                    list: Box::new(placeholder),
2787                },
2788                line,
2789            }),
2790            "reduce" | "rd" => Ok(Expr {
2791                kind: ExprKind::ReduceExpr {
2792                    block,
2793                    list: Box::new(placeholder),
2794                },
2795                line,
2796            }),
2797            "fore" | "e" | "ep" => Ok(Expr {
2798                kind: ExprKind::ForEachExpr {
2799                    block,
2800                    list: Box::new(placeholder),
2801                },
2802                line,
2803            }),
2804            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
2805                kind: ExprKind::PMapExpr {
2806                    block,
2807                    list: Box::new(placeholder),
2808                    progress: None,
2809                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
2810                    on_cluster: None,
2811                    stream: name == "pmaps" || name == "pflat_maps",
2812                },
2813                line,
2814            }),
2815            "pgrep" | "pgreps" => Ok(Expr {
2816                kind: ExprKind::PGrepExpr {
2817                    block,
2818                    list: Box::new(placeholder),
2819                    progress: None,
2820                    stream: name == "pgreps",
2821                },
2822                line,
2823            }),
2824            "pfor" => Ok(Expr {
2825                kind: ExprKind::PForExpr {
2826                    block,
2827                    list: Box::new(placeholder),
2828                    progress: None,
2829                },
2830                line,
2831            }),
2832            "preduce" => Ok(Expr {
2833                kind: ExprKind::PReduceExpr {
2834                    block,
2835                    list: Box::new(placeholder),
2836                    progress: None,
2837                },
2838                line,
2839            }),
2840            "pcache" => Ok(Expr {
2841                kind: ExprKind::PcacheExpr {
2842                    block,
2843                    list: Box::new(placeholder),
2844                    progress: None,
2845                },
2846                line,
2847            }),
2848            "psort" => Ok(Expr {
2849                kind: ExprKind::PSortExpr {
2850                    cmp: Some(block),
2851                    list: Box::new(placeholder),
2852                    progress: None,
2853                },
2854                line,
2855            }),
2856            _ => {
2857                // Generic: parse block and treat as FuncCall with code ref arg
2858                let code_ref = Expr {
2859                    kind: ExprKind::CodeRef {
2860                        params: vec![],
2861                        body: block,
2862                    },
2863                    line,
2864                };
2865                Ok(Expr {
2866                    kind: ExprKind::FuncCall {
2867                        name: name.to_string(),
2868                        args: vec![code_ref],
2869                    },
2870                    line,
2871                })
2872            }
2873        }
2874    }
2875
2876    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
2877    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
2878        let line = self.peek_line();
2879        self.advance(); // tie
2880        let target = match self.peek().clone() {
2881            Token::HashVar(h) => {
2882                self.advance();
2883                TieTarget::Hash(h)
2884            }
2885            Token::ArrayVar(a) => {
2886                self.advance();
2887                TieTarget::Array(a)
2888            }
2889            Token::ScalarVar(s) => {
2890                self.advance();
2891                TieTarget::Scalar(s)
2892            }
2893            tok => {
2894                return Err(self.syntax_err(
2895                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
2896                    self.peek_line(),
2897                ));
2898            }
2899        };
2900        self.expect(&Token::Comma)?;
2901        let class = self.parse_assign_expr()?;
2902        let mut args = Vec::new();
2903        while self.eat(&Token::Comma) {
2904            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
2905                break;
2906            }
2907            args.push(self.parse_assign_expr()?);
2908        }
2909        self.eat(&Token::Semicolon);
2910        Ok(Statement {
2911            label: None,
2912            kind: StmtKind::Tie {
2913                target,
2914                class,
2915                args,
2916            },
2917            line,
2918        })
2919    }
2920
2921    /// `given (EXPR) { ... }`
2922    fn parse_given(&mut self) -> PerlResult<Statement> {
2923        let line = self.peek_line();
2924        self.advance();
2925        self.expect(&Token::LParen)?;
2926        let topic = self.parse_expression()?;
2927        self.expect(&Token::RParen)?;
2928        let body = self.parse_block()?;
2929        self.eat(&Token::Semicolon);
2930        Ok(Statement {
2931            label: None,
2932            kind: StmtKind::Given { topic, body },
2933            line,
2934        })
2935    }
2936
2937    /// `when (COND) { ... }` — only meaningful inside `given`
2938    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
2939        let line = self.peek_line();
2940        self.advance();
2941        self.expect(&Token::LParen)?;
2942        let cond = self.parse_expression()?;
2943        self.expect(&Token::RParen)?;
2944        let body = self.parse_block()?;
2945        self.eat(&Token::Semicolon);
2946        Ok(Statement {
2947            label: None,
2948            kind: StmtKind::When { cond, body },
2949            line,
2950        })
2951    }
2952
2953    /// `default { ... }` — only meaningful inside `given`
2954    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
2955        let line = self.peek_line();
2956        self.advance();
2957        let body = self.parse_block()?;
2958        self.eat(&Token::Semicolon);
2959        Ok(Statement {
2960            label: None,
2961            kind: StmtKind::DefaultCase { body },
2962            line,
2963        })
2964    }
2965
2966    /// `cond { EXPR => RESULT, ..., default => RESULT }`
2967    ///
2968    /// Desugars to an if/elsif/else chain at parse time.
2969    /// Each arm is `condition => { body }` or `condition => expr`.
2970    /// `default => ...` becomes the else branch.
2971    fn parse_cond_expr(&mut self, line: usize) -> PerlResult<Expr> {
2972        self.expect(&Token::LBrace)?;
2973
2974        let mut arms: Vec<(Expr, Block)> = Vec::new();
2975        let mut else_block: Option<Block> = None;
2976
2977        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2978            let arm_line = self.peek_line();
2979
2980            // Check for `default =>`
2981            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
2982                && matches!(self.peek_at(1), Token::FatArrow);
2983
2984            if is_default {
2985                self.advance(); // consume `default`
2986                self.advance(); // consume `=>`
2987                let body = if matches!(self.peek(), Token::LBrace) {
2988                    self.parse_block()?
2989                } else {
2990                    let expr = self.parse_assign_expr()?;
2991                    vec![Statement {
2992                        label: None,
2993                        kind: StmtKind::Expression(expr),
2994                        line: arm_line,
2995                    }]
2996                };
2997                else_block = Some(body);
2998                self.eat(&Token::Comma);
2999                break; // default must be last
3000            }
3001
3002            // Parse condition expression (stop before `=>`)
3003            let condition = self.parse_assign_expr()?;
3004            self.expect(&Token::FatArrow)?;
3005
3006            let body = if matches!(self.peek(), Token::LBrace) {
3007                self.parse_block()?
3008            } else {
3009                let expr = self.parse_assign_expr()?;
3010                vec![Statement {
3011                    label: None,
3012                    kind: StmtKind::Expression(expr),
3013                    line: arm_line,
3014                }]
3015            };
3016
3017            arms.push((condition, body));
3018            self.eat(&Token::Comma);
3019        }
3020
3021        self.expect(&Token::RBrace)?;
3022
3023        if arms.is_empty() {
3024            return Err(self.syntax_err("cond requires at least one condition arm", line));
3025        }
3026
3027        // Build if/elsif/else chain from the arms.
3028        let (first_cond, first_body) = arms.remove(0);
3029        let elsifs: Vec<(Expr, Block)> = arms;
3030
3031        // Wrap in a do-block so `cond { ... }` is an expression.
3032        let if_stmt = Statement {
3033            label: None,
3034            kind: StmtKind::If {
3035                condition: first_cond,
3036                body: first_body,
3037                elsifs,
3038                else_block,
3039            },
3040            line,
3041        };
3042        let inner = Expr {
3043            kind: ExprKind::CodeRef {
3044                params: vec![],
3045                body: vec![if_stmt],
3046            },
3047            line,
3048        };
3049        Ok(Expr {
3050            kind: ExprKind::Do(Box::new(inner)),
3051            line,
3052        })
3053    }
3054
3055    /// `match (EXPR) { PATTERN => EXPR, ... }`
3056    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
3057        self.expect(&Token::LParen)?;
3058        let subject = self.parse_expression()?;
3059        self.expect(&Token::RParen)?;
3060        self.expect(&Token::LBrace)?;
3061        let mut arms = Vec::new();
3062        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3063            if self.eat(&Token::Semicolon) {
3064                continue;
3065            }
3066            let pattern = self.parse_match_pattern()?;
3067            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3068                self.advance();
3069                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3070                // separator (see [`Self::parse_comma_expr`]).
3071                Some(Box::new(self.parse_assign_expr()?))
3072            } else {
3073                None
3074            };
3075            self.expect(&Token::FatArrow)?;
3076            // Use assign-level parsing so commas separate arms, not `List` elements.
3077            let body = self.parse_assign_expr()?;
3078            arms.push(MatchArm {
3079                pattern,
3080                guard,
3081                body,
3082            });
3083            self.eat(&Token::Comma);
3084        }
3085        self.expect(&Token::RBrace)?;
3086        Ok(Expr {
3087            kind: ExprKind::AlgebraicMatch {
3088                subject: Box::new(subject),
3089                arms,
3090            },
3091            line,
3092        })
3093    }
3094
3095    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
3096        match self.peek().clone() {
3097            Token::Regex(pattern, flags, _delim) => {
3098                self.advance();
3099                Ok(MatchPattern::Regex { pattern, flags })
3100            }
3101            Token::Ident(ref s) if s == "_" => {
3102                self.advance();
3103                Ok(MatchPattern::Any)
3104            }
3105            Token::Ident(ref s) if s == "Some" => {
3106                self.advance();
3107                self.expect(&Token::LParen)?;
3108                let name = self.parse_scalar_var_name()?;
3109                self.expect(&Token::RParen)?;
3110                Ok(MatchPattern::OptionSome(name))
3111            }
3112            Token::LBracket => self.parse_match_array_pattern(),
3113            Token::LBrace => self.parse_match_hash_pattern(),
3114            Token::LParen => {
3115                self.advance();
3116                let e = self.parse_expression()?;
3117                self.expect(&Token::RParen)?;
3118                Ok(MatchPattern::Value(Box::new(e)))
3119            }
3120            _ => {
3121                let e = self.parse_assign_expr()?;
3122                Ok(MatchPattern::Value(Box::new(e)))
3123            }
3124        }
3125    }
3126
3127    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3128    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
3129        let mut elems = Vec::new();
3130        if self.eat(&Token::RBracket) {
3131            return Ok(vec![]);
3132        }
3133        loop {
3134            if matches!(self.peek(), Token::Star) {
3135                self.advance();
3136                elems.push(MatchArrayElem::Rest);
3137                self.eat(&Token::Comma);
3138                if !matches!(self.peek(), Token::RBracket) {
3139                    return Err(self.syntax_err(
3140                        "`*` must be the last element in an array match pattern",
3141                        self.peek_line(),
3142                    ));
3143                }
3144                self.expect(&Token::RBracket)?;
3145                return Ok(elems);
3146            }
3147            if let Token::ArrayVar(name) = self.peek().clone() {
3148                self.advance();
3149                elems.push(MatchArrayElem::RestBind(name));
3150                self.eat(&Token::Comma);
3151                if !matches!(self.peek(), Token::RBracket) {
3152                    return Err(self.syntax_err(
3153                        "`@name` rest bind must be the last element in an array match pattern",
3154                        self.peek_line(),
3155                    ));
3156                }
3157                self.expect(&Token::RBracket)?;
3158                return Ok(elems);
3159            }
3160            if let Token::ScalarVar(name) = self.peek().clone() {
3161                self.advance();
3162                elems.push(MatchArrayElem::CaptureScalar(name));
3163                if self.eat(&Token::Comma) {
3164                    if matches!(self.peek(), Token::RBracket) {
3165                        break;
3166                    }
3167                    continue;
3168                }
3169                break;
3170            }
3171            let e = self.parse_assign_expr()?;
3172            elems.push(MatchArrayElem::Expr(e));
3173            if self.eat(&Token::Comma) {
3174                if matches!(self.peek(), Token::RBracket) {
3175                    break;
3176                }
3177                continue;
3178            }
3179            break;
3180        }
3181        self.expect(&Token::RBracket)?;
3182        Ok(elems)
3183    }
3184
3185    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
3186        self.expect(&Token::LBracket)?;
3187        let elems = self.parse_match_array_elems_until_rbracket()?;
3188        Ok(MatchPattern::Array(elems))
3189    }
3190
3191    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
3192        self.expect(&Token::LBrace)?;
3193        let mut pairs = Vec::new();
3194        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3195            if self.eat(&Token::Semicolon) {
3196                continue;
3197            }
3198            let key = self.parse_assign_expr()?;
3199            self.expect(&Token::FatArrow)?;
3200            match self.advance().0 {
3201                Token::Ident(ref s) if s == "_" => {
3202                    pairs.push(MatchHashPair::KeyOnly { key });
3203                }
3204                Token::ScalarVar(name) => {
3205                    pairs.push(MatchHashPair::Capture { key, name });
3206                }
3207                tok => {
3208                    return Err(self.syntax_err(
3209                        format!(
3210                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3211                            tok
3212                        ),
3213                        self.peek_line(),
3214                    ));
3215                }
3216            }
3217            self.eat(&Token::Comma);
3218        }
3219        self.expect(&Token::RBrace)?;
3220        Ok(MatchPattern::Hash(pairs))
3221    }
3222
3223    /// `eval_timeout SECS { ... }`
3224    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
3225        let line = self.peek_line();
3226        self.advance();
3227        let timeout = self.parse_postfix()?;
3228        let body = self.parse_block_or_bareword_block_no_args()?;
3229        self.eat(&Token::Semicolon);
3230        Ok(Statement {
3231            label: None,
3232            kind: StmtKind::EvalTimeout { timeout, body },
3233            line,
3234        })
3235    }
3236
3237    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3238        match &mut cond.kind {
3239            ExprKind::Match {
3240                flags, scalar_g, ..
3241            } if flags.contains('g') => {
3242                *scalar_g = true;
3243            }
3244            ExprKind::UnaryOp {
3245                op: UnaryOp::LogNot,
3246                expr,
3247            } => {
3248                if let ExprKind::Match {
3249                    flags, scalar_g, ..
3250                } = &mut expr.kind
3251                {
3252                    if flags.contains('g') {
3253                        *scalar_g = true;
3254                    }
3255                }
3256            }
3257            _ => {}
3258        }
3259    }
3260
3261    fn parse_if(&mut self) -> PerlResult<Statement> {
3262        let line = self.peek_line();
3263        self.advance(); // 'if'
3264        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3265            if crate::compat_mode() {
3266                return Err(self.syntax_err(
3267                    "`if let` is a stryke extension (disabled by --compat)",
3268                    line,
3269                ));
3270            }
3271            return self.parse_if_let(line);
3272        }
3273        self.expect(&Token::LParen)?;
3274        let mut cond = self.parse_expression()?;
3275        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3276        self.expect(&Token::RParen)?;
3277        let body = self.parse_block()?;
3278
3279        let mut elsifs = Vec::new();
3280        let mut else_block = None;
3281
3282        loop {
3283            if let Token::Ident(ref kw) = self.peek().clone() {
3284                if kw == "elsif" {
3285                    self.advance();
3286                    self.expect(&Token::LParen)?;
3287                    let mut c = self.parse_expression()?;
3288                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3289                    self.expect(&Token::RParen)?;
3290                    let b = self.parse_block()?;
3291                    elsifs.push((c, b));
3292                    continue;
3293                }
3294                if kw == "else" {
3295                    self.advance();
3296                    else_block = Some(self.parse_block()?);
3297                }
3298            }
3299            break;
3300        }
3301
3302        Ok(Statement {
3303            label: None,
3304            kind: StmtKind::If {
3305                condition: cond,
3306                body,
3307                elsifs,
3308                else_block,
3309            },
3310            line,
3311        })
3312    }
3313
3314    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3315    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3316        self.advance(); // `let`
3317        let pattern = self.parse_match_pattern()?;
3318        self.expect(&Token::Assign)?;
3319        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3320        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3321        let rhs = self.parse_assign_expr();
3322        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3323        let rhs = rhs?;
3324        let then_block = self.parse_block()?;
3325        let else_block_opt = match self.peek().clone() {
3326            Token::Ident(ref kw) if kw == "else" => {
3327                self.advance();
3328                Some(self.parse_block()?)
3329            }
3330            Token::Ident(ref kw) if kw == "elsif" => {
3331                return Err(self.syntax_err(
3332                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3333                    self.peek_line(),
3334                ));
3335            }
3336            _ => None,
3337        };
3338        let then_expr = Self::expr_do_anon_block(then_block, line);
3339        let else_expr = if let Some(eb) = else_block_opt {
3340            Self::expr_do_anon_block(eb, line)
3341        } else {
3342            Expr {
3343                kind: ExprKind::Undef,
3344                line,
3345            }
3346        };
3347        let arms = vec![
3348            MatchArm {
3349                pattern,
3350                guard: None,
3351                body: then_expr,
3352            },
3353            MatchArm {
3354                pattern: MatchPattern::Any,
3355                guard: None,
3356                body: else_expr,
3357            },
3358        ];
3359        Ok(Statement {
3360            label: None,
3361            kind: StmtKind::Expression(Expr {
3362                kind: ExprKind::AlgebraicMatch {
3363                    subject: Box::new(rhs),
3364                    arms,
3365                },
3366                line,
3367            }),
3368            line,
3369        })
3370    }
3371
3372    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3373        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3374        Expr {
3375            kind: ExprKind::Do(Box::new(Expr {
3376                kind: ExprKind::CodeRef {
3377                    params: vec![],
3378                    body: block,
3379                },
3380                line: inner_line,
3381            })),
3382            line: outer_line,
3383        }
3384    }
3385
3386    fn parse_unless(&mut self) -> PerlResult<Statement> {
3387        let line = self.peek_line();
3388        self.advance(); // 'unless'
3389        self.expect(&Token::LParen)?;
3390        let mut cond = self.parse_expression()?;
3391        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3392        self.expect(&Token::RParen)?;
3393        let body = self.parse_block()?;
3394        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3395            if kw == "else" {
3396                self.advance();
3397                Some(self.parse_block()?)
3398            } else {
3399                None
3400            }
3401        } else {
3402            None
3403        };
3404        Ok(Statement {
3405            label: None,
3406            kind: StmtKind::Unless {
3407                condition: cond,
3408                body,
3409                else_block,
3410            },
3411            line,
3412        })
3413    }
3414
3415    fn parse_while(&mut self) -> PerlResult<Statement> {
3416        let line = self.peek_line();
3417        self.advance(); // 'while'
3418        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3419            if crate::compat_mode() {
3420                return Err(self.syntax_err(
3421                    "`while let` is a stryke extension (disabled by --compat)",
3422                    line,
3423                ));
3424            }
3425            return self.parse_while_let(line);
3426        }
3427        self.expect(&Token::LParen)?;
3428        let mut cond = self.parse_expression()?;
3429        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3430        self.expect(&Token::RParen)?;
3431        let body = self.parse_block()?;
3432        let continue_block = self.parse_optional_continue_block()?;
3433        Ok(Statement {
3434            label: None,
3435            kind: StmtKind::While {
3436                condition: cond,
3437                body,
3438                label: None,
3439                continue_block,
3440            },
3441            line,
3442        })
3443    }
3444
3445    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3446    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3447    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3448        self.advance(); // `let`
3449        let pattern = self.parse_match_pattern()?;
3450        self.expect(&Token::Assign)?;
3451        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3452        let rhs = self.parse_assign_expr();
3453        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3454        let rhs = rhs?;
3455        let mut user_body = self.parse_block()?;
3456        let continue_block = self.parse_optional_continue_block()?;
3457        user_body.push(Statement::new(
3458            StmtKind::Expression(Expr {
3459                kind: ExprKind::Integer(1),
3460                line,
3461            }),
3462            line,
3463        ));
3464        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3465        let match_expr = Expr {
3466            kind: ExprKind::AlgebraicMatch {
3467                subject: Box::new(rhs),
3468                arms: vec![
3469                    MatchArm {
3470                        pattern,
3471                        guard: None,
3472                        body: Self::expr_do_anon_block(user_body, line),
3473                    },
3474                    MatchArm {
3475                        pattern: MatchPattern::Any,
3476                        guard: None,
3477                        body: Expr {
3478                            kind: ExprKind::Integer(0),
3479                            line,
3480                        },
3481                    },
3482                ],
3483            },
3484            line,
3485        };
3486        let my_stmt = Statement::new(
3487            StmtKind::My(vec![VarDecl {
3488                sigil: Sigil::Scalar,
3489                name: tmp.clone(),
3490                initializer: Some(match_expr),
3491                frozen: false,
3492                type_annotation: None,
3493            }]),
3494            line,
3495        );
3496        let unless_last = Statement::new(
3497            StmtKind::Unless {
3498                condition: Expr {
3499                    kind: ExprKind::ScalarVar(tmp),
3500                    line,
3501                },
3502                body: vec![Statement::new(StmtKind::Last(None), line)],
3503                else_block: None,
3504            },
3505            line,
3506        );
3507        Ok(Statement::new(
3508            StmtKind::While {
3509                condition: Expr {
3510                    kind: ExprKind::Integer(1),
3511                    line,
3512                },
3513                body: vec![my_stmt, unless_last],
3514                label: None,
3515                continue_block,
3516            },
3517            line,
3518        ))
3519    }
3520
3521    fn parse_until(&mut self) -> PerlResult<Statement> {
3522        let line = self.peek_line();
3523        self.advance(); // 'until'
3524        self.expect(&Token::LParen)?;
3525        let mut cond = self.parse_expression()?;
3526        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3527        self.expect(&Token::RParen)?;
3528        let body = self.parse_block()?;
3529        let continue_block = self.parse_optional_continue_block()?;
3530        Ok(Statement {
3531            label: None,
3532            kind: StmtKind::Until {
3533                condition: cond,
3534                body,
3535                label: None,
3536                continue_block,
3537            },
3538            line,
3539        })
3540    }
3541
3542    /// `continue { ... }` after a loop body (optional).
3543    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3544        if let Token::Ident(ref kw) = self.peek().clone() {
3545            if kw == "continue" {
3546                self.advance();
3547                return Ok(Some(self.parse_block()?));
3548            }
3549        }
3550        Ok(None)
3551    }
3552
3553    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3554        let line = self.peek_line();
3555        self.advance(); // 'for'
3556
3557        // Peek to determine if C-style for or foreach
3558        // C-style: for (init; cond; step)
3559        // foreach-style: for $var (list) or for (list)
3560        match self.peek() {
3561            Token::LParen => {
3562                // Check if next after ( is a semicolon or an assignment — C-style
3563                // Or if it's a list — foreach-style
3564                // Heuristic: if the token after ( is 'my' or '$' followed by
3565                // content that contains ';', it's C-style.
3566                let saved = self.pos;
3567                self.advance(); // consume (
3568                                // Look for semicolon at paren depth 0
3569                let mut depth = 1;
3570                let mut has_semi = false;
3571                let mut scan = self.pos;
3572                while scan < self.tokens.len() {
3573                    match &self.tokens[scan].0 {
3574                        Token::LParen => depth += 1,
3575                        Token::RParen => {
3576                            depth -= 1;
3577                            if depth == 0 {
3578                                break;
3579                            }
3580                        }
3581                        Token::Semicolon if depth == 1 => {
3582                            has_semi = true;
3583                            break;
3584                        }
3585                        _ => {}
3586                    }
3587                    scan += 1;
3588                }
3589                self.pos = saved;
3590
3591                if has_semi {
3592                    self.parse_c_style_for(line)
3593                } else {
3594                    // foreach without explicit var — uses $_
3595                    self.expect(&Token::LParen)?;
3596                    let list = self.parse_expression()?;
3597                    self.expect(&Token::RParen)?;
3598                    let body = self.parse_block()?;
3599                    let continue_block = self.parse_optional_continue_block()?;
3600                    Ok(Statement {
3601                        label: None,
3602                        kind: StmtKind::Foreach {
3603                            var: "_".to_string(),
3604                            list,
3605                            body,
3606                            label: None,
3607                            continue_block,
3608                        },
3609                        line,
3610                    })
3611                }
3612            }
3613            Token::Ident(ref kw) if kw == "my" => {
3614                self.advance(); // 'my'
3615                let var = self.parse_scalar_var_name()?;
3616                self.expect(&Token::LParen)?;
3617                let list = self.parse_expression()?;
3618                self.expect(&Token::RParen)?;
3619                let body = self.parse_block()?;
3620                let continue_block = self.parse_optional_continue_block()?;
3621                Ok(Statement {
3622                    label: None,
3623                    kind: StmtKind::Foreach {
3624                        var,
3625                        list,
3626                        body,
3627                        label: None,
3628                        continue_block,
3629                    },
3630                    line,
3631                })
3632            }
3633            Token::ScalarVar(_) => {
3634                let var = self.parse_scalar_var_name()?;
3635                self.expect(&Token::LParen)?;
3636                let list = self.parse_expression()?;
3637                self.expect(&Token::RParen)?;
3638                let body = self.parse_block()?;
3639                let continue_block = self.parse_optional_continue_block()?;
3640                Ok(Statement {
3641                    label: None,
3642                    kind: StmtKind::Foreach {
3643                        var,
3644                        list,
3645                        body,
3646                        label: None,
3647                        continue_block,
3648                    },
3649                    line,
3650                })
3651            }
3652            _ => self.parse_c_style_for(line),
3653        }
3654    }
3655
3656    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
3657        self.expect(&Token::LParen)?;
3658        let init = if self.eat(&Token::Semicolon) {
3659            None
3660        } else {
3661            let s = self.parse_statement()?;
3662            self.eat(&Token::Semicolon);
3663            Some(Box::new(s))
3664        };
3665        let mut condition = if matches!(self.peek(), Token::Semicolon) {
3666            None
3667        } else {
3668            Some(self.parse_expression()?)
3669        };
3670        if let Some(ref mut c) = condition {
3671            Self::mark_match_scalar_g_for_boolean_condition(c);
3672        }
3673        self.expect(&Token::Semicolon)?;
3674        let step = if matches!(self.peek(), Token::RParen) {
3675            None
3676        } else {
3677            Some(self.parse_expression()?)
3678        };
3679        self.expect(&Token::RParen)?;
3680        let body = self.parse_block()?;
3681        let continue_block = self.parse_optional_continue_block()?;
3682        Ok(Statement {
3683            label: None,
3684            kind: StmtKind::For {
3685                init,
3686                condition,
3687                step,
3688                body,
3689                label: None,
3690                continue_block,
3691            },
3692            line,
3693        })
3694    }
3695
3696    fn parse_foreach(&mut self) -> PerlResult<Statement> {
3697        let line = self.peek_line();
3698        self.advance(); // 'foreach'
3699        let var = match self.peek() {
3700            Token::Ident(ref kw) if kw == "my" => {
3701                self.advance();
3702                self.parse_scalar_var_name()?
3703            }
3704            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
3705            _ => "_".to_string(),
3706        };
3707        self.expect(&Token::LParen)?;
3708        let list = self.parse_expression()?;
3709        self.expect(&Token::RParen)?;
3710        let body = self.parse_block()?;
3711        let continue_block = self.parse_optional_continue_block()?;
3712        Ok(Statement {
3713            label: None,
3714            kind: StmtKind::Foreach {
3715                var,
3716                list,
3717                body,
3718                label: None,
3719                continue_block,
3720            },
3721            line,
3722        })
3723    }
3724
3725    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
3726        match self.advance() {
3727            (Token::ScalarVar(name), _) => Ok(name),
3728            (tok, line) => {
3729                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
3730            }
3731        }
3732    }
3733
3734    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
3735    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
3736        let mut s = String::new();
3737        loop {
3738            match self.peek().clone() {
3739                Token::RParen => {
3740                    self.advance();
3741                    break;
3742                }
3743                Token::Eof => {
3744                    return Err(self.syntax_err(
3745                        "Unterminated sub prototype (expected ')' before end of input)",
3746                        self.peek_line(),
3747                    ));
3748                }
3749                Token::ScalarVar(v) if v == ")" => {
3750                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
3751                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
3752                    self.advance();
3753                    s.push('$');
3754                    if matches!(self.peek(), Token::LBrace) {
3755                        break;
3756                    }
3757                }
3758                Token::Ident(i) => {
3759                    let i = i.clone();
3760                    self.advance();
3761                    s.push_str(&i);
3762                }
3763                Token::Semicolon => {
3764                    self.advance();
3765                    s.push(';');
3766                }
3767                Token::LParen => {
3768                    self.advance();
3769                    s.push('(');
3770                }
3771                Token::LBracket => {
3772                    self.advance();
3773                    s.push('[');
3774                }
3775                Token::RBracket => {
3776                    self.advance();
3777                    s.push(']');
3778                }
3779                Token::Backslash => {
3780                    self.advance();
3781                    s.push('\\');
3782                }
3783                Token::Comma => {
3784                    self.advance();
3785                    s.push(',');
3786                }
3787                Token::ScalarVar(v) => {
3788                    let v = v.clone();
3789                    self.advance();
3790                    s.push('$');
3791                    s.push_str(&v);
3792                }
3793                Token::ArrayVar(v) => {
3794                    let v = v.clone();
3795                    self.advance();
3796                    s.push('@');
3797                    s.push_str(&v);
3798                }
3799                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
3800                Token::ArrayAt => {
3801                    self.advance();
3802                    s.push('@');
3803                }
3804                Token::HashVar(v) => {
3805                    let v = v.clone();
3806                    self.advance();
3807                    s.push('%');
3808                    s.push_str(&v);
3809                }
3810                Token::HashPercent => {
3811                    self.advance();
3812                    s.push('%');
3813                }
3814                Token::Plus => {
3815                    self.advance();
3816                    s.push('+');
3817                }
3818                Token::Minus => {
3819                    self.advance();
3820                    s.push('-');
3821                }
3822                Token::BitAnd => {
3823                    self.advance();
3824                    s.push('&');
3825                }
3826                tok => {
3827                    return Err(self.syntax_err(
3828                        format!("Unexpected token in sub prototype: {:?}", tok),
3829                        self.peek_line(),
3830                    ));
3831                }
3832            }
3833        }
3834        Ok(s)
3835    }
3836
3837    fn sub_signature_list_starts_here(&self) -> bool {
3838        match self.peek() {
3839            Token::LBrace | Token::LBracket => true,
3840            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
3841            Token::ArrayVar(_) | Token::HashVar(_) => true,
3842            _ => false,
3843        }
3844    }
3845
3846    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
3847        let (tok, line) = self.advance();
3848        match tok {
3849            Token::Ident(i) => Ok(i),
3850            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
3851            tok => Err(self.syntax_err(
3852                format!(
3853                    "sub signature: expected hash key (identifier or string), got {:?}",
3854                    tok
3855                ),
3856                line,
3857            )),
3858        }
3859    }
3860
3861    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
3862        let mut params = Vec::new();
3863        loop {
3864            if matches!(self.peek(), Token::RParen) {
3865                break;
3866            }
3867            match self.peek().clone() {
3868                Token::ScalarVar(name) => {
3869                    if name == "$$" || name == ")" {
3870                        return Err(self.syntax_err(
3871                            format!(
3872                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
3873                            ),
3874                            self.peek_line(),
3875                        ));
3876                    }
3877                    self.advance();
3878                    let ty = if self.eat(&Token::Colon) {
3879                        match self.peek() {
3880                            Token::Ident(ref tname) => {
3881                                let tname = tname.clone();
3882                                self.advance();
3883                                Some(match tname.as_str() {
3884                                    "Int" => PerlTypeName::Int,
3885                                    "Str" => PerlTypeName::Str,
3886                                    "Float" => PerlTypeName::Float,
3887                                    "Bool" => PerlTypeName::Bool,
3888                                    "Array" => PerlTypeName::Array,
3889                                    "Hash" => PerlTypeName::Hash,
3890                                    "Ref" => PerlTypeName::Ref,
3891                                    "Any" => PerlTypeName::Any,
3892                                    _ => PerlTypeName::Struct(tname),
3893                                })
3894                            }
3895                            _ => {
3896                                return Err(self.syntax_err(
3897                                    "expected type name after `:` in sub signature",
3898                                    self.peek_line(),
3899                                ));
3900                            }
3901                        }
3902                    } else {
3903                        None
3904                    };
3905                    // Check for default value: `$x = expr`
3906                    let default = if self.eat(&Token::Assign) {
3907                        Some(Box::new(self.parse_ternary()?))
3908                    } else {
3909                        None
3910                    };
3911                    params.push(SubSigParam::Scalar(name, ty, default));
3912                }
3913                Token::ArrayVar(name) => {
3914                    self.advance();
3915                    let default = if self.eat(&Token::Assign) {
3916                        Some(Box::new(self.parse_ternary()?))
3917                    } else {
3918                        None
3919                    };
3920                    params.push(SubSigParam::Array(name, default));
3921                }
3922                Token::HashVar(name) => {
3923                    self.advance();
3924                    let default = if self.eat(&Token::Assign) {
3925                        Some(Box::new(self.parse_ternary()?))
3926                    } else {
3927                        None
3928                    };
3929                    params.push(SubSigParam::Hash(name, default));
3930                }
3931                Token::LBracket => {
3932                    self.advance();
3933                    let elems = self.parse_match_array_elems_until_rbracket()?;
3934                    params.push(SubSigParam::ArrayDestruct(elems));
3935                }
3936                Token::LBrace => {
3937                    self.advance();
3938                    let mut pairs = Vec::new();
3939                    loop {
3940                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
3941                            break;
3942                        }
3943                        if self.eat(&Token::Comma) {
3944                            continue;
3945                        }
3946                        let key = self.parse_sub_signature_hash_key()?;
3947                        self.expect(&Token::FatArrow)?;
3948                        let bind = self.parse_scalar_var_name()?;
3949                        pairs.push((key, bind));
3950                        self.eat(&Token::Comma);
3951                    }
3952                    self.expect(&Token::RBrace)?;
3953                    params.push(SubSigParam::HashDestruct(pairs));
3954                }
3955                tok => {
3956                    return Err(self.syntax_err(
3957                        format!(
3958                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
3959                            tok
3960                        ),
3961                        self.peek_line(),
3962                    ));
3963                }
3964            }
3965            match self.peek() {
3966                Token::Comma => {
3967                    self.advance();
3968                    if matches!(self.peek(), Token::RParen) {
3969                        return Err(self.syntax_err(
3970                            "trailing `,` before `)` in sub signature",
3971                            self.peek_line(),
3972                        ));
3973                    }
3974                }
3975                Token::RParen => break,
3976                _ => {
3977                    return Err(self.syntax_err(
3978                        format!(
3979                            "expected `,` or `)` after sub signature parameter, got {:?}",
3980                            self.peek()
3981                        ),
3982                        self.peek_line(),
3983                    ));
3984                }
3985            }
3986        }
3987        Ok(params)
3988    }
3989
3990    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
3991    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
3992        if !matches!(self.peek(), Token::LParen) {
3993            return Ok((vec![], None));
3994        }
3995        self.advance();
3996        if matches!(self.peek(), Token::RParen) {
3997            self.advance();
3998            return Ok((vec![], Some(String::new())));
3999        }
4000        if self.sub_signature_list_starts_here() {
4001            let params = self.parse_sub_signature_param_list()?;
4002            self.expect(&Token::RParen)?;
4003            return Ok((params, None));
4004        }
4005        let proto = self.parse_legacy_sub_prototype_tail()?;
4006        Ok((vec![], Some(proto)))
4007    }
4008
4009    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4010    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
4011        while self.eat(&Token::Colon) {
4012            match self.advance() {
4013                (Token::Ident(_), _) => {}
4014                (tok, line) => {
4015                    return Err(self.syntax_err(
4016                        format!("Expected attribute name after `:`, got {:?}", tok),
4017                        line,
4018                    ));
4019                }
4020            }
4021            if self.eat(&Token::LParen) {
4022                let mut depth = 1usize;
4023                while depth > 0 {
4024                    match self.advance().0 {
4025                        Token::LParen => depth += 1,
4026                        Token::RParen => {
4027                            depth -= 1;
4028                        }
4029                        Token::Eof => {
4030                            return Err(self.syntax_err(
4031                                "Unterminated sub attribute argument list",
4032                                self.peek_line(),
4033                            ));
4034                        }
4035                        _ => {}
4036                    }
4037                }
4038            }
4039        }
4040        Ok(())
4041    }
4042
4043    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> PerlResult<Statement> {
4044        let line = self.peek_line();
4045        self.advance(); // 'sub' or 'fn'
4046        match self.peek().clone() {
4047            Token::Ident(_) => {
4048                let name = self.parse_package_qualified_identifier()?;
4049                if !crate::compat_mode() {
4050                    self.check_udf_shadows_builtin(&name, line)?;
4051                }
4052                self.declared_subs.insert(name.clone());
4053                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4054                self.parse_sub_attributes()?;
4055                let body = self.parse_block()?;
4056                Ok(Statement {
4057                    label: None,
4058                    kind: StmtKind::SubDecl {
4059                        name,
4060                        params,
4061                        body,
4062                        prototype,
4063                    },
4064                    line,
4065                })
4066            }
4067            Token::LParen | Token::LBrace | Token::Colon => {
4068                // In non-compat mode, `fn {}` anonymous is not allowed — must use `fn {}`
4069                if is_sub_keyword && !crate::compat_mode() {
4070                    return Err(self.syntax_err(
4071                        "stryke uses `fn {}` instead of `fn {}` (this is not Perl 5)",
4072                        line,
4073                    ));
4074                }
4075                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4076                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4077                self.parse_sub_attributes()?;
4078                let body = self.parse_block()?;
4079                Ok(Statement {
4080                    label: None,
4081                    kind: StmtKind::Expression(Expr {
4082                        kind: ExprKind::CodeRef { params, body },
4083                        line,
4084                    }),
4085                    line,
4086                })
4087            }
4088            tok => Err(self.syntax_err(
4089                format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4090                self.peek_line(),
4091            )),
4092        }
4093    }
4094
4095    /// `struct Name { field => Type, ... ; fn method { } }`
4096    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
4097        let line = self.peek_line();
4098        self.advance(); // struct
4099        let name = match self.advance() {
4100            (Token::Ident(n), _) => n,
4101            (tok, err_line) => {
4102                return Err(
4103                    self.syntax_err(format!("Expected struct name, got {:?}", tok), err_line)
4104                )
4105            }
4106        };
4107        self.expect(&Token::LBrace)?;
4108        let mut fields = Vec::new();
4109        let mut methods = Vec::new();
4110        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4111            // Check for method definition: `fn name { }` or `fn name { }`
4112            let is_method = match self.peek() {
4113                Token::Ident(s) => s == "fn" || s == "sub",
4114                _ => false,
4115            };
4116            if is_method {
4117                self.advance(); // fn/sub
4118                let method_name = match self.advance() {
4119                    (Token::Ident(n), _) => n,
4120                    (tok, err_line) => {
4121                        return Err(self
4122                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4123                    }
4124                };
4125                // Parse optional signature: `($self, $arg: Type, ...)`
4126                let params = if self.eat(&Token::LParen) {
4127                    let p = self.parse_sub_signature_param_list()?;
4128                    self.expect(&Token::RParen)?;
4129                    p
4130                } else {
4131                    Vec::new()
4132                };
4133                // parse_block handles its own { } delimiters
4134                let body = self.parse_block()?;
4135                methods.push(crate::ast::StructMethod {
4136                    name: method_name,
4137                    params,
4138                    body,
4139                });
4140                // Optional trailing comma/semicolon after method
4141                self.eat(&Token::Comma);
4142                self.eat(&Token::Semicolon);
4143                continue;
4144            }
4145
4146            let field_name = match self.advance() {
4147                (Token::Ident(n), _) => n,
4148                (tok, err_line) => {
4149                    return Err(
4150                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4151                    )
4152                }
4153            };
4154            // Support both `field => Type` and bare `field` (implies Any type)
4155            let ty = if self.eat(&Token::FatArrow) {
4156                self.parse_type_name()?
4157            } else {
4158                crate::ast::PerlTypeName::Any
4159            };
4160            let default = if self.eat(&Token::Assign) {
4161                // Use parse_ternary to avoid consuming commas (next field separator)
4162                Some(self.parse_ternary()?)
4163            } else {
4164                None
4165            };
4166            fields.push(StructField {
4167                name: field_name,
4168                ty,
4169                default,
4170            });
4171            if !self.eat(&Token::Comma) {
4172                // Also allow semicolons as field separators
4173                self.eat(&Token::Semicolon);
4174            }
4175        }
4176        self.expect(&Token::RBrace)?;
4177        self.eat(&Token::Semicolon);
4178        Ok(Statement {
4179            label: None,
4180            kind: StmtKind::StructDecl {
4181                def: StructDef {
4182                    name,
4183                    fields,
4184                    methods,
4185                },
4186            },
4187            line,
4188        })
4189    }
4190
4191    /// `enum Name { Variant1, Variant2 => Type, ... }`
4192    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
4193        let line = self.peek_line();
4194        self.advance(); // enum
4195        let name = match self.advance() {
4196            (Token::Ident(n), _) => n,
4197            (tok, err_line) => {
4198                return Err(self.syntax_err(format!("Expected enum name, got {:?}", tok), err_line))
4199            }
4200        };
4201        self.expect(&Token::LBrace)?;
4202        let mut variants = Vec::new();
4203        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4204            let variant_name = match self.advance() {
4205                (Token::Ident(n), _) => n,
4206                (tok, err_line) => {
4207                    return Err(
4208                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
4209                    )
4210                }
4211            };
4212            let ty = if self.eat(&Token::FatArrow) {
4213                Some(self.parse_type_name()?)
4214            } else {
4215                None
4216            };
4217            variants.push(EnumVariant {
4218                name: variant_name,
4219                ty,
4220            });
4221            if !self.eat(&Token::Comma) {
4222                self.eat(&Token::Semicolon);
4223            }
4224        }
4225        self.expect(&Token::RBrace)?;
4226        self.eat(&Token::Semicolon);
4227        Ok(Statement {
4228            label: None,
4229            kind: StmtKind::EnumDecl {
4230                def: EnumDef { name, variants },
4231            },
4232            line,
4233        })
4234    }
4235
4236    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
4237    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
4238        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
4239        let line = self.peek_line();
4240        self.advance(); // class
4241        let name = match self.advance() {
4242            (Token::Ident(n), _) => n,
4243            (tok, err_line) => {
4244                return Err(self.syntax_err(format!("Expected class name, got {:?}", tok), err_line))
4245            }
4246        };
4247
4248        // Parse `extends Parent1, Parent2`
4249        let mut extends = Vec::new();
4250        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
4251            self.advance(); // extends
4252            loop {
4253                match self.advance() {
4254                    (Token::Ident(parent), _) => extends.push(parent),
4255                    (tok, err_line) => {
4256                        return Err(self.syntax_err(
4257                            format!("Expected parent class name after `extends`, got {:?}", tok),
4258                            err_line,
4259                        ))
4260                    }
4261                }
4262                if !self.eat(&Token::Comma) {
4263                    break;
4264                }
4265            }
4266        }
4267
4268        // Parse `impl Trait1, Trait2`
4269        let mut implements = Vec::new();
4270        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
4271            self.advance(); // impl
4272            loop {
4273                match self.advance() {
4274                    (Token::Ident(trait_name), _) => implements.push(trait_name),
4275                    (tok, err_line) => {
4276                        return Err(self.syntax_err(
4277                            format!("Expected trait name after `impl`, got {:?}", tok),
4278                            err_line,
4279                        ))
4280                    }
4281                }
4282                if !self.eat(&Token::Comma) {
4283                    break;
4284                }
4285            }
4286        }
4287
4288        self.expect(&Token::LBrace)?;
4289        let mut fields = Vec::new();
4290        let mut methods = Vec::new();
4291        let mut static_fields = Vec::new();
4292
4293        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4294            // Check for visibility modifier
4295            let visibility = match self.peek() {
4296                Token::Ident(ref s) if s == "pub" => {
4297                    self.advance();
4298                    Visibility::Public
4299                }
4300                Token::Ident(ref s) if s == "priv" => {
4301                    self.advance();
4302                    Visibility::Private
4303                }
4304                Token::Ident(ref s) if s == "prot" => {
4305                    self.advance();
4306                    Visibility::Protected
4307                }
4308                _ => Visibility::Public, // default public
4309            };
4310
4311            // Check for static field: `static name: Type = default`
4312            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
4313                self.advance(); // static
4314
4315                // Could be a static method (`static fn`) or static field
4316                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4317                    // static fn is same as fn Self.name — handled below but not here
4318                    return Err(self.syntax_err(
4319                        "use `fn Self.name` for static methods, not `static fn`",
4320                        self.peek_line(),
4321                    ));
4322                }
4323
4324                let field_name = match self.advance() {
4325                    (Token::Ident(n), _) => n,
4326                    (tok, err_line) => {
4327                        return Err(self.syntax_err(
4328                            format!("Expected static field name, got {:?}", tok),
4329                            err_line,
4330                        ))
4331                    }
4332                };
4333
4334                let ty = if self.eat(&Token::Colon) {
4335                    self.parse_type_name()?
4336                } else {
4337                    crate::ast::PerlTypeName::Any
4338                };
4339
4340                let default = if self.eat(&Token::Assign) {
4341                    Some(self.parse_ternary()?)
4342                } else {
4343                    None
4344                };
4345
4346                static_fields.push(ClassStaticField {
4347                    name: field_name,
4348                    ty,
4349                    visibility,
4350                    default,
4351                });
4352
4353                if !self.eat(&Token::Comma) {
4354                    self.eat(&Token::Semicolon);
4355                }
4356                continue;
4357            }
4358
4359            // Check for `final` modifier before fn
4360            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
4361            if method_is_final {
4362                self.advance(); // final
4363            }
4364
4365            // Check for method: `fn name` or `fn Self.name` (static)
4366            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
4367            if is_method {
4368                self.advance(); // fn/sub
4369
4370                // Check for static method: `fn Self.name`
4371                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
4372                if is_static {
4373                    self.advance(); // Self
4374                    self.expect(&Token::Dot)?;
4375                }
4376
4377                let method_name = match self.advance() {
4378                    (Token::Ident(n), _) => n,
4379                    (tok, err_line) => {
4380                        return Err(self
4381                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4382                    }
4383                };
4384
4385                // Parse optional signature
4386                let params = if self.eat(&Token::LParen) {
4387                    let p = self.parse_sub_signature_param_list()?;
4388                    self.expect(&Token::RParen)?;
4389                    p
4390                } else {
4391                    Vec::new()
4392                };
4393
4394                // Body is optional (abstract method in trait has no body)
4395                let body = if matches!(self.peek(), Token::LBrace) {
4396                    Some(self.parse_block()?)
4397                } else {
4398                    None
4399                };
4400
4401                methods.push(ClassMethod {
4402                    name: method_name,
4403                    params,
4404                    body,
4405                    visibility,
4406                    is_static,
4407                    is_final: method_is_final,
4408                });
4409                self.eat(&Token::Comma);
4410                self.eat(&Token::Semicolon);
4411                continue;
4412            } else if method_is_final {
4413                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
4414            }
4415
4416            // Parse field: `name: Type = default`
4417            let field_name = match self.advance() {
4418                (Token::Ident(n), _) => n,
4419                (tok, err_line) => {
4420                    return Err(
4421                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4422                    )
4423                }
4424            };
4425
4426            // Type after colon: `name: Type`
4427            let ty = if self.eat(&Token::Colon) {
4428                self.parse_type_name()?
4429            } else {
4430                crate::ast::PerlTypeName::Any
4431            };
4432
4433            // Default value after `=`
4434            let default = if self.eat(&Token::Assign) {
4435                Some(self.parse_ternary()?)
4436            } else {
4437                None
4438            };
4439
4440            fields.push(ClassField {
4441                name: field_name,
4442                ty,
4443                visibility,
4444                default,
4445            });
4446
4447            if !self.eat(&Token::Comma) {
4448                self.eat(&Token::Semicolon);
4449            }
4450        }
4451
4452        self.expect(&Token::RBrace)?;
4453        self.eat(&Token::Semicolon);
4454
4455        Ok(Statement {
4456            label: None,
4457            kind: StmtKind::ClassDecl {
4458                def: ClassDef {
4459                    name,
4460                    is_abstract,
4461                    is_final,
4462                    extends,
4463                    implements,
4464                    fields,
4465                    methods,
4466                    static_fields,
4467                },
4468            },
4469            line,
4470        })
4471    }
4472
4473    /// `trait Name { fn required; fn with_default { } }`
4474    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
4475        use crate::ast::{ClassMethod, TraitDef, Visibility};
4476        let line = self.peek_line();
4477        self.advance(); // trait
4478        let name = match self.advance() {
4479            (Token::Ident(n), _) => n,
4480            (tok, err_line) => {
4481                return Err(self.syntax_err(format!("Expected trait name, got {:?}", tok), err_line))
4482            }
4483        };
4484
4485        self.expect(&Token::LBrace)?;
4486        let mut methods = Vec::new();
4487
4488        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4489            // Optional visibility
4490            let visibility = match self.peek() {
4491                Token::Ident(ref s) if s == "pub" => {
4492                    self.advance();
4493                    Visibility::Public
4494                }
4495                Token::Ident(ref s) if s == "priv" => {
4496                    self.advance();
4497                    Visibility::Private
4498                }
4499                Token::Ident(ref s) if s == "prot" => {
4500                    self.advance();
4501                    Visibility::Protected
4502                }
4503                _ => Visibility::Public,
4504            };
4505
4506            // Expect `fn` or `sub`
4507            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4508                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
4509            }
4510            self.advance(); // fn/sub
4511
4512            let method_name = match self.advance() {
4513                (Token::Ident(n), _) => n,
4514                (tok, err_line) => {
4515                    return Err(
4516                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
4517                    )
4518                }
4519            };
4520
4521            // Optional signature
4522            let params = if self.eat(&Token::LParen) {
4523                let p = self.parse_sub_signature_param_list()?;
4524                self.expect(&Token::RParen)?;
4525                p
4526            } else {
4527                Vec::new()
4528            };
4529
4530            // Body is optional (no body = abstract/required method)
4531            let body = if matches!(self.peek(), Token::LBrace) {
4532                Some(self.parse_block()?)
4533            } else {
4534                None
4535            };
4536
4537            methods.push(ClassMethod {
4538                name: method_name,
4539                params,
4540                body,
4541                visibility,
4542                is_static: false,
4543                is_final: false,
4544            });
4545
4546            self.eat(&Token::Comma);
4547            self.eat(&Token::Semicolon);
4548        }
4549
4550        self.expect(&Token::RBrace)?;
4551        self.eat(&Token::Semicolon);
4552
4553        Ok(Statement {
4554            label: None,
4555            kind: StmtKind::TraitDecl {
4556                def: TraitDef { name, methods },
4557            },
4558            line,
4559        })
4560    }
4561
4562    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
4563        match &target.kind {
4564            ExprKind::ScalarVar(name) => Some(VarDecl {
4565                sigil: Sigil::Scalar,
4566                name: name.clone(),
4567                initializer: None,
4568                frozen: false,
4569                type_annotation: None,
4570            }),
4571            ExprKind::ArrayVar(name) => Some(VarDecl {
4572                sigil: Sigil::Array,
4573                name: name.clone(),
4574                initializer: None,
4575                frozen: false,
4576                type_annotation: None,
4577            }),
4578            ExprKind::HashVar(name) => Some(VarDecl {
4579                sigil: Sigil::Hash,
4580                name: name.clone(),
4581                initializer: None,
4582                frozen: false,
4583                type_annotation: None,
4584            }),
4585            ExprKind::Typeglob(name) => Some(VarDecl {
4586                sigil: Sigil::Typeglob,
4587                name: name.clone(),
4588                initializer: None,
4589                frozen: false,
4590                type_annotation: None,
4591            }),
4592            _ => None,
4593        }
4594    }
4595
4596    fn parse_decl_array_destructure(
4597        &mut self,
4598        keyword: &str,
4599        line: usize,
4600    ) -> PerlResult<Statement> {
4601        self.expect(&Token::LBracket)?;
4602        let elems = self.parse_match_array_elems_until_rbracket()?;
4603        self.expect(&Token::Assign)?;
4604        self.suppress_scalar_hash_brace += 1;
4605        let rhs = self.parse_expression()?;
4606        self.suppress_scalar_hash_brace -= 1;
4607        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
4608        self.parse_stmt_postfix_modifier(stmt)
4609    }
4610
4611    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
4612        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
4613            unreachable!("parse_match_hash_pattern returns Hash");
4614        };
4615        self.expect(&Token::Assign)?;
4616        self.suppress_scalar_hash_brace += 1;
4617        let rhs = self.parse_expression()?;
4618        self.suppress_scalar_hash_brace -= 1;
4619        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
4620        self.parse_stmt_postfix_modifier(stmt)
4621    }
4622
4623    fn desugar_array_destructure(
4624        &mut self,
4625        keyword: &str,
4626        line: usize,
4627        elems: Vec<MatchArrayElem>,
4628        rhs: Expr,
4629    ) -> PerlResult<Statement> {
4630        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4631        let mut stmts: Vec<Statement> = Vec::new();
4632        stmts.push(destructure_stmt_from_var_decls(
4633            keyword,
4634            vec![VarDecl {
4635                sigil: Sigil::Scalar,
4636                name: tmp.clone(),
4637                initializer: Some(rhs),
4638                frozen: false,
4639                type_annotation: None,
4640            }],
4641            line,
4642        ));
4643
4644        let has_rest = elems
4645            .iter()
4646            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
4647        let fixed_slots = elems
4648            .iter()
4649            .filter(|e| {
4650                matches!(
4651                    e,
4652                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
4653                )
4654            })
4655            .count();
4656        if !has_rest {
4657            let cond = Expr {
4658                kind: ExprKind::BinOp {
4659                    left: Box::new(destructure_expr_array_len(&tmp, line)),
4660                    op: BinOp::NumEq,
4661                    right: Box::new(Expr {
4662                        kind: ExprKind::Integer(fixed_slots as i64),
4663                        line,
4664                    }),
4665                },
4666                line,
4667            };
4668            stmts.push(destructure_stmt_unless_die(
4669                line,
4670                cond,
4671                "array destructure: length mismatch",
4672            ));
4673        }
4674
4675        let mut idx: i64 = 0;
4676        for elem in elems {
4677            match elem {
4678                MatchArrayElem::Rest => break,
4679                MatchArrayElem::RestBind(name) => {
4680                    let list_source = Expr {
4681                        kind: ExprKind::Deref {
4682                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4683                            kind: Sigil::Array,
4684                        },
4685                        line,
4686                    };
4687                    let last_ix = Expr {
4688                        kind: ExprKind::BinOp {
4689                            left: Box::new(destructure_expr_array_len(&tmp, line)),
4690                            op: BinOp::Sub,
4691                            right: Box::new(Expr {
4692                                kind: ExprKind::Integer(1),
4693                                line,
4694                            }),
4695                        },
4696                        line,
4697                    };
4698                    let range = Expr {
4699                        kind: ExprKind::Range {
4700                            from: Box::new(Expr {
4701                                kind: ExprKind::Integer(idx),
4702                                line,
4703                            }),
4704                            to: Box::new(last_ix),
4705                            exclusive: false,
4706                            step: None,
4707                        },
4708                        line,
4709                    };
4710                    let slice = Expr {
4711                        kind: ExprKind::AnonymousListSlice {
4712                            source: Box::new(list_source),
4713                            indices: vec![range],
4714                        },
4715                        line,
4716                    };
4717                    stmts.push(destructure_stmt_from_var_decls(
4718                        keyword,
4719                        vec![VarDecl {
4720                            sigil: Sigil::Array,
4721                            name,
4722                            initializer: Some(slice),
4723                            frozen: false,
4724                            type_annotation: None,
4725                        }],
4726                        line,
4727                    ));
4728                    break;
4729                }
4730                MatchArrayElem::CaptureScalar(name) => {
4731                    let arrow = Expr {
4732                        kind: ExprKind::ArrowDeref {
4733                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4734                            index: Box::new(Expr {
4735                                kind: ExprKind::Integer(idx),
4736                                line,
4737                            }),
4738                            kind: DerefKind::Array,
4739                        },
4740                        line,
4741                    };
4742                    stmts.push(destructure_stmt_from_var_decls(
4743                        keyword,
4744                        vec![VarDecl {
4745                            sigil: Sigil::Scalar,
4746                            name,
4747                            initializer: Some(arrow),
4748                            frozen: false,
4749                            type_annotation: None,
4750                        }],
4751                        line,
4752                    ));
4753                    idx += 1;
4754                }
4755                MatchArrayElem::Expr(e) => {
4756                    let elem_subj = Expr {
4757                        kind: ExprKind::ArrowDeref {
4758                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4759                            index: Box::new(Expr {
4760                                kind: ExprKind::Integer(idx),
4761                                line,
4762                            }),
4763                            kind: DerefKind::Array,
4764                        },
4765                        line,
4766                    };
4767                    let match_expr = Expr {
4768                        kind: ExprKind::AlgebraicMatch {
4769                            subject: Box::new(elem_subj),
4770                            arms: vec![
4771                                MatchArm {
4772                                    pattern: MatchPattern::Value(Box::new(e.clone())),
4773                                    guard: None,
4774                                    body: Expr {
4775                                        kind: ExprKind::Integer(0),
4776                                        line,
4777                                    },
4778                                },
4779                                MatchArm {
4780                                    pattern: MatchPattern::Any,
4781                                    guard: None,
4782                                    body: Expr {
4783                                        kind: ExprKind::Die(vec![Expr {
4784                                            kind: ExprKind::String(
4785                                                "array destructure: element pattern mismatch"
4786                                                    .to_string(),
4787                                            ),
4788                                            line,
4789                                        }]),
4790                                        line,
4791                                    },
4792                                },
4793                            ],
4794                        },
4795                        line,
4796                    };
4797                    stmts.push(Statement {
4798                        label: None,
4799                        kind: StmtKind::Expression(match_expr),
4800                        line,
4801                    });
4802                    idx += 1;
4803                }
4804            }
4805        }
4806
4807        Ok(Statement {
4808            label: None,
4809            kind: StmtKind::StmtGroup(stmts),
4810            line,
4811        })
4812    }
4813
4814    fn desugar_hash_destructure(
4815        &mut self,
4816        keyword: &str,
4817        line: usize,
4818        pairs: Vec<MatchHashPair>,
4819        rhs: Expr,
4820    ) -> PerlResult<Statement> {
4821        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4822        let mut stmts: Vec<Statement> = Vec::new();
4823        stmts.push(destructure_stmt_from_var_decls(
4824            keyword,
4825            vec![VarDecl {
4826                sigil: Sigil::Scalar,
4827                name: tmp.clone(),
4828                initializer: Some(rhs),
4829                frozen: false,
4830                type_annotation: None,
4831            }],
4832            line,
4833        ));
4834
4835        for pair in pairs {
4836            match pair {
4837                MatchHashPair::KeyOnly { key } => {
4838                    let exists_op = Expr {
4839                        kind: ExprKind::Exists(Box::new(Expr {
4840                            kind: ExprKind::ArrowDeref {
4841                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4842                                index: Box::new(key),
4843                                kind: DerefKind::Hash,
4844                            },
4845                            line,
4846                        })),
4847                        line,
4848                    };
4849                    stmts.push(destructure_stmt_unless_die(
4850                        line,
4851                        exists_op,
4852                        "hash destructure: missing required key",
4853                    ));
4854                }
4855                MatchHashPair::Capture { key, name } => {
4856                    let init = Expr {
4857                        kind: ExprKind::ArrowDeref {
4858                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4859                            index: Box::new(key),
4860                            kind: DerefKind::Hash,
4861                        },
4862                        line,
4863                    };
4864                    stmts.push(destructure_stmt_from_var_decls(
4865                        keyword,
4866                        vec![VarDecl {
4867                            sigil: Sigil::Scalar,
4868                            name,
4869                            initializer: Some(init),
4870                            frozen: false,
4871                            type_annotation: None,
4872                        }],
4873                        line,
4874                    ));
4875                }
4876            }
4877        }
4878
4879        Ok(Statement {
4880            label: None,
4881            kind: StmtKind::StmtGroup(stmts),
4882            line,
4883        })
4884    }
4885
4886    fn parse_my_our_local(
4887        &mut self,
4888        keyword: &str,
4889        allow_type_annotation: bool,
4890    ) -> PerlResult<Statement> {
4891        let line = self.peek_line();
4892        self.advance(); // 'my'/'our'/'local'
4893
4894        if keyword == "local"
4895            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
4896        {
4897            let target = self.parse_postfix()?;
4898            let mut initializer: Option<Expr> = None;
4899            if self.eat(&Token::Assign) {
4900                initializer = Some(self.parse_expression()?);
4901            } else if matches!(
4902                self.peek(),
4903                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
4904            ) {
4905                if matches!(&target.kind, ExprKind::Typeglob(_)) {
4906                    return Err(self.syntax_err(
4907                        "compound assignment on typeglob declaration is not supported",
4908                        self.peek_line(),
4909                    ));
4910                }
4911                let op = match self.peek().clone() {
4912                    Token::OrAssign => BinOp::LogOr,
4913                    Token::DefinedOrAssign => BinOp::DefinedOr,
4914                    Token::AndAssign => BinOp::LogAnd,
4915                    _ => unreachable!(),
4916                };
4917                self.advance();
4918                let rhs = self.parse_assign_expr()?;
4919                let tgt_line = target.line;
4920                initializer = Some(Expr {
4921                    kind: ExprKind::CompoundAssign {
4922                        target: Box::new(target.clone()),
4923                        op,
4924                        value: Box::new(rhs),
4925                    },
4926                    line: tgt_line,
4927                });
4928            }
4929
4930            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
4931                decl.initializer = initializer;
4932                StmtKind::Local(vec![decl])
4933            } else {
4934                StmtKind::LocalExpr {
4935                    target,
4936                    initializer,
4937                }
4938            };
4939            let stmt = Statement {
4940                label: None,
4941                kind,
4942                line,
4943            };
4944            return self.parse_stmt_postfix_modifier(stmt);
4945        }
4946
4947        if matches!(self.peek(), Token::LBracket) {
4948            return self.parse_decl_array_destructure(keyword, line);
4949        }
4950        if matches!(self.peek(), Token::LBrace) {
4951            return self.parse_decl_hash_destructure(keyword, line);
4952        }
4953
4954        let mut decls = Vec::new();
4955
4956        if self.eat(&Token::LParen) {
4957            // my ($a, @b, %c)
4958            while !matches!(self.peek(), Token::RParen | Token::Eof) {
4959                let decl = self.parse_var_decl(allow_type_annotation)?;
4960                decls.push(decl);
4961                if !self.eat(&Token::Comma) {
4962                    break;
4963                }
4964            }
4965            self.expect(&Token::RParen)?;
4966        } else {
4967            decls.push(self.parse_var_decl(allow_type_annotation)?);
4968        }
4969
4970        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
4971        if self.eat(&Token::Assign) {
4972            if keyword == "our" && decls.len() == 1 {
4973                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
4974                    self.advance();
4975                    decls.push(self.parse_var_decl(allow_type_annotation)?);
4976                    if !self.eat(&Token::Assign) {
4977                        return Err(self.syntax_err(
4978                            "expected `=` after `our` in chained our-declaration",
4979                            self.peek_line(),
4980                        ));
4981                    }
4982                }
4983            }
4984            let val = self.parse_expression()?;
4985            // Validate assignment for single variable declarations (not destructuring)
4986            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
4987            if !crate::compat_mode() && decls.len() == 1 {
4988                let decl = &decls[0];
4989                let target_kind = match decl.sigil {
4990                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
4991                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
4992                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
4993                    Sigil::Typeglob => {
4994                        // Skip validation for typeglob
4995                        if decls.len() == 1 {
4996                            decls[0].initializer = Some(val);
4997                        } else {
4998                            for d in &mut decls {
4999                                d.initializer = Some(val.clone());
5000                            }
5001                        }
5002                        return Ok(Statement {
5003                            label: None,
5004                            kind: match keyword {
5005                                "my" => StmtKind::My(decls),
5006                                "mysync" => StmtKind::MySync(decls),
5007                                "our" => StmtKind::Our(decls),
5008                                "local" => StmtKind::Local(decls),
5009                                "state" => StmtKind::State(decls),
5010                                _ => unreachable!(),
5011                            },
5012                            line,
5013                        });
5014                    }
5015                };
5016                let target = Expr {
5017                    kind: target_kind,
5018                    line,
5019                };
5020                self.validate_assignment(&target, &val, line)?;
5021            }
5022            if decls.len() == 1 {
5023                decls[0].initializer = Some(val);
5024            } else {
5025                for decl in &mut decls {
5026                    decl.initializer = Some(val.clone());
5027                }
5028            }
5029        } else if decls.len() == 1 {
5030            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5031            let op = match self.peek().clone() {
5032                Token::OrAssign => Some(BinOp::LogOr),
5033                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5034                Token::AndAssign => Some(BinOp::LogAnd),
5035                _ => None,
5036            };
5037            if let Some(op) = op {
5038                let d = &decls[0];
5039                if matches!(d.sigil, Sigil::Typeglob) {
5040                    return Err(self.syntax_err(
5041                        "compound assignment on typeglob declaration is not supported",
5042                        self.peek_line(),
5043                    ));
5044                }
5045                self.advance();
5046                let rhs = self.parse_assign_expr()?;
5047                let target = Expr {
5048                    kind: match d.sigil {
5049                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5050                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
5051                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
5052                        Sigil::Typeglob => unreachable!(),
5053                    },
5054                    line,
5055                };
5056                decls[0].initializer = Some(Expr {
5057                    kind: ExprKind::CompoundAssign {
5058                        target: Box::new(target),
5059                        op,
5060                        value: Box::new(rhs),
5061                    },
5062                    line,
5063                });
5064            }
5065        }
5066
5067        let kind = match keyword {
5068            "my" => StmtKind::My(decls),
5069            "mysync" => StmtKind::MySync(decls),
5070            "our" => StmtKind::Our(decls),
5071            "local" => StmtKind::Local(decls),
5072            "state" => StmtKind::State(decls),
5073            _ => unreachable!(),
5074        };
5075        let stmt = Statement {
5076            label: None,
5077            kind,
5078            line,
5079        };
5080        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
5081        self.parse_stmt_postfix_modifier(stmt)
5082    }
5083
5084    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
5085        let mut decl = match self.advance() {
5086            (Token::ScalarVar(name), _) => VarDecl {
5087                sigil: Sigil::Scalar,
5088                name,
5089                initializer: None,
5090                frozen: false,
5091                type_annotation: None,
5092            },
5093            (Token::ArrayVar(name), _) => VarDecl {
5094                sigil: Sigil::Array,
5095                name,
5096                initializer: None,
5097                frozen: false,
5098                type_annotation: None,
5099            },
5100            (Token::HashVar(name), line) => {
5101                if !crate::compat_mode() {
5102                    self.check_hash_shadows_reserved(&name, line)?;
5103                }
5104                VarDecl {
5105                    sigil: Sigil::Hash,
5106                    name,
5107                    initializer: None,
5108                    frozen: false,
5109                    type_annotation: None,
5110                }
5111            }
5112            (Token::Star, _line) => {
5113                let name = match self.advance() {
5114                    (Token::Ident(n), _) => n,
5115                    (tok, l) => {
5116                        return Err(self
5117                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
5118                    }
5119                };
5120                VarDecl {
5121                    sigil: Sigil::Typeglob,
5122                    name,
5123                    initializer: None,
5124                    frozen: false,
5125                    type_annotation: None,
5126                }
5127            }
5128            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
5129            // slot in a list assignment. The interpreter treats `undef`-named
5130            // scalar decls as throwaway: declared into a unique sink so the
5131            // distribute-to-decls loop advances past the slot.
5132            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
5133                sigil: Sigil::Scalar,
5134                // Synthesize a name that user code cannot reference. Each
5135                // sink slot in a list-assign gets its own unique name so the
5136                // declarations don't collide.
5137                name: format!("__undef_sink_{}", self.pos),
5138                initializer: None,
5139                frozen: false,
5140                type_annotation: None,
5141            },
5142            (tok, line) => {
5143                return Err(self.syntax_err(
5144                    format!("Expected variable in declaration, got {:?}", tok),
5145                    line,
5146                ));
5147            }
5148        };
5149        if allow_type_annotation && self.eat(&Token::Colon) {
5150            let ty = self.parse_type_name()?;
5151            if decl.sigil != Sigil::Scalar {
5152                return Err(self.syntax_err(
5153                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
5154                    self.peek_line(),
5155                ));
5156            }
5157            decl.type_annotation = Some(ty);
5158        }
5159        Ok(decl)
5160    }
5161
5162    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
5163        match self.advance() {
5164            (Token::Ident(name), _) => match name.as_str() {
5165                "Int" => Ok(PerlTypeName::Int),
5166                "Str" => Ok(PerlTypeName::Str),
5167                "Float" => Ok(PerlTypeName::Float),
5168                "Bool" => Ok(PerlTypeName::Bool),
5169                "Array" => Ok(PerlTypeName::Array),
5170                "Hash" => Ok(PerlTypeName::Hash),
5171                "Ref" => Ok(PerlTypeName::Ref),
5172                "Any" => Ok(PerlTypeName::Any),
5173                _ => Ok(PerlTypeName::Struct(name)),
5174            },
5175            (tok, err_line) => Err(self.syntax_err(
5176                format!("Expected type name after `:`, got {:?}", tok),
5177                err_line,
5178            )),
5179        }
5180    }
5181
5182    fn parse_package(&mut self) -> PerlResult<Statement> {
5183        let line = self.peek_line();
5184        self.advance(); // 'package'
5185        let name = match self.advance() {
5186            (Token::Ident(n), _) => n,
5187            (tok, line) => {
5188                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
5189            }
5190        };
5191        // Handle Foo::Bar
5192        let mut full_name = name;
5193        while self.eat(&Token::PackageSep) {
5194            if let (Token::Ident(part), _) = self.advance() {
5195                full_name = format!("{}::{}", full_name, part);
5196            }
5197        }
5198        self.eat(&Token::Semicolon);
5199        Ok(Statement {
5200            label: None,
5201            kind: StmtKind::Package { name: full_name },
5202            line,
5203        })
5204    }
5205
5206    fn parse_use(&mut self) -> PerlResult<Statement> {
5207        let line = self.peek_line();
5208        self.advance(); // 'use'
5209        let (tok, tok_line) = self.advance();
5210        match tok {
5211            Token::Float(v) => {
5212                self.eat(&Token::Semicolon);
5213                Ok(Statement {
5214                    label: None,
5215                    kind: StmtKind::UsePerlVersion { version: v },
5216                    line,
5217                })
5218            }
5219            Token::Integer(n) => {
5220                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5221                    self.eat(&Token::Semicolon);
5222                    Ok(Statement {
5223                        label: None,
5224                        kind: StmtKind::UsePerlVersion { version: n as f64 },
5225                        line,
5226                    })
5227                } else {
5228                    Err(self.syntax_err(
5229                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
5230                        line,
5231                    ))
5232                }
5233            }
5234            Token::Ident(n) => {
5235                let mut full_name = n;
5236                while self.eat(&Token::PackageSep) {
5237                    if let (Token::Ident(part), _) = self.advance() {
5238                        full_name = format!("{}::{}", full_name, part);
5239                    }
5240                }
5241                if full_name == "overload" {
5242                    let mut pairs = Vec::new();
5243                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
5244                        loop {
5245                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
5246                            {
5247                                break;
5248                            }
5249                            let key_e = this.parse_assign_expr()?;
5250                            this.expect(&Token::FatArrow)?;
5251                            let val_e = this.parse_assign_expr()?;
5252                            let key = this.expr_to_overload_key(&key_e)?;
5253                            let val = this.expr_to_overload_sub(&val_e)?;
5254                            pairs.push((key, val));
5255                            if !this.eat(&Token::Comma) {
5256                                break;
5257                            }
5258                        }
5259                        Ok(())
5260                    };
5261                    if self.eat(&Token::LParen) {
5262                        // `use overload ();` — common in JSON::PP and other modules.
5263                        parse_overload_pairs(self)?;
5264                        self.expect(&Token::RParen)?;
5265                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
5266                        parse_overload_pairs(self)?;
5267                    }
5268                    self.eat(&Token::Semicolon);
5269                    return Ok(Statement {
5270                        label: None,
5271                        kind: StmtKind::UseOverload { pairs },
5272                        line,
5273                    });
5274                }
5275                let mut imports = Vec::new();
5276                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5277                    && !self.next_is_new_statement_start(tok_line)
5278                {
5279                    loop {
5280                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5281                            break;
5282                        }
5283                        imports.push(self.parse_expression()?);
5284                        if !self.eat(&Token::Comma) {
5285                            break;
5286                        }
5287                    }
5288                }
5289                self.eat(&Token::Semicolon);
5290                Ok(Statement {
5291                    label: None,
5292                    kind: StmtKind::Use {
5293                        module: full_name,
5294                        imports,
5295                    },
5296                    line,
5297                })
5298            }
5299            other => Err(self.syntax_err(
5300                format!("Expected module name or version after use, got {:?}", other),
5301                tok_line,
5302            )),
5303        }
5304    }
5305
5306    fn parse_no(&mut self) -> PerlResult<Statement> {
5307        let line = self.peek_line();
5308        self.advance(); // 'no'
5309        let module = match self.advance() {
5310            (Token::Ident(n), tok_line) => (n, tok_line),
5311            (tok, line) => {
5312                return Err(self.syntax_err(
5313                    format!("Expected module name after no, got {:?}", tok),
5314                    line,
5315                ))
5316            }
5317        };
5318        let (module_name, tok_line) = module;
5319        let mut full_name = module_name;
5320        while self.eat(&Token::PackageSep) {
5321            if let (Token::Ident(part), _) = self.advance() {
5322                full_name = format!("{}::{}", full_name, part);
5323            }
5324        }
5325        let mut imports = Vec::new();
5326        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5327            && !self.next_is_new_statement_start(tok_line)
5328        {
5329            loop {
5330                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5331                    break;
5332                }
5333                imports.push(self.parse_expression()?);
5334                if !self.eat(&Token::Comma) {
5335                    break;
5336                }
5337            }
5338        }
5339        self.eat(&Token::Semicolon);
5340        Ok(Statement {
5341            label: None,
5342            kind: StmtKind::No {
5343                module: full_name,
5344                imports,
5345            },
5346            line,
5347        })
5348    }
5349
5350    fn parse_return(&mut self) -> PerlResult<Statement> {
5351        let line = self.peek_line();
5352        self.advance(); // 'return'
5353        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
5354            None
5355        } else {
5356            // Only parse up to the assign level to avoid consuming postfix if/unless
5357            Some(self.parse_assign_expr()?)
5358        };
5359        // Check for postfix modifiers on return
5360        let stmt = Statement {
5361            label: None,
5362            kind: StmtKind::Return(val),
5363            line,
5364        };
5365        if let Token::Ident(ref kw) = self.peek().clone() {
5366            match kw.as_str() {
5367                "if" => {
5368                    self.advance();
5369                    let cond = self.parse_expression()?;
5370                    self.eat(&Token::Semicolon);
5371                    return Ok(Statement {
5372                        label: None,
5373                        kind: StmtKind::If {
5374                            condition: cond,
5375                            body: vec![stmt],
5376                            elsifs: vec![],
5377                            else_block: None,
5378                        },
5379                        line,
5380                    });
5381                }
5382                "unless" => {
5383                    self.advance();
5384                    let cond = self.parse_expression()?;
5385                    self.eat(&Token::Semicolon);
5386                    return Ok(Statement {
5387                        label: None,
5388                        kind: StmtKind::Unless {
5389                            condition: cond,
5390                            body: vec![stmt],
5391                            else_block: None,
5392                        },
5393                        line,
5394                    });
5395                }
5396                _ => {}
5397            }
5398        }
5399        self.eat(&Token::Semicolon);
5400        Ok(stmt)
5401    }
5402
5403    // ── Expressions (Pratt / precedence climbing) ──
5404
5405    fn parse_expression(&mut self) -> PerlResult<Expr> {
5406        self.parse_comma_expr()
5407    }
5408
5409    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
5410        let expr = self.parse_assign_expr()?;
5411        let mut exprs = vec![expr];
5412        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5413            if matches!(
5414                self.peek(),
5415                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
5416            ) {
5417                break; // trailing comma
5418            }
5419            exprs.push(self.parse_assign_expr()?);
5420        }
5421        if exprs.len() == 1 {
5422            return Ok(exprs.pop().unwrap());
5423        }
5424        let line = exprs[0].line;
5425        Ok(Expr {
5426            kind: ExprKind::List(exprs),
5427            line,
5428        })
5429    }
5430
5431    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
5432        let expr = self.parse_ternary()?;
5433        let line = expr.line;
5434
5435        match self.peek().clone() {
5436            Token::Assign => {
5437                self.advance();
5438                let right = self.parse_assign_expr()?;
5439                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
5440                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
5441                    if args.is_empty() {
5442                        // Destructure again to take ownership
5443                        let ExprKind::MethodCall {
5444                            object,
5445                            method,
5446                            super_call,
5447                            ..
5448                        } = expr.kind
5449                        else {
5450                            unreachable!()
5451                        };
5452                        return Ok(Expr {
5453                            kind: ExprKind::MethodCall {
5454                                object,
5455                                method,
5456                                args: vec![right],
5457                                super_call,
5458                            },
5459                            line,
5460                        });
5461                    }
5462                }
5463                self.validate_assignment(&expr, &right, line)?;
5464                Ok(Expr {
5465                    kind: ExprKind::Assign {
5466                        target: Box::new(expr),
5467                        value: Box::new(right),
5468                    },
5469                    line,
5470                })
5471            }
5472            Token::PlusAssign => {
5473                self.advance();
5474                let r = self.parse_assign_expr()?;
5475                Ok(Expr {
5476                    kind: ExprKind::CompoundAssign {
5477                        target: Box::new(expr),
5478                        op: BinOp::Add,
5479                        value: Box::new(r),
5480                    },
5481                    line,
5482                })
5483            }
5484            Token::MinusAssign => {
5485                self.advance();
5486                let r = self.parse_assign_expr()?;
5487                Ok(Expr {
5488                    kind: ExprKind::CompoundAssign {
5489                        target: Box::new(expr),
5490                        op: BinOp::Sub,
5491                        value: Box::new(r),
5492                    },
5493                    line,
5494                })
5495            }
5496            Token::MulAssign => {
5497                self.advance();
5498                let r = self.parse_assign_expr()?;
5499                Ok(Expr {
5500                    kind: ExprKind::CompoundAssign {
5501                        target: Box::new(expr),
5502                        op: BinOp::Mul,
5503                        value: Box::new(r),
5504                    },
5505                    line,
5506                })
5507            }
5508            Token::DivAssign => {
5509                self.advance();
5510                let r = self.parse_assign_expr()?;
5511                Ok(Expr {
5512                    kind: ExprKind::CompoundAssign {
5513                        target: Box::new(expr),
5514                        op: BinOp::Div,
5515                        value: Box::new(r),
5516                    },
5517                    line,
5518                })
5519            }
5520            Token::ModAssign => {
5521                self.advance();
5522                let r = self.parse_assign_expr()?;
5523                Ok(Expr {
5524                    kind: ExprKind::CompoundAssign {
5525                        target: Box::new(expr),
5526                        op: BinOp::Mod,
5527                        value: Box::new(r),
5528                    },
5529                    line,
5530                })
5531            }
5532            Token::PowAssign => {
5533                self.advance();
5534                let r = self.parse_assign_expr()?;
5535                Ok(Expr {
5536                    kind: ExprKind::CompoundAssign {
5537                        target: Box::new(expr),
5538                        op: BinOp::Pow,
5539                        value: Box::new(r),
5540                    },
5541                    line,
5542                })
5543            }
5544            Token::DotAssign => {
5545                self.advance();
5546                let r = self.parse_assign_expr()?;
5547                Ok(Expr {
5548                    kind: ExprKind::CompoundAssign {
5549                        target: Box::new(expr),
5550                        op: BinOp::Concat,
5551                        value: Box::new(r),
5552                    },
5553                    line,
5554                })
5555            }
5556            Token::BitAndAssign => {
5557                self.advance();
5558                let r = self.parse_assign_expr()?;
5559                Ok(Expr {
5560                    kind: ExprKind::CompoundAssign {
5561                        target: Box::new(expr),
5562                        op: BinOp::BitAnd,
5563                        value: Box::new(r),
5564                    },
5565                    line,
5566                })
5567            }
5568            Token::BitOrAssign => {
5569                self.advance();
5570                let r = self.parse_assign_expr()?;
5571                Ok(Expr {
5572                    kind: ExprKind::CompoundAssign {
5573                        target: Box::new(expr),
5574                        op: BinOp::BitOr,
5575                        value: Box::new(r),
5576                    },
5577                    line,
5578                })
5579            }
5580            Token::XorAssign => {
5581                self.advance();
5582                let r = self.parse_assign_expr()?;
5583                Ok(Expr {
5584                    kind: ExprKind::CompoundAssign {
5585                        target: Box::new(expr),
5586                        op: BinOp::BitXor,
5587                        value: Box::new(r),
5588                    },
5589                    line,
5590                })
5591            }
5592            Token::ShiftLeftAssign => {
5593                self.advance();
5594                let r = self.parse_assign_expr()?;
5595                Ok(Expr {
5596                    kind: ExprKind::CompoundAssign {
5597                        target: Box::new(expr),
5598                        op: BinOp::ShiftLeft,
5599                        value: Box::new(r),
5600                    },
5601                    line,
5602                })
5603            }
5604            Token::ShiftRightAssign => {
5605                self.advance();
5606                let r = self.parse_assign_expr()?;
5607                Ok(Expr {
5608                    kind: ExprKind::CompoundAssign {
5609                        target: Box::new(expr),
5610                        op: BinOp::ShiftRight,
5611                        value: Box::new(r),
5612                    },
5613                    line,
5614                })
5615            }
5616            Token::OrAssign => {
5617                self.advance();
5618                let r = self.parse_assign_expr()?;
5619                Ok(Expr {
5620                    kind: ExprKind::CompoundAssign {
5621                        target: Box::new(expr),
5622                        op: BinOp::LogOr,
5623                        value: Box::new(r),
5624                    },
5625                    line,
5626                })
5627            }
5628            Token::DefinedOrAssign => {
5629                self.advance();
5630                let r = self.parse_assign_expr()?;
5631                Ok(Expr {
5632                    kind: ExprKind::CompoundAssign {
5633                        target: Box::new(expr),
5634                        op: BinOp::DefinedOr,
5635                        value: Box::new(r),
5636                    },
5637                    line,
5638                })
5639            }
5640            Token::AndAssign => {
5641                self.advance();
5642                let r = self.parse_assign_expr()?;
5643                Ok(Expr {
5644                    kind: ExprKind::CompoundAssign {
5645                        target: Box::new(expr),
5646                        op: BinOp::LogAnd,
5647                        value: Box::new(r),
5648                    },
5649                    line,
5650                })
5651            }
5652            _ => Ok(expr),
5653        }
5654    }
5655
5656    fn parse_ternary(&mut self) -> PerlResult<Expr> {
5657        let expr = self.parse_pipe_forward()?;
5658        if self.eat(&Token::Question) {
5659            let line = expr.line;
5660            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
5661            let then_expr = self.parse_assign_expr();
5662            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
5663            let then_expr = then_expr?;
5664            self.expect(&Token::Colon)?;
5665            let else_expr = self.parse_assign_expr()?;
5666            return Ok(Expr {
5667                kind: ExprKind::Ternary {
5668                    condition: Box::new(expr),
5669                    then_expr: Box::new(then_expr),
5670                    else_expr: Box::new(else_expr),
5671                },
5672                line,
5673            });
5674        }
5675        Ok(expr)
5676    }
5677
5678    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
5679    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
5680    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
5681    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
5682    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
5683    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
5684        let mut left = self.parse_or_word()?;
5685        // Inside a paren-less arg list, `|>` is a hard terminator for the
5686        // enclosing call — leave it for the outer `parse_pipe_forward` loop
5687        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
5688        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
5689        // outer `|>` via its first-arg `parse_assign_expr`.
5690        if self.no_pipe_forward_depth > 0 {
5691            return Ok(left);
5692        }
5693        while matches!(self.peek(), Token::PipeForward) {
5694            if crate::compat_mode() {
5695                return Err(self.syntax_err(
5696                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
5697                    left.line,
5698                ));
5699            }
5700            let line = left.line;
5701            self.advance();
5702            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
5703            // `join`, …) accept a placeholder in place of their list operand.
5704            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
5705            let right_result = self.parse_or_word();
5706            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
5707            let right = right_result?;
5708            left = self.pipe_forward_apply(left, right, line)?;
5709        }
5710        Ok(left)
5711    }
5712
5713    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
5714    /// its **first** argument (Elixir / R / proposed-JS convention).
5715    ///
5716    /// The strategy depends on the shape of `rhs`:
5717    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
5718    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
5719    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
5720    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
5721    ///   matching the `(data, filter)` signature the builtin expects.
5722    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
5723    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
5724    ///   `lhs` (these parse a single default `$_` when called without an arg, so
5725    ///   piping overrides that default; first-arg and last-arg are identical).
5726    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
5727    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
5728    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
5729    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
5730    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
5731    ///   as the sole argument.
5732    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
5733    ///   since silently calling a non-callable at runtime would be worse.
5734    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
5735        let Expr { kind, line: rline } = rhs;
5736        let new_kind = match kind {
5737            // ── Generic / user-defined calls ───────────────────────────────────
5738            ExprKind::FuncCall { name, mut args } => {
5739                match name.as_str() {
5740                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
5741                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
5742                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
5743                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "letters"
5744                    | "letters_uc" | "letters_lc" | "punctuation" | "numbers" | "graphemes"
5745                    | "columns" | "sentences" | "paragraphs" | "sections" | "trim" | "avg"
5746                    | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml" | "to_html"
5747                    | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
5748                    | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline" | "bar_chart"
5749                    | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed" | "cb"
5750                    | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
5751                        if args.is_empty() {
5752                            args.push(lhs);
5753                        } else {
5754                            args[0] = lhs;
5755                        }
5756                    }
5757                    "chunked" | "windowed" => {
5758                        if args.is_empty() {
5759                            return Err(self.syntax_err(
5760                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
5761                                line,
5762                            ));
5763                        }
5764                        args.insert(0, lhs);
5765                    }
5766                    "List::Util::reduce" | "List::Util::fold" => {
5767                        args.push(lhs);
5768                    }
5769                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
5770                        // data |> grep_v "pattern" → grep_v("pattern", data...)
5771                        // data |> pluck "key" → pluck("key", data...)
5772                        // data |> tee "file" → tee("file", data...)
5773                        // data |> nth N → nth(N, data...)
5774                        // data |> chunk N → chunk(N, data...)
5775                        args.push(lhs);
5776                    }
5777                    "enumerate" | "dedup" => {
5778                        // data |> enumerate → enumerate(data)
5779                        // data |> dedup → dedup(data)
5780                        args.insert(0, lhs);
5781                    }
5782                    "clamp" => {
5783                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
5784                        args.push(lhs);
5785                    }
5786                    "pfirst" | "pany" | "any" | "all" | "none" | "first" | "take_while"
5787                    | "drop_while" | "skip_while" | "reject" | "tap" | "peek" | "group_by"
5788                    | "chunk_by" | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
5789                        if args.len() < 2 {
5790                            return Err(self.syntax_err(
5791                                format!(
5792                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
5793                                ),
5794                                line,
5795                            ));
5796                        }
5797                        args[1] = lhs;
5798                    }
5799                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail" => {
5800                        if args.is_empty() {
5801                            return Err(self.syntax_err(
5802                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
5803                                line,
5804                            ));
5805                        }
5806                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
5807                        args.insert(0, lhs);
5808                    }
5809                    _ => {
5810                        if self.thread_last_mode {
5811                            args.push(lhs);
5812                        } else {
5813                            args.insert(0, lhs);
5814                        }
5815                    }
5816                }
5817                ExprKind::FuncCall { name, args }
5818            }
5819            ExprKind::MethodCall {
5820                object,
5821                method,
5822                mut args,
5823                super_call,
5824            } => {
5825                if self.thread_last_mode {
5826                    args.push(lhs);
5827                } else {
5828                    args.insert(0, lhs);
5829                }
5830                ExprKind::MethodCall {
5831                    object,
5832                    method,
5833                    args,
5834                    super_call,
5835                }
5836            }
5837            ExprKind::IndirectCall {
5838                target,
5839                mut args,
5840                ampersand,
5841                pass_caller_arglist: _,
5842            } => {
5843                if self.thread_last_mode {
5844                    args.push(lhs);
5845                } else {
5846                    args.insert(0, lhs);
5847                }
5848                ExprKind::IndirectCall {
5849                    target,
5850                    args,
5851                    ampersand,
5852                    // Prepending an explicit first arg means this is no longer
5853                    // "pass the caller's @_" — that form is only bare `&$cr`.
5854                    pass_caller_arglist: false,
5855                }
5856            }
5857
5858            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
5859            ExprKind::Print { handle, mut args } => {
5860                if self.thread_last_mode {
5861                    args.push(lhs);
5862                } else {
5863                    args.insert(0, lhs);
5864                }
5865                ExprKind::Print { handle, args }
5866            }
5867            ExprKind::Say { handle, mut args } => {
5868                if self.thread_last_mode {
5869                    args.push(lhs);
5870                } else {
5871                    args.insert(0, lhs);
5872                }
5873                ExprKind::Say { handle, args }
5874            }
5875            ExprKind::Printf { handle, mut args } => {
5876                if self.thread_last_mode {
5877                    args.push(lhs);
5878                } else {
5879                    args.insert(0, lhs);
5880                }
5881                ExprKind::Printf { handle, args }
5882            }
5883            ExprKind::Die(mut args) => {
5884                if self.thread_last_mode {
5885                    args.push(lhs);
5886                } else {
5887                    args.insert(0, lhs);
5888                }
5889                ExprKind::Die(args)
5890            }
5891            ExprKind::Warn(mut args) => {
5892                if self.thread_last_mode {
5893                    args.push(lhs);
5894                } else {
5895                    args.insert(0, lhs);
5896                }
5897                ExprKind::Warn(args)
5898            }
5899
5900            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
5901            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
5902            //   but piping the format string is the rarer case. Prepending
5903            //   to the values list gives `sprintf(format, lhs, ...args)` for
5904            //   the common `$n |> sprintf "count=%d"` case.
5905            ExprKind::Sprintf { format, mut args } => {
5906                if self.thread_last_mode {
5907                    args.push(lhs);
5908                } else {
5909                    args.insert(0, lhs);
5910                }
5911                ExprKind::Sprintf { format, args }
5912            }
5913
5914            // ── System / exec / globbing / filesystem variadics ────────────────
5915            ExprKind::System(mut args) => {
5916                if self.thread_last_mode {
5917                    args.push(lhs);
5918                } else {
5919                    args.insert(0, lhs);
5920                }
5921                ExprKind::System(args)
5922            }
5923            ExprKind::Exec(mut args) => {
5924                if self.thread_last_mode {
5925                    args.push(lhs);
5926                } else {
5927                    args.insert(0, lhs);
5928                }
5929                ExprKind::Exec(args)
5930            }
5931            ExprKind::Unlink(mut args) => {
5932                if self.thread_last_mode {
5933                    args.push(lhs);
5934                } else {
5935                    args.insert(0, lhs);
5936                }
5937                ExprKind::Unlink(args)
5938            }
5939            ExprKind::Chmod(mut args) => {
5940                if self.thread_last_mode {
5941                    args.push(lhs);
5942                } else {
5943                    args.insert(0, lhs);
5944                }
5945                ExprKind::Chmod(args)
5946            }
5947            ExprKind::Chown(mut args) => {
5948                if self.thread_last_mode {
5949                    args.push(lhs);
5950                } else {
5951                    args.insert(0, lhs);
5952                }
5953                ExprKind::Chown(args)
5954            }
5955            ExprKind::Glob(mut args) => {
5956                if self.thread_last_mode {
5957                    args.push(lhs);
5958                } else {
5959                    args.insert(0, lhs);
5960                }
5961                ExprKind::Glob(args)
5962            }
5963            ExprKind::Files(mut args) => {
5964                if self.thread_last_mode {
5965                    args.push(lhs);
5966                } else {
5967                    args.insert(0, lhs);
5968                }
5969                ExprKind::Files(args)
5970            }
5971            ExprKind::Filesf(mut args) => {
5972                if self.thread_last_mode {
5973                    args.push(lhs);
5974                } else {
5975                    args.insert(0, lhs);
5976                }
5977                ExprKind::Filesf(args)
5978            }
5979            ExprKind::FilesfRecursive(mut args) => {
5980                if self.thread_last_mode {
5981                    args.push(lhs);
5982                } else {
5983                    args.insert(0, lhs);
5984                }
5985                ExprKind::FilesfRecursive(args)
5986            }
5987            ExprKind::Dirs(mut args) => {
5988                if self.thread_last_mode {
5989                    args.push(lhs);
5990                } else {
5991                    args.insert(0, lhs);
5992                }
5993                ExprKind::Dirs(args)
5994            }
5995            ExprKind::DirsRecursive(mut args) => {
5996                if self.thread_last_mode {
5997                    args.push(lhs);
5998                } else {
5999                    args.insert(0, lhs);
6000                }
6001                ExprKind::DirsRecursive(args)
6002            }
6003            ExprKind::SymLinks(mut args) => {
6004                if self.thread_last_mode {
6005                    args.push(lhs);
6006                } else {
6007                    args.insert(0, lhs);
6008                }
6009                ExprKind::SymLinks(args)
6010            }
6011            ExprKind::Sockets(mut args) => {
6012                if self.thread_last_mode {
6013                    args.push(lhs);
6014                } else {
6015                    args.insert(0, lhs);
6016                }
6017                ExprKind::Sockets(args)
6018            }
6019            ExprKind::Pipes(mut args) => {
6020                if self.thread_last_mode {
6021                    args.push(lhs);
6022                } else {
6023                    args.insert(0, lhs);
6024                }
6025                ExprKind::Pipes(args)
6026            }
6027            ExprKind::BlockDevices(mut args) => {
6028                if self.thread_last_mode {
6029                    args.push(lhs);
6030                } else {
6031                    args.insert(0, lhs);
6032                }
6033                ExprKind::BlockDevices(args)
6034            }
6035            ExprKind::CharDevices(mut args) => {
6036                if self.thread_last_mode {
6037                    args.push(lhs);
6038                } else {
6039                    args.insert(0, lhs);
6040                }
6041                ExprKind::CharDevices(args)
6042            }
6043            ExprKind::GlobPar { mut args, progress } => {
6044                if self.thread_last_mode {
6045                    args.push(lhs);
6046                } else {
6047                    args.insert(0, lhs);
6048                }
6049                ExprKind::GlobPar { args, progress }
6050            }
6051            ExprKind::ParSed { mut args, progress } => {
6052                if self.thread_last_mode {
6053                    args.push(lhs);
6054                } else {
6055                    args.insert(0, lhs);
6056                }
6057                ExprKind::ParSed { args, progress }
6058            }
6059
6060            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
6061            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
6062            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
6063            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
6064            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
6065            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
6066            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
6067            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
6068            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
6069            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
6070            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
6071            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
6072            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
6073            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
6074            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
6075            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
6076            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
6077            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
6078            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
6079            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
6080            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
6081            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
6082            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
6083            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
6084            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
6085            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
6086            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
6087            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
6088            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
6089            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
6090            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
6091            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
6092            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
6093            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
6094            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
6095            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
6096            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
6097            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
6098            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
6099            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
6100            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
6101            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
6102            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
6103            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
6104            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
6105            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
6106            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
6107            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
6108            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
6109            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
6110            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
6111            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
6112
6113            // ── Higher-order / list-taking forms: replace the `list` slot ──────
6114            ExprKind::MapExpr {
6115                block,
6116                list: _,
6117                flatten_array_refs,
6118                stream,
6119            } => ExprKind::MapExpr {
6120                block,
6121                list: Box::new(lhs),
6122                flatten_array_refs,
6123                stream,
6124            },
6125            ExprKind::MapExprComma {
6126                expr,
6127                list: _,
6128                flatten_array_refs,
6129                stream,
6130            } => ExprKind::MapExprComma {
6131                expr,
6132                list: Box::new(lhs),
6133                flatten_array_refs,
6134                stream,
6135            },
6136            ExprKind::GrepExpr {
6137                block,
6138                list: _,
6139                keyword,
6140            } => ExprKind::GrepExpr {
6141                block,
6142                list: Box::new(lhs),
6143                keyword,
6144            },
6145            ExprKind::GrepExprComma {
6146                expr,
6147                list: _,
6148                keyword,
6149            } => ExprKind::GrepExprComma {
6150                expr,
6151                list: Box::new(lhs),
6152                keyword,
6153            },
6154            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
6155                block,
6156                list: Box::new(lhs),
6157            },
6158            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
6159                cmp,
6160                list: Box::new(lhs),
6161            },
6162            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
6163                separator,
6164                list: Box::new(lhs),
6165            },
6166            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
6167                block,
6168                list: Box::new(lhs),
6169            },
6170            ExprKind::PMapExpr {
6171                block,
6172                list: _,
6173                progress,
6174                flat_outputs,
6175                on_cluster,
6176                stream,
6177            } => ExprKind::PMapExpr {
6178                block,
6179                list: Box::new(lhs),
6180                progress,
6181                flat_outputs,
6182                on_cluster,
6183                stream,
6184            },
6185            ExprKind::PMapChunkedExpr {
6186                chunk_size,
6187                block,
6188                list: _,
6189                progress,
6190            } => ExprKind::PMapChunkedExpr {
6191                chunk_size,
6192                block,
6193                list: Box::new(lhs),
6194                progress,
6195            },
6196            ExprKind::PGrepExpr {
6197                block,
6198                list: _,
6199                progress,
6200                stream,
6201            } => ExprKind::PGrepExpr {
6202                block,
6203                list: Box::new(lhs),
6204                progress,
6205                stream,
6206            },
6207            ExprKind::PForExpr {
6208                block,
6209                list: _,
6210                progress,
6211            } => ExprKind::PForExpr {
6212                block,
6213                list: Box::new(lhs),
6214                progress,
6215            },
6216            ExprKind::PSortExpr {
6217                cmp,
6218                list: _,
6219                progress,
6220            } => ExprKind::PSortExpr {
6221                cmp,
6222                list: Box::new(lhs),
6223                progress,
6224            },
6225            ExprKind::PReduceExpr {
6226                block,
6227                list: _,
6228                progress,
6229            } => ExprKind::PReduceExpr {
6230                block,
6231                list: Box::new(lhs),
6232                progress,
6233            },
6234            ExprKind::PcacheExpr {
6235                block,
6236                list: _,
6237                progress,
6238            } => ExprKind::PcacheExpr {
6239                block,
6240                list: Box::new(lhs),
6241                progress,
6242            },
6243            ExprKind::PReduceInitExpr {
6244                init,
6245                block,
6246                list: _,
6247                progress,
6248            } => ExprKind::PReduceInitExpr {
6249                init,
6250                block,
6251                list: Box::new(lhs),
6252                progress,
6253            },
6254            ExprKind::PMapReduceExpr {
6255                map_block,
6256                reduce_block,
6257                list: _,
6258                progress,
6259            } => ExprKind::PMapReduceExpr {
6260                map_block,
6261                reduce_block,
6262                list: Box::new(lhs),
6263                progress,
6264            },
6265
6266            // ── Push / unshift: first arg is the array, so pipe the LHS
6267            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
6268            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
6269            //     directly for that.
6270            ExprKind::Push { array, mut values } => {
6271                values.insert(0, lhs);
6272                ExprKind::Push { array, values }
6273            }
6274            ExprKind::Unshift { array, mut values } => {
6275                values.insert(0, lhs);
6276                ExprKind::Unshift { array, values }
6277            }
6278
6279            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
6280            ExprKind::SplitExpr {
6281                pattern,
6282                string: _,
6283                limit,
6284            } => ExprKind::SplitExpr {
6285                pattern,
6286                string: Box::new(lhs),
6287                limit,
6288            },
6289
6290            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
6291            //    Auto-inject `r` flag so the substitution returns the modified
6292            //    string instead of the match count (non-destructive / Perl /r).
6293            ExprKind::Substitution {
6294                pattern,
6295                replacement,
6296                mut flags,
6297                expr: _,
6298                delim,
6299            } => {
6300                if !flags.contains('r') {
6301                    flags.push('r');
6302                }
6303                ExprKind::Substitution {
6304                    expr: Box::new(lhs),
6305                    pattern,
6306                    replacement,
6307                    flags,
6308                    delim,
6309                }
6310            }
6311            ExprKind::Transliterate {
6312                from,
6313                to,
6314                mut flags,
6315                expr: _,
6316                delim,
6317            } => {
6318                if !flags.contains('r') {
6319                    flags.push('r');
6320                }
6321                ExprKind::Transliterate {
6322                    expr: Box::new(lhs),
6323                    from,
6324                    to,
6325                    flags,
6326                    delim,
6327                }
6328            }
6329            ExprKind::Match {
6330                pattern,
6331                flags,
6332                scalar_g,
6333                expr: _,
6334                delim,
6335            } => ExprKind::Match {
6336                expr: Box::new(lhs),
6337                pattern,
6338                flags,
6339                scalar_g,
6340                delim,
6341            },
6342            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
6343            ExprKind::Regex(pattern, flags) => ExprKind::Match {
6344                expr: Box::new(lhs),
6345                pattern,
6346                flags,
6347                scalar_g: false,
6348                delim: '/',
6349            },
6350
6351            // ── Bareword function name → plain unary call ──────────────────────
6352            ExprKind::Bareword(name) => match name.as_str() {
6353                "reverse" => {
6354                    if !crate::compat_mode() {
6355                        return Err(self.syntax_err(
6356                            "stryke uses `rev` instead of `reverse` (this is not Perl 5)",
6357                            line,
6358                        ));
6359                    }
6360                    ExprKind::ReverseExpr(Box::new(lhs))
6361                }
6362                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
6363                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
6364                    name: "uniq".to_string(),
6365                    args: vec![lhs],
6366                },
6367                "fl" | "flatten" => ExprKind::FuncCall {
6368                    name: "flatten".to_string(),
6369                    args: vec![lhs],
6370                },
6371                _ => ExprKind::FuncCall {
6372                    name,
6373                    args: vec![lhs],
6374                },
6375            },
6376
6377            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
6378            kind @ (ExprKind::ScalarVar(_)
6379            | ExprKind::ArrayElement { .. }
6380            | ExprKind::HashElement { .. }
6381            | ExprKind::Deref { .. }
6382            | ExprKind::ArrowDeref { .. }
6383            | ExprKind::CodeRef { .. }
6384            | ExprKind::SubroutineRef(_)
6385            | ExprKind::SubroutineCodeRef(_)
6386            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
6387                target: Box::new(Expr { kind, line: rline }),
6388                args: vec![lhs],
6389                ampersand: false,
6390                pass_caller_arglist: false,
6391            },
6392
6393            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
6394            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
6395            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
6396            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
6397                ExprKind::IndirectCall {
6398                    target: inner,
6399                    args: vec![lhs],
6400                    ampersand: false,
6401                    pass_caller_arglist: false,
6402                }
6403            }
6404
6405            other => {
6406                return Err(self.syntax_err(
6407                    format!(
6408                        "right-hand side of `|>` must be a call, builtin, or coderef \
6409                         expression (got {})",
6410                        Self::expr_kind_name(&other)
6411                    ),
6412                    line,
6413                ));
6414            }
6415        };
6416        Ok(Expr {
6417            kind: new_kind,
6418            line,
6419        })
6420    }
6421
6422    /// Short label for an `ExprKind` (used in `|>` error messages).
6423    fn expr_kind_name(kind: &ExprKind) -> &'static str {
6424        match kind {
6425            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
6426            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
6427            ExprKind::BinOp { .. } => "binary expression",
6428            ExprKind::UnaryOp { .. } => "unary expression",
6429            ExprKind::Ternary { .. } => "ternary expression",
6430            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
6431            ExprKind::List(_) => "list expression",
6432            ExprKind::Range { .. } => "range expression",
6433            _ => "expression",
6434        }
6435    }
6436
6437    // or / not (lowest precedence word operators)
6438    fn parse_or_word(&mut self) -> PerlResult<Expr> {
6439        let mut left = self.parse_and_word()?;
6440        while matches!(self.peek(), Token::LogOrWord) {
6441            let line = left.line;
6442            self.advance();
6443            let right = self.parse_and_word()?;
6444            left = Expr {
6445                kind: ExprKind::BinOp {
6446                    left: Box::new(left),
6447                    op: BinOp::LogOrWord,
6448                    right: Box::new(right),
6449                },
6450                line,
6451            };
6452        }
6453        Ok(left)
6454    }
6455
6456    fn parse_and_word(&mut self) -> PerlResult<Expr> {
6457        let mut left = self.parse_not_word()?;
6458        while matches!(self.peek(), Token::LogAndWord) {
6459            let line = left.line;
6460            self.advance();
6461            let right = self.parse_not_word()?;
6462            left = Expr {
6463                kind: ExprKind::BinOp {
6464                    left: Box::new(left),
6465                    op: BinOp::LogAndWord,
6466                    right: Box::new(right),
6467                },
6468                line,
6469            };
6470        }
6471        Ok(left)
6472    }
6473
6474    fn parse_not_word(&mut self) -> PerlResult<Expr> {
6475        if matches!(self.peek(), Token::LogNotWord) {
6476            let line = self.peek_line();
6477            self.advance();
6478            let expr = self.parse_not_word()?;
6479            return Ok(Expr {
6480                kind: ExprKind::UnaryOp {
6481                    op: UnaryOp::LogNotWord,
6482                    expr: Box::new(expr),
6483                },
6484                line,
6485            });
6486        }
6487        self.parse_range()
6488    }
6489
6490    fn parse_log_or(&mut self) -> PerlResult<Expr> {
6491        let mut left = self.parse_log_and()?;
6492        loop {
6493            let op = match self.peek() {
6494                Token::LogOr => BinOp::LogOr,
6495                Token::DefinedOr => BinOp::DefinedOr,
6496                _ => break,
6497            };
6498            let line = left.line;
6499            self.advance();
6500            let right = self.parse_log_and()?;
6501            left = Expr {
6502                kind: ExprKind::BinOp {
6503                    left: Box::new(left),
6504                    op,
6505                    right: Box::new(right),
6506                },
6507                line,
6508            };
6509        }
6510        Ok(left)
6511    }
6512
6513    fn parse_log_and(&mut self) -> PerlResult<Expr> {
6514        let mut left = self.parse_bit_or()?;
6515        while matches!(self.peek(), Token::LogAnd) {
6516            let line = left.line;
6517            self.advance();
6518            let right = self.parse_bit_or()?;
6519            left = Expr {
6520                kind: ExprKind::BinOp {
6521                    left: Box::new(left),
6522                    op: BinOp::LogAnd,
6523                    right: Box::new(right),
6524                },
6525                line,
6526            };
6527        }
6528        Ok(left)
6529    }
6530
6531    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
6532        let mut left = self.parse_bit_xor()?;
6533        while matches!(self.peek(), Token::BitOr) {
6534            let line = left.line;
6535            self.advance();
6536            let right = self.parse_bit_xor()?;
6537            left = Expr {
6538                kind: ExprKind::BinOp {
6539                    left: Box::new(left),
6540                    op: BinOp::BitOr,
6541                    right: Box::new(right),
6542                },
6543                line,
6544            };
6545        }
6546        Ok(left)
6547    }
6548
6549    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
6550        let mut left = self.parse_bit_and()?;
6551        while matches!(self.peek(), Token::BitXor) {
6552            let line = left.line;
6553            self.advance();
6554            let right = self.parse_bit_and()?;
6555            left = Expr {
6556                kind: ExprKind::BinOp {
6557                    left: Box::new(left),
6558                    op: BinOp::BitXor,
6559                    right: Box::new(right),
6560                },
6561                line,
6562            };
6563        }
6564        Ok(left)
6565    }
6566
6567    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
6568        let mut left = self.parse_equality()?;
6569        while matches!(self.peek(), Token::BitAnd) {
6570            let line = left.line;
6571            self.advance();
6572            let right = self.parse_equality()?;
6573            left = Expr {
6574                kind: ExprKind::BinOp {
6575                    left: Box::new(left),
6576                    op: BinOp::BitAnd,
6577                    right: Box::new(right),
6578                },
6579                line,
6580            };
6581        }
6582        Ok(left)
6583    }
6584
6585    fn parse_equality(&mut self) -> PerlResult<Expr> {
6586        let mut left = self.parse_comparison()?;
6587        loop {
6588            let op = match self.peek() {
6589                Token::NumEq => BinOp::NumEq,
6590                Token::NumNe => BinOp::NumNe,
6591                Token::StrEq => BinOp::StrEq,
6592                Token::StrNe => BinOp::StrNe,
6593                Token::Spaceship => BinOp::Spaceship,
6594                Token::StrCmp => BinOp::StrCmp,
6595                _ => break,
6596            };
6597            let line = left.line;
6598            self.advance();
6599            let right = self.parse_comparison()?;
6600            left = Expr {
6601                kind: ExprKind::BinOp {
6602                    left: Box::new(left),
6603                    op,
6604                    right: Box::new(right),
6605                },
6606                line,
6607            };
6608        }
6609        Ok(left)
6610    }
6611
6612    fn parse_comparison(&mut self) -> PerlResult<Expr> {
6613        let left = self.parse_shift()?;
6614        let first_op = match self.peek() {
6615            Token::NumLt => BinOp::NumLt,
6616            Token::NumGt => BinOp::NumGt,
6617            Token::NumLe => BinOp::NumLe,
6618            Token::NumGe => BinOp::NumGe,
6619            Token::StrLt => BinOp::StrLt,
6620            Token::StrGt => BinOp::StrGt,
6621            Token::StrLe => BinOp::StrLe,
6622            Token::StrGe => BinOp::StrGe,
6623            _ => return Ok(left),
6624        };
6625        let line = left.line;
6626        self.advance();
6627        let middle = self.parse_shift()?;
6628
6629        let second_op = match self.peek() {
6630            Token::NumLt => Some(BinOp::NumLt),
6631            Token::NumGt => Some(BinOp::NumGt),
6632            Token::NumLe => Some(BinOp::NumLe),
6633            Token::NumGe => Some(BinOp::NumGe),
6634            Token::StrLt => Some(BinOp::StrLt),
6635            Token::StrGt => Some(BinOp::StrGt),
6636            Token::StrLe => Some(BinOp::StrLe),
6637            Token::StrGe => Some(BinOp::StrGe),
6638            _ => None,
6639        };
6640
6641        if second_op.is_none() {
6642            return Ok(Expr {
6643                kind: ExprKind::BinOp {
6644                    left: Box::new(left),
6645                    op: first_op,
6646                    right: Box::new(middle),
6647                },
6648                line,
6649            });
6650        }
6651
6652        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
6653        // Collect all operands and operators for chains like `1 < x < 10 < y`
6654        let mut operands = vec![left, middle];
6655        let mut ops = vec![first_op];
6656
6657        loop {
6658            let op = match self.peek() {
6659                Token::NumLt => BinOp::NumLt,
6660                Token::NumGt => BinOp::NumGt,
6661                Token::NumLe => BinOp::NumLe,
6662                Token::NumGe => BinOp::NumGe,
6663                Token::StrLt => BinOp::StrLt,
6664                Token::StrGt => BinOp::StrGt,
6665                Token::StrLe => BinOp::StrLe,
6666                Token::StrGe => BinOp::StrGe,
6667                _ => break,
6668            };
6669            self.advance();
6670            ops.push(op);
6671            operands.push(self.parse_shift()?);
6672        }
6673
6674        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
6675        let mut result = Expr {
6676            kind: ExprKind::BinOp {
6677                left: Box::new(operands[0].clone()),
6678                op: ops[0],
6679                right: Box::new(operands[1].clone()),
6680            },
6681            line,
6682        };
6683
6684        for i in 1..ops.len() {
6685            let cmp = Expr {
6686                kind: ExprKind::BinOp {
6687                    left: Box::new(operands[i].clone()),
6688                    op: ops[i],
6689                    right: Box::new(operands[i + 1].clone()),
6690                },
6691                line,
6692            };
6693            result = Expr {
6694                kind: ExprKind::BinOp {
6695                    left: Box::new(result),
6696                    op: BinOp::LogAnd,
6697                    right: Box::new(cmp),
6698                },
6699                line,
6700            };
6701        }
6702
6703        Ok(result)
6704    }
6705
6706    fn parse_shift(&mut self) -> PerlResult<Expr> {
6707        let mut left = self.parse_addition()?;
6708        loop {
6709            let op = match self.peek() {
6710                Token::ShiftLeft => BinOp::ShiftLeft,
6711                Token::ShiftRight => BinOp::ShiftRight,
6712                _ => break,
6713            };
6714            let line = left.line;
6715            self.advance();
6716            let right = self.parse_addition()?;
6717            left = Expr {
6718                kind: ExprKind::BinOp {
6719                    left: Box::new(left),
6720                    op,
6721                    right: Box::new(right),
6722                },
6723                line,
6724            };
6725        }
6726        Ok(left)
6727    }
6728
6729    fn parse_addition(&mut self) -> PerlResult<Expr> {
6730        let mut left = self.parse_multiplication()?;
6731        loop {
6732            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
6733            // the next statement, not a binary operator continuing this expression.
6734            let op = match self.peek() {
6735                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
6736                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
6737                Token::Dot => BinOp::Concat,
6738                _ => break,
6739            };
6740            let line = left.line;
6741            self.advance();
6742            let right = self.parse_multiplication()?;
6743            left = Expr {
6744                kind: ExprKind::BinOp {
6745                    left: Box::new(left),
6746                    op,
6747                    right: Box::new(right),
6748                },
6749                line,
6750            };
6751        }
6752        Ok(left)
6753    }
6754
6755    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
6756        let mut left = self.parse_regex_bind()?;
6757        loop {
6758            let op = match self.peek() {
6759                Token::Star => BinOp::Mul,
6760                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
6761                // Implicit semicolon: `%` on a new line is a hash dereference or hash
6762                // sigil for the next statement, not modulo operator on this expression.
6763                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
6764                Token::X => {
6765                    let line = left.line;
6766                    self.advance();
6767                    let right = self.parse_regex_bind()?;
6768                    left = Expr {
6769                        kind: ExprKind::Repeat {
6770                            expr: Box::new(left),
6771                            count: Box::new(right),
6772                        },
6773                        line,
6774                    };
6775                    continue;
6776                }
6777                _ => break,
6778            };
6779            let line = left.line;
6780            self.advance();
6781            let right = self.parse_regex_bind()?;
6782            left = Expr {
6783                kind: ExprKind::BinOp {
6784                    left: Box::new(left),
6785                    op,
6786                    right: Box::new(right),
6787                },
6788                line,
6789            };
6790        }
6791        Ok(left)
6792    }
6793
6794    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
6795        let left = self.parse_unary()?;
6796        match self.peek() {
6797            Token::BindMatch => {
6798                let line = left.line;
6799                self.advance();
6800                match self.peek().clone() {
6801                    Token::Regex(pattern, flags, delim) => {
6802                        self.advance();
6803                        Ok(Expr {
6804                            kind: ExprKind::Match {
6805                                expr: Box::new(left),
6806                                pattern,
6807                                flags,
6808                                scalar_g: false,
6809                                delim,
6810                            },
6811                            line,
6812                        })
6813                    }
6814                    Token::Ident(ref s) if s.starts_with('\x00') => {
6815                        let (Token::Ident(encoded), _) = self.advance() else {
6816                            unreachable!()
6817                        };
6818                        let parts: Vec<&str> = encoded.split('\x00').collect();
6819                        if parts.len() >= 4 && parts[1] == "s" {
6820                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6821                            Ok(Expr {
6822                                kind: ExprKind::Substitution {
6823                                    expr: Box::new(left),
6824                                    pattern: parts[2].to_string(),
6825                                    replacement: parts[3].to_string(),
6826                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6827                                    delim,
6828                                },
6829                                line,
6830                            })
6831                        } else if parts.len() >= 4 && parts[1] == "tr" {
6832                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6833                            Ok(Expr {
6834                                kind: ExprKind::Transliterate {
6835                                    expr: Box::new(left),
6836                                    from: parts[2].to_string(),
6837                                    to: parts[3].to_string(),
6838                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6839                                    delim,
6840                                },
6841                                line,
6842                            })
6843                        } else {
6844                            Err(self.syntax_err("Invalid regex binding", line))
6845                        }
6846                    }
6847                    _ => {
6848                        let rhs = self.parse_unary()?;
6849                        Ok(Expr {
6850                            kind: ExprKind::BinOp {
6851                                left: Box::new(left),
6852                                op: BinOp::BindMatch,
6853                                right: Box::new(rhs),
6854                            },
6855                            line,
6856                        })
6857                    }
6858                }
6859            }
6860            Token::BindNotMatch => {
6861                let line = left.line;
6862                self.advance();
6863                match self.peek().clone() {
6864                    Token::Regex(pattern, flags, delim) => {
6865                        self.advance();
6866                        Ok(Expr {
6867                            kind: ExprKind::UnaryOp {
6868                                op: UnaryOp::LogNot,
6869                                expr: Box::new(Expr {
6870                                    kind: ExprKind::Match {
6871                                        expr: Box::new(left),
6872                                        pattern,
6873                                        flags,
6874                                        scalar_g: false,
6875                                        delim,
6876                                    },
6877                                    line,
6878                                }),
6879                            },
6880                            line,
6881                        })
6882                    }
6883                    Token::Ident(ref s) if s.starts_with('\x00') => {
6884                        let (Token::Ident(encoded), _) = self.advance() else {
6885                            unreachable!()
6886                        };
6887                        let parts: Vec<&str> = encoded.split('\x00').collect();
6888                        if parts.len() >= 4 && parts[1] == "s" {
6889                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6890                            Ok(Expr {
6891                                kind: ExprKind::UnaryOp {
6892                                    op: UnaryOp::LogNot,
6893                                    expr: Box::new(Expr {
6894                                        kind: ExprKind::Substitution {
6895                                            expr: Box::new(left),
6896                                            pattern: parts[2].to_string(),
6897                                            replacement: parts[3].to_string(),
6898                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6899                                            delim,
6900                                        },
6901                                        line,
6902                                    }),
6903                                },
6904                                line,
6905                            })
6906                        } else if parts.len() >= 4 && parts[1] == "tr" {
6907                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6908                            Ok(Expr {
6909                                kind: ExprKind::UnaryOp {
6910                                    op: UnaryOp::LogNot,
6911                                    expr: Box::new(Expr {
6912                                        kind: ExprKind::Transliterate {
6913                                            expr: Box::new(left),
6914                                            from: parts[2].to_string(),
6915                                            to: parts[3].to_string(),
6916                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6917                                            delim,
6918                                        },
6919                                        line,
6920                                    }),
6921                                },
6922                                line,
6923                            })
6924                        } else {
6925                            Err(self.syntax_err("Invalid regex binding after !~", line))
6926                        }
6927                    }
6928                    _ => {
6929                        let rhs = self.parse_unary()?;
6930                        Ok(Expr {
6931                            kind: ExprKind::BinOp {
6932                                left: Box::new(left),
6933                                op: BinOp::BindNotMatch,
6934                                right: Box::new(rhs),
6935                            },
6936                            line,
6937                        })
6938                    }
6939                }
6940            }
6941            _ => Ok(left),
6942        }
6943    }
6944
6945    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
6946    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
6947    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
6948        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
6949        let result = self.parse_range();
6950        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
6951        result
6952    }
6953
6954    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
6955    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
6956    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
6957    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
6958    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
6959    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
6960    fn parse_range(&mut self) -> PerlResult<Expr> {
6961        let left = self.parse_log_or()?;
6962        let line = left.line;
6963        // `1..10` or `1...10` (traditional) or `1:10` (short form)
6964        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
6965            (true, false)
6966        } else if self.eat(&Token::Range) {
6967            (false, false)
6968        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
6969            // `1:10` short form — only valid for numeric ranges, not ternary
6970            // Lookahead: must be followed by something that looks like a range endpoint
6971            (false, true)
6972        } else {
6973            return Ok(left);
6974        };
6975        let right = self.parse_log_or()?;
6976        // Optional step: `1..100:2` or `1:100:2`
6977        let step = if self.eat(&Token::Colon) {
6978            Some(Box::new(self.parse_unary()?))
6979        } else {
6980            None
6981        };
6982        Ok(Expr {
6983            kind: ExprKind::Range {
6984                from: Box::new(left),
6985                to: Box::new(right),
6986                exclusive,
6987                step,
6988            },
6989            line,
6990        })
6991    }
6992
6993    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
6994    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
6995        let mut name = match self.advance() {
6996            (Token::Ident(n), _) => n,
6997            (tok, l) => {
6998                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
6999            }
7000        };
7001        while self.eat(&Token::PackageSep) {
7002            match self.advance() {
7003                (Token::Ident(part), _) => {
7004                    name.push_str("::");
7005                    name.push_str(&part);
7006                }
7007                (tok, l) => {
7008                    return Err(self
7009                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
7010                }
7011            }
7012        }
7013        Ok(name)
7014    }
7015
7016    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
7017    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
7018        self.parse_package_qualified_identifier()
7019    }
7020
7021    fn parse_unary(&mut self) -> PerlResult<Expr> {
7022        let line = self.peek_line();
7023        match self.peek().clone() {
7024            Token::Minus => {
7025                self.advance();
7026                let expr = self.parse_power()?;
7027                Ok(Expr {
7028                    kind: ExprKind::UnaryOp {
7029                        op: UnaryOp::Negate,
7030                        expr: Box::new(expr),
7031                    },
7032                    line,
7033                })
7034            }
7035            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
7036            // and for scalar context; treat as a no-op on the parsed operand.
7037            Token::Plus => {
7038                self.advance();
7039                self.parse_unary()
7040            }
7041            Token::LogNot => {
7042                self.advance();
7043                let expr = self.parse_unary()?;
7044                Ok(Expr {
7045                    kind: ExprKind::UnaryOp {
7046                        op: UnaryOp::LogNot,
7047                        expr: Box::new(expr),
7048                    },
7049                    line,
7050                })
7051            }
7052            Token::BitNot => {
7053                self.advance();
7054                let expr = self.parse_unary()?;
7055                Ok(Expr {
7056                    kind: ExprKind::UnaryOp {
7057                        op: UnaryOp::BitNot,
7058                        expr: Box::new(expr),
7059                    },
7060                    line,
7061                })
7062            }
7063            Token::Increment => {
7064                self.advance();
7065                let expr = self.parse_postfix()?;
7066                Ok(Expr {
7067                    kind: ExprKind::UnaryOp {
7068                        op: UnaryOp::PreIncrement,
7069                        expr: Box::new(expr),
7070                    },
7071                    line,
7072                })
7073            }
7074            Token::Decrement => {
7075                self.advance();
7076                let expr = self.parse_postfix()?;
7077                Ok(Expr {
7078                    kind: ExprKind::UnaryOp {
7079                        op: UnaryOp::PreDecrement,
7080                        expr: Box::new(expr),
7081                    },
7082                    line,
7083                })
7084            }
7085            Token::BitAnd => {
7086                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
7087                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
7088                self.advance();
7089                if matches!(self.peek(), Token::LBrace) {
7090                    self.advance();
7091                    let inner = self.parse_expression()?;
7092                    self.expect(&Token::RBrace)?;
7093                    return Ok(Expr {
7094                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
7095                        line,
7096                    });
7097                }
7098                if matches!(self.peek(), Token::Ident(_)) {
7099                    let name = self.parse_qualified_subroutine_name()?;
7100                    return Ok(Expr {
7101                        kind: ExprKind::SubroutineRef(name),
7102                        line,
7103                    });
7104                }
7105                let target = self.parse_primary()?;
7106                if matches!(self.peek(), Token::LParen) {
7107                    self.advance();
7108                    let args = self.parse_arg_list()?;
7109                    self.expect(&Token::RParen)?;
7110                    return Ok(Expr {
7111                        kind: ExprKind::IndirectCall {
7112                            target: Box::new(target),
7113                            args,
7114                            ampersand: true,
7115                            pass_caller_arglist: false,
7116                        },
7117                        line,
7118                    });
7119                }
7120                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
7121                Ok(Expr {
7122                    kind: ExprKind::IndirectCall {
7123                        target: Box::new(target),
7124                        args: vec![],
7125                        ampersand: true,
7126                        pass_caller_arglist: true,
7127                    },
7128                    line,
7129                })
7130            }
7131            Token::Backslash => {
7132                self.advance();
7133                let expr = self.parse_unary()?;
7134                if let ExprKind::SubroutineRef(name) = expr.kind {
7135                    return Ok(Expr {
7136                        kind: ExprKind::SubroutineCodeRef(name),
7137                        line,
7138                    });
7139                }
7140                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
7141                    return Ok(expr);
7142                }
7143                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
7144                Ok(Expr {
7145                    kind: ExprKind::ScalarRef(Box::new(expr)),
7146                    line,
7147                })
7148            }
7149            Token::FileTest(op) => {
7150                self.advance();
7151                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
7152                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
7153                    Expr {
7154                        kind: ExprKind::ScalarVar("_".into()),
7155                        line: self.peek_line(),
7156                    }
7157                } else {
7158                    self.parse_unary()?
7159                };
7160                Ok(Expr {
7161                    kind: ExprKind::FileTest {
7162                        op,
7163                        expr: Box::new(expr),
7164                    },
7165                    line,
7166                })
7167            }
7168            _ => self.parse_power(),
7169        }
7170    }
7171
7172    fn parse_power(&mut self) -> PerlResult<Expr> {
7173        let left = self.parse_postfix()?;
7174        if matches!(self.peek(), Token::Power) {
7175            let line = left.line;
7176            self.advance();
7177            let right = self.parse_unary()?; // right-associative
7178            return Ok(Expr {
7179                kind: ExprKind::BinOp {
7180                    left: Box::new(left),
7181                    op: BinOp::Pow,
7182                    right: Box::new(right),
7183                },
7184                line,
7185            });
7186        }
7187        Ok(left)
7188    }
7189
7190    fn parse_postfix(&mut self) -> PerlResult<Expr> {
7191        let mut expr = self.parse_primary()?;
7192        loop {
7193            match self.peek().clone() {
7194                Token::Increment => {
7195                    // Implicit semicolon: `++` on a new line is a prefix operator
7196                    // on the next statement, not postfix on the previous expression.
7197                    if self.peek_line() > self.prev_line() {
7198                        break;
7199                    }
7200                    let line = expr.line;
7201                    self.advance();
7202                    expr = Expr {
7203                        kind: ExprKind::PostfixOp {
7204                            expr: Box::new(expr),
7205                            op: PostfixOp::Increment,
7206                        },
7207                        line,
7208                    };
7209                }
7210                Token::Decrement => {
7211                    // Implicit semicolon: `--` on a new line is a prefix operator
7212                    // on the next statement, not postfix on the previous expression.
7213                    if self.peek_line() > self.prev_line() {
7214                        break;
7215                    }
7216                    let line = expr.line;
7217                    self.advance();
7218                    expr = Expr {
7219                        kind: ExprKind::PostfixOp {
7220                            expr: Box::new(expr),
7221                            op: PostfixOp::Decrement,
7222                        },
7223                        line,
7224                    };
7225                }
7226                Token::LParen => {
7227                    if self.suppress_indirect_paren_call > 0 {
7228                        break;
7229                    }
7230                    // Implicit semicolon: `(` on a new line after an expression
7231                    // is a new statement, not a postfix code-ref call.
7232                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
7233                    if self.peek_line() > self.prev_line() {
7234                        break;
7235                    }
7236                    let line = expr.line;
7237                    self.advance();
7238                    let args = self.parse_arg_list()?;
7239                    self.expect(&Token::RParen)?;
7240                    expr = Expr {
7241                        kind: ExprKind::IndirectCall {
7242                            target: Box::new(expr),
7243                            args,
7244                            ampersand: false,
7245                            pass_caller_arglist: false,
7246                        },
7247                        line,
7248                    };
7249                }
7250                Token::Arrow => {
7251                    let line = expr.line;
7252                    self.advance();
7253                    match self.peek().clone() {
7254                        Token::LBracket => {
7255                            self.advance();
7256                            let index = self.parse_expression()?;
7257                            self.expect(&Token::RBracket)?;
7258                            expr = Expr {
7259                                kind: ExprKind::ArrowDeref {
7260                                    expr: Box::new(expr),
7261                                    index: Box::new(index),
7262                                    kind: DerefKind::Array,
7263                                },
7264                                line,
7265                            };
7266                        }
7267                        Token::LBrace => {
7268                            self.advance();
7269                            let key = self.parse_hash_subscript_key()?;
7270                            self.expect(&Token::RBrace)?;
7271                            expr = Expr {
7272                                kind: ExprKind::ArrowDeref {
7273                                    expr: Box::new(expr),
7274                                    index: Box::new(key),
7275                                    kind: DerefKind::Hash,
7276                                },
7277                                line,
7278                            };
7279                        }
7280                        Token::LParen => {
7281                            self.advance();
7282                            let args = self.parse_arg_list()?;
7283                            self.expect(&Token::RParen)?;
7284                            expr = Expr {
7285                                kind: ExprKind::ArrowDeref {
7286                                    expr: Box::new(expr),
7287                                    index: Box::new(Expr {
7288                                        kind: ExprKind::List(args),
7289                                        line,
7290                                    }),
7291                                    kind: DerefKind::Call,
7292                                },
7293                                line,
7294                            };
7295                        }
7296                        Token::Ident(method) => {
7297                            self.advance();
7298                            if method == "SUPER" {
7299                                self.expect(&Token::PackageSep)?;
7300                                let real_method = match self.advance() {
7301                                    (Token::Ident(n), _) => n,
7302                                    (tok, l) => {
7303                                        return Err(self.syntax_err(
7304                                            format!(
7305                                                "Expected method name after SUPER::, got {:?}",
7306                                                tok
7307                                            ),
7308                                            l,
7309                                        ));
7310                                    }
7311                                };
7312                                let args = if self.eat(&Token::LParen) {
7313                                    let a = self.parse_arg_list()?;
7314                                    self.expect(&Token::RParen)?;
7315                                    a
7316                                } else {
7317                                    self.parse_method_arg_list_no_paren()?
7318                                };
7319                                expr = Expr {
7320                                    kind: ExprKind::MethodCall {
7321                                        object: Box::new(expr),
7322                                        method: real_method,
7323                                        args,
7324                                        super_call: true,
7325                                    },
7326                                    line,
7327                                };
7328                            } else {
7329                                let mut method_name = method;
7330                                while self.eat(&Token::PackageSep) {
7331                                    match self.advance() {
7332                                        (Token::Ident(part), _) => {
7333                                            method_name.push_str("::");
7334                                            method_name.push_str(&part);
7335                                        }
7336                                        (tok, l) => {
7337                                            return Err(self.syntax_err(
7338                                                format!(
7339                                                    "Expected identifier after :: in method name, got {:?}",
7340                                                    tok
7341                                                ),
7342                                                l,
7343                                            ));
7344                                        }
7345                                    }
7346                                }
7347                                let args = if self.eat(&Token::LParen) {
7348                                    let a = self.parse_arg_list()?;
7349                                    self.expect(&Token::RParen)?;
7350                                    a
7351                                } else {
7352                                    self.parse_method_arg_list_no_paren()?
7353                                };
7354                                expr = Expr {
7355                                    kind: ExprKind::MethodCall {
7356                                        object: Box::new(expr),
7357                                        method: method_name,
7358                                        args,
7359                                        super_call: false,
7360                                    },
7361                                    line,
7362                                };
7363                            }
7364                        }
7365                        // Postfix dereference (Perl 5.20+, default 5.24+):
7366                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
7367                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
7368                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
7369                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
7370                        Token::ArrayAt => {
7371                            self.advance(); // consume `@`
7372                            match self.peek().clone() {
7373                                Token::Star => {
7374                                    self.advance();
7375                                    expr = Expr {
7376                                        kind: ExprKind::Deref {
7377                                            expr: Box::new(expr),
7378                                            kind: Sigil::Array,
7379                                        },
7380                                        line,
7381                                    };
7382                                }
7383                                Token::LBracket => {
7384                                    self.advance();
7385                                    let mut indices = Vec::new();
7386                                    while !matches!(self.peek(), Token::RBracket | Token::Eof) {
7387                                        indices.push(self.parse_assign_expr()?);
7388                                        if !self.eat(&Token::Comma) {
7389                                            break;
7390                                        }
7391                                    }
7392                                    self.expect(&Token::RBracket)?;
7393                                    let source = Expr {
7394                                        kind: ExprKind::Deref {
7395                                            expr: Box::new(expr),
7396                                            kind: Sigil::Array,
7397                                        },
7398                                        line,
7399                                    };
7400                                    expr = Expr {
7401                                        kind: ExprKind::AnonymousListSlice {
7402                                            source: Box::new(source),
7403                                            indices,
7404                                        },
7405                                        line,
7406                                    };
7407                                }
7408                                Token::LBrace => {
7409                                    self.advance();
7410                                    let mut keys = Vec::new();
7411                                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7412                                        keys.push(self.parse_assign_expr()?);
7413                                        if !self.eat(&Token::Comma) {
7414                                            break;
7415                                        }
7416                                    }
7417                                    self.expect(&Token::RBrace)?;
7418                                    expr = Expr {
7419                                        kind: ExprKind::HashSliceDeref {
7420                                            container: Box::new(expr),
7421                                            keys,
7422                                        },
7423                                        line,
7424                                    };
7425                                }
7426                                tok => {
7427                                    return Err(self.syntax_err(
7428                                        format!(
7429                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
7430                                            tok
7431                                        ),
7432                                        line,
7433                                    ));
7434                                }
7435                            }
7436                        }
7437                        Token::HashPercent => {
7438                            self.advance(); // consume `%`
7439                            match self.peek().clone() {
7440                                Token::Star => {
7441                                    self.advance();
7442                                    expr = Expr {
7443                                        kind: ExprKind::Deref {
7444                                            expr: Box::new(expr),
7445                                            kind: Sigil::Hash,
7446                                        },
7447                                        line,
7448                                    };
7449                                }
7450                                tok => {
7451                                    return Err(self.syntax_err(
7452                                        format!("Expected `*` after `->%`, got {:?}", tok),
7453                                        line,
7454                                    ));
7455                                }
7456                            }
7457                        }
7458                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
7459                        Token::X => {
7460                            self.advance();
7461                            let args = if self.eat(&Token::LParen) {
7462                                let a = self.parse_arg_list()?;
7463                                self.expect(&Token::RParen)?;
7464                                a
7465                            } else {
7466                                self.parse_method_arg_list_no_paren()?
7467                            };
7468                            expr = Expr {
7469                                kind: ExprKind::MethodCall {
7470                                    object: Box::new(expr),
7471                                    method: "x".to_string(),
7472                                    args,
7473                                    super_call: false,
7474                                },
7475                                line,
7476                            };
7477                        }
7478                        _ => break,
7479                    }
7480                }
7481                Token::LBracket => {
7482                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
7483                    let line = expr.line;
7484                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
7485                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7486                            let name = name.clone();
7487                            self.advance();
7488                            let index = self.parse_expression()?;
7489                            self.expect(&Token::RBracket)?;
7490                            expr = Expr {
7491                                kind: ExprKind::ArrayElement {
7492                                    array: name,
7493                                    index: Box::new(index),
7494                                },
7495                                line,
7496                            };
7497                        }
7498                    } else if postfix_lbracket_is_arrow_container(&expr) {
7499                        self.advance();
7500                        let indices = self.parse_arg_list()?;
7501                        self.expect(&Token::RBracket)?;
7502                        expr = Expr {
7503                            kind: ExprKind::ArrowDeref {
7504                                expr: Box::new(expr),
7505                                index: Box::new(Expr {
7506                                    kind: ExprKind::List(indices),
7507                                    line,
7508                                }),
7509                                kind: DerefKind::Array,
7510                            },
7511                            line,
7512                        };
7513                    } else {
7514                        self.advance();
7515                        let indices = self.parse_arg_list()?;
7516                        self.expect(&Token::RBracket)?;
7517                        expr = Expr {
7518                            kind: ExprKind::AnonymousListSlice {
7519                                source: Box::new(expr),
7520                                indices,
7521                            },
7522                            line,
7523                        };
7524                    }
7525                }
7526                Token::LBrace => {
7527                    if self.suppress_scalar_hash_brace > 0 {
7528                        break;
7529                    }
7530                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
7531                    // not a hash subscript on the preceding expression.
7532                    if self.peek_line() > self.prev_line() {
7533                        break;
7534                    }
7535                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
7536                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
7537                    let line = expr.line;
7538                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
7539                    let is_chainable_hash_subscript = is_scalar_named_hash
7540                        || matches!(
7541                            expr.kind,
7542                            ExprKind::HashElement { .. }
7543                                | ExprKind::ArrayElement { .. }
7544                                | ExprKind::ArrowDeref { .. }
7545                                | ExprKind::Deref {
7546                                    kind: Sigil::Scalar,
7547                                    ..
7548                                }
7549                        );
7550                    if !is_chainable_hash_subscript {
7551                        break;
7552                    }
7553                    self.advance();
7554                    let key = self.parse_hash_subscript_key()?;
7555                    self.expect(&Token::RBrace)?;
7556                    expr = if is_scalar_named_hash {
7557                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7558                            let name = name.clone();
7559                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
7560                            if name == "_" {
7561                                Expr {
7562                                    kind: ExprKind::ArrowDeref {
7563                                        expr: Box::new(Expr {
7564                                            kind: ExprKind::ScalarVar("_".into()),
7565                                            line,
7566                                        }),
7567                                        index: Box::new(key),
7568                                        kind: DerefKind::Hash,
7569                                    },
7570                                    line,
7571                                }
7572                            } else {
7573                                Expr {
7574                                    kind: ExprKind::HashElement {
7575                                        hash: name,
7576                                        key: Box::new(key),
7577                                    },
7578                                    line,
7579                                }
7580                            }
7581                        } else {
7582                            unreachable!("is_scalar_named_hash implies ScalarVar");
7583                        }
7584                    } else {
7585                        Expr {
7586                            kind: ExprKind::ArrowDeref {
7587                                expr: Box::new(expr),
7588                                index: Box::new(key),
7589                                kind: DerefKind::Hash,
7590                            },
7591                            line,
7592                        }
7593                    };
7594                }
7595                _ => break,
7596            }
7597        }
7598        Ok(expr)
7599    }
7600
7601    fn parse_primary(&mut self) -> PerlResult<Expr> {
7602        let line = self.peek_line();
7603        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
7604        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
7605        // assigned value(s); has the side effect of declaring the variable in
7606        // the current scope.  See `ExprKind::MyExpr`.
7607        if let Token::Ident(ref kw) = self.peek().clone() {
7608            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
7609                let kw_owned = kw.clone();
7610                // Parse exactly like the statement form via `parse_my_our_local`,
7611                // then unwrap the resulting `StmtKind::*` back into a list of
7612                // `VarDecl`s for the expression node.  This re-uses the full
7613                // syntax (typed sigs, list destructuring, type annotations).
7614                let saved_pos = self.pos;
7615                let stmt = self.parse_my_our_local(&kw_owned, false)?;
7616                let decls = match stmt.kind {
7617                    StmtKind::My(d)
7618                    | StmtKind::Our(d)
7619                    | StmtKind::State(d)
7620                    | StmtKind::Local(d) => d,
7621                    _ => {
7622                        // `local *FOO = …` / non-decl forms — fall back to the
7623                        // statement parser (already advanced); restore position
7624                        // and let the surrounding code handle it as a statement
7625                        // by erroring loudly here.
7626                        self.pos = saved_pos;
7627                        return Err(self.syntax_err(
7628                            "`my`/`our`/`local` in expression must declare variables",
7629                            line,
7630                        ));
7631                    }
7632                };
7633                return Ok(Expr {
7634                    kind: ExprKind::MyExpr {
7635                        keyword: kw_owned,
7636                        decls,
7637                    },
7638                    line,
7639                });
7640            }
7641        }
7642        match self.peek().clone() {
7643            Token::Integer(n) => {
7644                self.advance();
7645                Ok(Expr {
7646                    kind: ExprKind::Integer(n),
7647                    line,
7648                })
7649            }
7650            Token::Float(f) => {
7651                self.advance();
7652                Ok(Expr {
7653                    kind: ExprKind::Float(f),
7654                    line,
7655                })
7656            }
7657            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
7658            // Valid in any expression position; evaluates the block and yields its last value.
7659            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
7660            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
7661            // instead pipe-applied as a coderef — that path is never reached from here.
7662            Token::ArrowBrace => {
7663                self.advance();
7664                let mut stmts = Vec::new();
7665                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7666                    if self.eat(&Token::Semicolon) {
7667                        continue;
7668                    }
7669                    stmts.push(self.parse_statement()?);
7670                }
7671                self.expect(&Token::RBrace)?;
7672                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
7673                let inner = Expr {
7674                    kind: ExprKind::CodeRef {
7675                        params: vec![],
7676                        body: stmts,
7677                    },
7678                    line: inner_line,
7679                };
7680                Ok(Expr {
7681                    kind: ExprKind::Do(Box::new(inner)),
7682                    line,
7683                })
7684            }
7685            Token::Star => {
7686                self.advance();
7687                if matches!(self.peek(), Token::LBrace) {
7688                    self.advance();
7689                    let inner = self.parse_expression()?;
7690                    self.expect(&Token::RBrace)?;
7691                    return Ok(Expr {
7692                        kind: ExprKind::Deref {
7693                            expr: Box::new(inner),
7694                            kind: Sigil::Typeglob,
7695                        },
7696                        line,
7697                    });
7698                }
7699                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
7700                if matches!(
7701                    self.peek(),
7702                    Token::ScalarVar(_)
7703                        | Token::ArrayVar(_)
7704                        | Token::HashVar(_)
7705                        | Token::DerefScalarVar(_)
7706                        | Token::HashPercent
7707                ) {
7708                    let inner = self.parse_postfix()?;
7709                    return Ok(Expr {
7710                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
7711                        line,
7712                    });
7713                }
7714                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
7715                let mut full_name = match self.advance() {
7716                    (Token::Ident(n), _) => n,
7717                    (Token::X, _) => "x".to_string(),
7718                    (tok, l) => {
7719                        return Err(self
7720                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
7721                    }
7722                };
7723                while self.eat(&Token::PackageSep) {
7724                    match self.advance() {
7725                        (Token::Ident(part), _) => {
7726                            full_name = format!("{}::{}", full_name, part);
7727                        }
7728                        (Token::X, _) => {
7729                            full_name = format!("{}::x", full_name);
7730                        }
7731                        (tok, l) => {
7732                            return Err(self.syntax_err(
7733                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
7734                                l,
7735                            ));
7736                        }
7737                    }
7738                }
7739                Ok(Expr {
7740                    kind: ExprKind::Typeglob(full_name),
7741                    line,
7742                })
7743            }
7744            Token::SingleString(s) => {
7745                self.advance();
7746                Ok(Expr {
7747                    kind: ExprKind::String(s),
7748                    line,
7749                })
7750            }
7751            Token::DoubleString(s) => {
7752                self.advance();
7753                self.parse_interpolated_string(&s, line)
7754            }
7755            Token::BacktickString(s) => {
7756                self.advance();
7757                let inner = self.parse_interpolated_string(&s, line)?;
7758                Ok(Expr {
7759                    kind: ExprKind::Qx(Box::new(inner)),
7760                    line,
7761                })
7762            }
7763            Token::HereDoc(_, body, interpolate) => {
7764                self.advance();
7765                if interpolate {
7766                    self.parse_interpolated_string(&body, line)
7767                } else {
7768                    Ok(Expr {
7769                        kind: ExprKind::String(body),
7770                        line,
7771                    })
7772                }
7773            }
7774            Token::Regex(pattern, flags, _delim) => {
7775                self.advance();
7776                Ok(Expr {
7777                    kind: ExprKind::Regex(pattern, flags),
7778                    line,
7779                })
7780            }
7781            Token::QW(words) => {
7782                self.advance();
7783                Ok(Expr {
7784                    kind: ExprKind::QW(words),
7785                    line,
7786                })
7787            }
7788            Token::DerefScalarVar(name) => {
7789                self.advance();
7790                Ok(Expr {
7791                    kind: ExprKind::Deref {
7792                        expr: Box::new(Expr {
7793                            kind: ExprKind::ScalarVar(name),
7794                            line,
7795                        }),
7796                        kind: Sigil::Scalar,
7797                    },
7798                    line,
7799                })
7800            }
7801            Token::ScalarVar(name) => {
7802                self.advance();
7803                Ok(Expr {
7804                    kind: ExprKind::ScalarVar(name),
7805                    line,
7806                })
7807            }
7808            Token::ArrayVar(name) => {
7809                self.advance();
7810                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
7811                match self.peek() {
7812                    Token::LBracket => {
7813                        self.advance();
7814                        let indices = self.parse_arg_list()?;
7815                        self.expect(&Token::RBracket)?;
7816                        Ok(Expr {
7817                            kind: ExprKind::ArraySlice {
7818                                array: name,
7819                                indices,
7820                            },
7821                            line,
7822                        })
7823                    }
7824                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
7825                        self.advance();
7826                        let keys = self.parse_arg_list()?;
7827                        self.expect(&Token::RBrace)?;
7828                        Ok(Expr {
7829                            kind: ExprKind::HashSlice { hash: name, keys },
7830                            line,
7831                        })
7832                    }
7833                    _ => Ok(Expr {
7834                        kind: ExprKind::ArrayVar(name),
7835                        line,
7836                    }),
7837                }
7838            }
7839            Token::HashVar(name) => {
7840                self.advance();
7841                Ok(Expr {
7842                    kind: ExprKind::HashVar(name),
7843                    line,
7844                })
7845            }
7846            Token::HashPercent => {
7847                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
7848                self.advance();
7849                if matches!(self.peek(), Token::ScalarVar(_)) {
7850                    let n = match self.advance() {
7851                        (Token::ScalarVar(n), _) => n,
7852                        (tok, l) => {
7853                            return Err(self.syntax_err(
7854                                format!("Expected scalar variable after %%, got {:?}", tok),
7855                                l,
7856                            ));
7857                        }
7858                    };
7859                    return Ok(Expr {
7860                        kind: ExprKind::Deref {
7861                            expr: Box::new(Expr {
7862                                kind: ExprKind::ScalarVar(n),
7863                                line,
7864                            }),
7865                            kind: Sigil::Hash,
7866                        },
7867                        line,
7868                    });
7869                }
7870                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
7871                // anonymous hashref inline, using `[...]` as the delimiter to avoid
7872                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
7873                // Real Perl errors on `%[...]` syntactically, so no compat risk.
7874                if matches!(self.peek(), Token::LBracket) {
7875                    self.advance();
7876                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7877                    self.expect(&Token::RBracket)?;
7878                    let href = Expr {
7879                        kind: ExprKind::HashRef(pairs),
7880                        line,
7881                    };
7882                    return Ok(Expr {
7883                        kind: ExprKind::Deref {
7884                            expr: Box::new(href),
7885                            kind: Sigil::Hash,
7886                        },
7887                        line,
7888                    });
7889                }
7890                self.expect(&Token::LBrace)?;
7891                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
7892                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
7893                // heuristic is famously unreliable — when the first non-whitespace
7894                // token is an ident/string followed by `=>`, treat the whole thing
7895                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
7896                let looks_like_pair = matches!(
7897                    self.peek(),
7898                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7899                ) && matches!(self.peek_at(1), Token::FatArrow);
7900                let inner = if looks_like_pair {
7901                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7902                    Expr {
7903                        kind: ExprKind::HashRef(pairs),
7904                        line,
7905                    }
7906                } else {
7907                    self.parse_expression()?
7908                };
7909                self.expect(&Token::RBrace)?;
7910                Ok(Expr {
7911                    kind: ExprKind::Deref {
7912                        expr: Box::new(inner),
7913                        kind: Sigil::Hash,
7914                    },
7915                    line,
7916                })
7917            }
7918            Token::ArrayAt => {
7919                self.advance();
7920                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
7921                if matches!(self.peek(), Token::LBrace) {
7922                    self.advance();
7923                    let inner = self.parse_expression()?;
7924                    self.expect(&Token::RBrace)?;
7925                    return Ok(Expr {
7926                        kind: ExprKind::Deref {
7927                            expr: Box::new(inner),
7928                            kind: Sigil::Array,
7929                        },
7930                        line,
7931                    });
7932                }
7933                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
7934                // anonymous arrayref inline. Real Perl rejects `@[...]` at
7935                // the parser level, so this extension has no compat risk.
7936                if matches!(self.peek(), Token::LBracket) {
7937                    self.advance();
7938                    let mut elems = Vec::new();
7939                    if !matches!(self.peek(), Token::RBracket) {
7940                        elems.push(self.parse_assign_expr()?);
7941                        while self.eat(&Token::Comma) {
7942                            if matches!(self.peek(), Token::RBracket) {
7943                                break;
7944                            }
7945                            elems.push(self.parse_assign_expr()?);
7946                        }
7947                    }
7948                    self.expect(&Token::RBracket)?;
7949                    let aref = Expr {
7950                        kind: ExprKind::ArrayRef(elems),
7951                        line,
7952                    };
7953                    return Ok(Expr {
7954                        kind: ExprKind::Deref {
7955                            expr: Box::new(aref),
7956                            kind: Sigil::Array,
7957                        },
7958                        line,
7959                    });
7960                }
7961                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
7962                let container = match self.peek().clone() {
7963                    Token::ScalarVar(n) => {
7964                        self.advance();
7965                        Expr {
7966                            kind: ExprKind::ScalarVar(n),
7967                            line,
7968                        }
7969                    }
7970                    _ => {
7971                        return Err(self.syntax_err(
7972                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
7973                            line,
7974                        ));
7975                    }
7976                };
7977                if matches!(self.peek(), Token::LBrace) {
7978                    self.advance();
7979                    let keys = self.parse_arg_list()?;
7980                    self.expect(&Token::RBrace)?;
7981                    return Ok(Expr {
7982                        kind: ExprKind::HashSliceDeref {
7983                            container: Box::new(container),
7984                            keys,
7985                        },
7986                        line,
7987                    });
7988                }
7989                Ok(Expr {
7990                    kind: ExprKind::Deref {
7991                        expr: Box::new(container),
7992                        kind: Sigil::Array,
7993                    },
7994                    line,
7995                })
7996            }
7997            Token::LParen => {
7998                self.advance();
7999                if matches!(self.peek(), Token::RParen) {
8000                    self.advance();
8001                    return Ok(Expr {
8002                        kind: ExprKind::List(vec![]),
8003                        line,
8004                    });
8005                }
8006                // Inside parens, pipe-forward is allowed even if we're in a
8007                // paren-less arg context. Save and restore no_pipe_forward_depth.
8008                let saved_no_pipe = self.no_pipe_forward_depth;
8009                self.no_pipe_forward_depth = 0;
8010                let expr = self.parse_expression();
8011                self.no_pipe_forward_depth = saved_no_pipe;
8012                let expr = expr?;
8013                self.expect(&Token::RParen)?;
8014                Ok(expr)
8015            }
8016            Token::LBracket => {
8017                self.advance();
8018                let elems = self.parse_arg_list()?;
8019                self.expect(&Token::RBracket)?;
8020                Ok(Expr {
8021                    kind: ExprKind::ArrayRef(elems),
8022                    line,
8023                })
8024            }
8025            Token::LBrace => {
8026                // Could be hash ref or block — disambiguate
8027                self.advance();
8028                // Try to parse as hash ref: { key => val, ... }
8029                let saved = self.pos;
8030                match self.try_parse_hash_ref() {
8031                    Ok(pairs) => Ok(Expr {
8032                        kind: ExprKind::HashRef(pairs),
8033                        line,
8034                    }),
8035                    Err(_) => {
8036                        self.pos = saved;
8037                        // Parse as block, wrap in code ref
8038                        let mut stmts = Vec::new();
8039                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8040                            if self.eat(&Token::Semicolon) {
8041                                continue;
8042                            }
8043                            stmts.push(self.parse_statement()?);
8044                        }
8045                        self.expect(&Token::RBrace)?;
8046                        Ok(Expr {
8047                            kind: ExprKind::CodeRef {
8048                                params: vec![],
8049                                body: stmts,
8050                            },
8051                            line,
8052                        })
8053                    }
8054                }
8055            }
8056            Token::Diamond => {
8057                self.advance();
8058                Ok(Expr {
8059                    kind: ExprKind::ReadLine(None),
8060                    line,
8061                })
8062            }
8063            Token::ReadLine(handle) => {
8064                self.advance();
8065                Ok(Expr {
8066                    kind: ExprKind::ReadLine(Some(handle)),
8067                    line,
8068                })
8069            }
8070
8071            // Named functions / builtins
8072            Token::ThreadArrow => {
8073                self.advance();
8074                self.parse_thread_macro(line, false)
8075            }
8076            Token::ThreadArrowLast => {
8077                self.advance();
8078                self.parse_thread_macro(line, true)
8079            }
8080            Token::Ident(ref name) => {
8081                let name = name.clone();
8082                // Handle s///
8083                if name.starts_with('\x00') {
8084                    self.advance();
8085                    let parts: Vec<&str> = name.split('\x00').collect();
8086                    if parts.len() >= 4 && parts[1] == "s" {
8087                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8088                        return Ok(Expr {
8089                            kind: ExprKind::Substitution {
8090                                expr: Box::new(Expr {
8091                                    kind: ExprKind::ScalarVar("_".into()),
8092                                    line,
8093                                }),
8094                                pattern: parts[2].to_string(),
8095                                replacement: parts[3].to_string(),
8096                                flags: parts.get(4).unwrap_or(&"").to_string(),
8097                                delim,
8098                            },
8099                            line,
8100                        });
8101                    }
8102                    if parts.len() >= 4 && parts[1] == "tr" {
8103                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8104                        return Ok(Expr {
8105                            kind: ExprKind::Transliterate {
8106                                expr: Box::new(Expr {
8107                                    kind: ExprKind::ScalarVar("_".into()),
8108                                    line,
8109                                }),
8110                                from: parts[2].to_string(),
8111                                to: parts[3].to_string(),
8112                                flags: parts.get(4).unwrap_or(&"").to_string(),
8113                                delim,
8114                            },
8115                            line,
8116                        });
8117                    }
8118                    return Err(self.syntax_err("Unexpected encoded token", line));
8119                }
8120                self.parse_named_expr(name)
8121            }
8122
8123            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
8124            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
8125            Token::Percent => {
8126                self.advance();
8127                match self.peek().clone() {
8128                    Token::Ident(name) => {
8129                        self.advance();
8130                        Ok(Expr {
8131                            kind: ExprKind::HashVar(name),
8132                            line,
8133                        })
8134                    }
8135                    Token::ScalarVar(n) => {
8136                        self.advance();
8137                        Ok(Expr {
8138                            kind: ExprKind::Deref {
8139                                expr: Box::new(Expr {
8140                                    kind: ExprKind::ScalarVar(n),
8141                                    line,
8142                                }),
8143                                kind: Sigil::Hash,
8144                            },
8145                            line,
8146                        })
8147                    }
8148                    Token::LBrace => {
8149                        self.advance();
8150                        let looks_like_pair = matches!(
8151                            self.peek(),
8152                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8153                        ) && matches!(self.peek_at(1), Token::FatArrow);
8154                        let inner = if looks_like_pair {
8155                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8156                            Expr {
8157                                kind: ExprKind::HashRef(pairs),
8158                                line,
8159                            }
8160                        } else {
8161                            self.parse_expression()?
8162                        };
8163                        self.expect(&Token::RBrace)?;
8164                        Ok(Expr {
8165                            kind: ExprKind::Deref {
8166                                expr: Box::new(inner),
8167                                kind: Sigil::Hash,
8168                            },
8169                            line,
8170                        })
8171                    }
8172                    Token::LBracket => {
8173                        self.advance();
8174                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8175                        self.expect(&Token::RBracket)?;
8176                        let href = Expr {
8177                            kind: ExprKind::HashRef(pairs),
8178                            line,
8179                        };
8180                        Ok(Expr {
8181                            kind: ExprKind::Deref {
8182                                expr: Box::new(href),
8183                                kind: Sigil::Hash,
8184                            },
8185                            line,
8186                        })
8187                    }
8188                    tok => Err(self.syntax_err(
8189                        format!(
8190                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
8191                            tok
8192                        ),
8193                        line,
8194                    )),
8195                }
8196            }
8197
8198            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
8199        }
8200    }
8201
8202    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
8203        let line = self.peek_line();
8204        self.advance(); // consume the ident
8205        while self.eat(&Token::PackageSep) {
8206            match self.advance() {
8207                (Token::Ident(part), _) => {
8208                    name = format!("{}::{}", name, part);
8209                }
8210                (tok, err_line) => {
8211                    return Err(self.syntax_err(
8212                        format!("Expected identifier after `::`, got {:?}", tok),
8213                        err_line,
8214                    ));
8215                }
8216            }
8217        }
8218
8219        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
8220        // before `=>` is treated as a string key, matching Perl 5 semantics.
8221        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
8222        if matches!(self.peek(), Token::FatArrow) {
8223            return Ok(Expr {
8224                kind: ExprKind::String(name),
8225                line,
8226            });
8227        }
8228
8229        if crate::compat_mode() {
8230            if let Some(ext) = Self::stryke_extension_name(&name) {
8231                if !self.declared_subs.contains(&name) {
8232                    return Err(self.syntax_err(
8233                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
8234                        line,
8235                    ));
8236                }
8237            }
8238        }
8239
8240        match name.as_str() {
8241            "__FILE__" => Ok(Expr {
8242                kind: ExprKind::MagicConst(MagicConstKind::File),
8243                line,
8244            }),
8245            "__LINE__" => Ok(Expr {
8246                kind: ExprKind::MagicConst(MagicConstKind::Line),
8247                line,
8248            }),
8249            "__SUB__" => Ok(Expr {
8250                kind: ExprKind::MagicConst(MagicConstKind::Sub),
8251                line,
8252            }),
8253            "stdin" => Ok(Expr {
8254                kind: ExprKind::FuncCall {
8255                    name: "stdin".into(),
8256                    args: vec![],
8257                },
8258                line,
8259            }),
8260            "range" => {
8261                let args = self.parse_builtin_args()?;
8262                Ok(Expr {
8263                    kind: ExprKind::FuncCall {
8264                        name: "range".into(),
8265                        args,
8266                    },
8267                    line,
8268                })
8269            }
8270            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
8271            "say" => {
8272                if !crate::compat_mode() {
8273                    return Err(self.syntax_err(
8274                        "stryke uses `p` instead of `say` (this is not Perl 5)",
8275                        line,
8276                    ));
8277                }
8278                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
8279            }
8280            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
8281            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
8282            "die" => {
8283                let args = self.parse_list_until_terminator()?;
8284                Ok(Expr {
8285                    kind: ExprKind::Die(args),
8286                    line,
8287                })
8288            }
8289            "warn" => {
8290                let args = self.parse_list_until_terminator()?;
8291                Ok(Expr {
8292                    kind: ExprKind::Warn(args),
8293                    line,
8294                })
8295            }
8296            // `croak` / `confess` — `Carp` builtins available without `use Carp`
8297            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
8298            // `die` — TODO: croak should report caller's file/line, confess
8299            // should append a full stack trace.
8300            "croak" | "confess" => {
8301                let args = self.parse_list_until_terminator()?;
8302                Ok(Expr {
8303                    kind: ExprKind::Die(args),
8304                    line,
8305                })
8306            }
8307            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
8308            "carp" | "cluck" => {
8309                let args = self.parse_list_until_terminator()?;
8310                Ok(Expr {
8311                    kind: ExprKind::Warn(args),
8312                    line,
8313                })
8314            }
8315            "chomp" => {
8316                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8317                    return Ok(e);
8318                }
8319                let a = self.parse_one_arg_or_default()?;
8320                Ok(Expr {
8321                    kind: ExprKind::Chomp(Box::new(a)),
8322                    line,
8323                })
8324            }
8325            "chop" => {
8326                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8327                    return Ok(e);
8328                }
8329                let a = self.parse_one_arg_or_default()?;
8330                Ok(Expr {
8331                    kind: ExprKind::Chop(Box::new(a)),
8332                    line,
8333                })
8334            }
8335            "length" => {
8336                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8337                    return Ok(e);
8338                }
8339                let a = self.parse_one_arg_or_default()?;
8340                Ok(Expr {
8341                    kind: ExprKind::Length(Box::new(a)),
8342                    line,
8343                })
8344            }
8345            "defined" => {
8346                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8347                    return Ok(e);
8348                }
8349                let a = self.parse_one_arg_or_default()?;
8350                Ok(Expr {
8351                    kind: ExprKind::Defined(Box::new(a)),
8352                    line,
8353                })
8354            }
8355            "ref" => {
8356                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8357                    return Ok(e);
8358                }
8359                let a = self.parse_one_arg_or_default()?;
8360                Ok(Expr {
8361                    kind: ExprKind::Ref(Box::new(a)),
8362                    line,
8363                })
8364            }
8365            "undef" => {
8366                // `undef $var` sets `$var` to undef — but a variable on a new line
8367                // is a separate statement (implicit semicolon), not an argument.
8368                if self.peek_line() == self.prev_line()
8369                    && matches!(
8370                        self.peek(),
8371                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
8372                    )
8373                {
8374                    let target = self.parse_primary()?;
8375                    return Ok(Expr {
8376                        kind: ExprKind::Assign {
8377                            target: Box::new(target),
8378                            value: Box::new(Expr {
8379                                kind: ExprKind::Undef,
8380                                line,
8381                            }),
8382                        },
8383                        line,
8384                    });
8385                }
8386                Ok(Expr {
8387                    kind: ExprKind::Undef,
8388                    line,
8389                })
8390            }
8391            "scalar" => {
8392                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8393                    return Ok(e);
8394                }
8395                let a = self.parse_one_arg_or_default()?;
8396                Ok(Expr {
8397                    kind: ExprKind::ScalarContext(Box::new(a)),
8398                    line,
8399                })
8400            }
8401            "abs" => {
8402                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8403                    return Ok(e);
8404                }
8405                let a = self.parse_one_arg_or_default()?;
8406                Ok(Expr {
8407                    kind: ExprKind::Abs(Box::new(a)),
8408                    line,
8409                })
8410            }
8411            // stryke unary numeric extensions — treat like `abs` so a bare
8412            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
8413            // call with implicit `$_` rather than falling through to the
8414            // generic `Bareword` arm (which stringifies to `"inc"`).
8415            "inc" | "dec" => {
8416                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8417                    return Ok(e);
8418                }
8419                let a = self.parse_one_arg_or_default()?;
8420                Ok(Expr {
8421                    kind: ExprKind::FuncCall {
8422                        name,
8423                        args: vec![a],
8424                    },
8425                    line,
8426                })
8427            }
8428            "int" => {
8429                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8430                    return Ok(e);
8431                }
8432                let a = self.parse_one_arg_or_default()?;
8433                Ok(Expr {
8434                    kind: ExprKind::Int(Box::new(a)),
8435                    line,
8436                })
8437            }
8438            "sqrt" => {
8439                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8440                    return Ok(e);
8441                }
8442                let a = self.parse_one_arg_or_default()?;
8443                Ok(Expr {
8444                    kind: ExprKind::Sqrt(Box::new(a)),
8445                    line,
8446                })
8447            }
8448            "sin" => {
8449                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8450                    return Ok(e);
8451                }
8452                let a = self.parse_one_arg_or_default()?;
8453                Ok(Expr {
8454                    kind: ExprKind::Sin(Box::new(a)),
8455                    line,
8456                })
8457            }
8458            "cos" => {
8459                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8460                    return Ok(e);
8461                }
8462                let a = self.parse_one_arg_or_default()?;
8463                Ok(Expr {
8464                    kind: ExprKind::Cos(Box::new(a)),
8465                    line,
8466                })
8467            }
8468            "atan2" => {
8469                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8470                    return Ok(e);
8471                }
8472                let args = self.parse_builtin_args()?;
8473                if args.len() != 2 {
8474                    return Err(self.syntax_err("atan2 requires two arguments", line));
8475                }
8476                Ok(Expr {
8477                    kind: ExprKind::Atan2 {
8478                        y: Box::new(args[0].clone()),
8479                        x: Box::new(args[1].clone()),
8480                    },
8481                    line,
8482                })
8483            }
8484            "exp" => {
8485                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8486                    return Ok(e);
8487                }
8488                let a = self.parse_one_arg_or_default()?;
8489                Ok(Expr {
8490                    kind: ExprKind::Exp(Box::new(a)),
8491                    line,
8492                })
8493            }
8494            "log" => {
8495                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8496                    return Ok(e);
8497                }
8498                let a = self.parse_one_arg_or_default()?;
8499                Ok(Expr {
8500                    kind: ExprKind::Log(Box::new(a)),
8501                    line,
8502                })
8503            }
8504            "input" => {
8505                let args = if matches!(
8506                    self.peek(),
8507                    Token::Semicolon
8508                        | Token::RBrace
8509                        | Token::RParen
8510                        | Token::Eof
8511                        | Token::Comma
8512                        | Token::PipeForward
8513                ) {
8514                    vec![]
8515                } else if matches!(self.peek(), Token::LParen) {
8516                    self.advance();
8517                    if matches!(self.peek(), Token::RParen) {
8518                        self.advance();
8519                        vec![]
8520                    } else {
8521                        let a = self.parse_expression()?;
8522                        self.expect(&Token::RParen)?;
8523                        vec![a]
8524                    }
8525                } else {
8526                    let a = self.parse_one_arg()?;
8527                    vec![a]
8528                };
8529                Ok(Expr {
8530                    kind: ExprKind::FuncCall {
8531                        name: "input".to_string(),
8532                        args,
8533                    },
8534                    line,
8535                })
8536            }
8537            "rand" => {
8538                if matches!(
8539                    self.peek(),
8540                    Token::Semicolon
8541                        | Token::RBrace
8542                        | Token::RParen
8543                        | Token::Eof
8544                        | Token::Comma
8545                        | Token::PipeForward
8546                ) {
8547                    Ok(Expr {
8548                        kind: ExprKind::Rand(None),
8549                        line,
8550                    })
8551                } else if matches!(self.peek(), Token::LParen) {
8552                    self.advance();
8553                    if matches!(self.peek(), Token::RParen) {
8554                        self.advance();
8555                        Ok(Expr {
8556                            kind: ExprKind::Rand(None),
8557                            line,
8558                        })
8559                    } else {
8560                        let a = self.parse_expression()?;
8561                        self.expect(&Token::RParen)?;
8562                        Ok(Expr {
8563                            kind: ExprKind::Rand(Some(Box::new(a))),
8564                            line,
8565                        })
8566                    }
8567                } else {
8568                    let a = self.parse_one_arg()?;
8569                    Ok(Expr {
8570                        kind: ExprKind::Rand(Some(Box::new(a))),
8571                        line,
8572                    })
8573                }
8574            }
8575            "srand" => {
8576                if matches!(
8577                    self.peek(),
8578                    Token::Semicolon
8579                        | Token::RBrace
8580                        | Token::RParen
8581                        | Token::Eof
8582                        | Token::Comma
8583                        | Token::PipeForward
8584                ) {
8585                    Ok(Expr {
8586                        kind: ExprKind::Srand(None),
8587                        line,
8588                    })
8589                } else if matches!(self.peek(), Token::LParen) {
8590                    self.advance();
8591                    if matches!(self.peek(), Token::RParen) {
8592                        self.advance();
8593                        Ok(Expr {
8594                            kind: ExprKind::Srand(None),
8595                            line,
8596                        })
8597                    } else {
8598                        let a = self.parse_expression()?;
8599                        self.expect(&Token::RParen)?;
8600                        Ok(Expr {
8601                            kind: ExprKind::Srand(Some(Box::new(a))),
8602                            line,
8603                        })
8604                    }
8605                } else {
8606                    let a = self.parse_one_arg()?;
8607                    Ok(Expr {
8608                        kind: ExprKind::Srand(Some(Box::new(a))),
8609                        line,
8610                    })
8611                }
8612            }
8613            "hex" => {
8614                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8615                    return Ok(e);
8616                }
8617                let a = self.parse_one_arg_or_default()?;
8618                Ok(Expr {
8619                    kind: ExprKind::Hex(Box::new(a)),
8620                    line,
8621                })
8622            }
8623            "oct" => {
8624                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8625                    return Ok(e);
8626                }
8627                let a = self.parse_one_arg_or_default()?;
8628                Ok(Expr {
8629                    kind: ExprKind::Oct(Box::new(a)),
8630                    line,
8631                })
8632            }
8633            "chr" => {
8634                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8635                    return Ok(e);
8636                }
8637                let a = self.parse_one_arg_or_default()?;
8638                Ok(Expr {
8639                    kind: ExprKind::Chr(Box::new(a)),
8640                    line,
8641                })
8642            }
8643            "ord" => {
8644                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8645                    return Ok(e);
8646                }
8647                let a = self.parse_one_arg_or_default()?;
8648                Ok(Expr {
8649                    kind: ExprKind::Ord(Box::new(a)),
8650                    line,
8651                })
8652            }
8653            "lc" => {
8654                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8655                    return Ok(e);
8656                }
8657                let a = self.parse_one_arg_or_default()?;
8658                Ok(Expr {
8659                    kind: ExprKind::Lc(Box::new(a)),
8660                    line,
8661                })
8662            }
8663            "uc" => {
8664                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8665                    return Ok(e);
8666                }
8667                let a = self.parse_one_arg_or_default()?;
8668                Ok(Expr {
8669                    kind: ExprKind::Uc(Box::new(a)),
8670                    line,
8671                })
8672            }
8673            "lcfirst" => {
8674                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8675                    return Ok(e);
8676                }
8677                let a = self.parse_one_arg_or_default()?;
8678                Ok(Expr {
8679                    kind: ExprKind::Lcfirst(Box::new(a)),
8680                    line,
8681                })
8682            }
8683            "ucfirst" => {
8684                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8685                    return Ok(e);
8686                }
8687                let a = self.parse_one_arg_or_default()?;
8688                Ok(Expr {
8689                    kind: ExprKind::Ucfirst(Box::new(a)),
8690                    line,
8691                })
8692            }
8693            "fc" => {
8694                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8695                    return Ok(e);
8696                }
8697                let a = self.parse_one_arg_or_default()?;
8698                Ok(Expr {
8699                    kind: ExprKind::Fc(Box::new(a)),
8700                    line,
8701                })
8702            }
8703            "crypt" => {
8704                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8705                    return Ok(e);
8706                }
8707                let args = self.parse_builtin_args()?;
8708                if args.len() != 2 {
8709                    return Err(self.syntax_err("crypt requires two arguments", line));
8710                }
8711                Ok(Expr {
8712                    kind: ExprKind::Crypt {
8713                        plaintext: Box::new(args[0].clone()),
8714                        salt: Box::new(args[1].clone()),
8715                    },
8716                    line,
8717                })
8718            }
8719            "pos" => {
8720                if matches!(
8721                    self.peek(),
8722                    Token::Semicolon
8723                        | Token::RBrace
8724                        | Token::RParen
8725                        | Token::Eof
8726                        | Token::Comma
8727                        | Token::PipeForward
8728                ) {
8729                    Ok(Expr {
8730                        kind: ExprKind::Pos(None),
8731                        line,
8732                    })
8733                } else if matches!(self.peek(), Token::Assign) {
8734                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
8735                    self.advance();
8736                    let rhs = self.parse_assign_expr()?;
8737                    Ok(Expr {
8738                        kind: ExprKind::Assign {
8739                            target: Box::new(Expr {
8740                                kind: ExprKind::Pos(Some(Box::new(Expr {
8741                                    kind: ExprKind::ScalarVar("_".into()),
8742                                    line,
8743                                }))),
8744                                line,
8745                            }),
8746                            value: Box::new(rhs),
8747                        },
8748                        line,
8749                    })
8750                } else if matches!(self.peek(), Token::LParen) {
8751                    self.advance();
8752                    if matches!(self.peek(), Token::RParen) {
8753                        self.advance();
8754                        Ok(Expr {
8755                            kind: ExprKind::Pos(None),
8756                            line,
8757                        })
8758                    } else {
8759                        let a = self.parse_expression()?;
8760                        self.expect(&Token::RParen)?;
8761                        Ok(Expr {
8762                            kind: ExprKind::Pos(Some(Box::new(a))),
8763                            line,
8764                        })
8765                    }
8766                } else {
8767                    let saved = self.pos;
8768                    let subj = self.parse_unary()?;
8769                    if matches!(self.peek(), Token::Assign) {
8770                        self.advance();
8771                        let rhs = self.parse_assign_expr()?;
8772                        Ok(Expr {
8773                            kind: ExprKind::Assign {
8774                                target: Box::new(Expr {
8775                                    kind: ExprKind::Pos(Some(Box::new(subj))),
8776                                    line,
8777                                }),
8778                                value: Box::new(rhs),
8779                            },
8780                            line,
8781                        })
8782                    } else {
8783                        self.pos = saved;
8784                        let a = self.parse_one_arg()?;
8785                        Ok(Expr {
8786                            kind: ExprKind::Pos(Some(Box::new(a))),
8787                            line,
8788                        })
8789                    }
8790                }
8791            }
8792            "study" => {
8793                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8794                    return Ok(e);
8795                }
8796                let a = self.parse_one_arg_or_default()?;
8797                Ok(Expr {
8798                    kind: ExprKind::Study(Box::new(a)),
8799                    line,
8800                })
8801            }
8802            "push" => {
8803                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8804                    return Ok(e);
8805                }
8806                let args = self.parse_builtin_args()?;
8807                let (first, rest) = args
8808                    .split_first()
8809                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
8810                Ok(Expr {
8811                    kind: ExprKind::Push {
8812                        array: Box::new(first.clone()),
8813                        values: rest.to_vec(),
8814                    },
8815                    line,
8816                })
8817            }
8818            "pop" => {
8819                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8820                    return Ok(e);
8821                }
8822                let a = self.parse_one_arg_or_argv()?;
8823                Ok(Expr {
8824                    kind: ExprKind::Pop(Box::new(a)),
8825                    line,
8826                })
8827            }
8828            "shift" => {
8829                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8830                    return Ok(e);
8831                }
8832                let a = self.parse_one_arg_or_argv()?;
8833                Ok(Expr {
8834                    kind: ExprKind::Shift(Box::new(a)),
8835                    line,
8836                })
8837            }
8838            "unshift" => {
8839                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8840                    return Ok(e);
8841                }
8842                let args = self.parse_builtin_args()?;
8843                let (first, rest) = args
8844                    .split_first()
8845                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
8846                Ok(Expr {
8847                    kind: ExprKind::Unshift {
8848                        array: Box::new(first.clone()),
8849                        values: rest.to_vec(),
8850                    },
8851                    line,
8852                })
8853            }
8854            "splice" => {
8855                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8856                    return Ok(e);
8857                }
8858                let args = self.parse_builtin_args()?;
8859                let mut iter = args.into_iter();
8860                let array = Box::new(
8861                    iter.next()
8862                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
8863                );
8864                let offset = iter.next().map(Box::new);
8865                let length = iter.next().map(Box::new);
8866                let replacement: Vec<Expr> = iter.collect();
8867                Ok(Expr {
8868                    kind: ExprKind::Splice {
8869                        array,
8870                        offset,
8871                        length,
8872                        replacement,
8873                    },
8874                    line,
8875                })
8876            }
8877            "delete" => {
8878                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8879                    return Ok(e);
8880                }
8881                let a = self.parse_postfix()?;
8882                Ok(Expr {
8883                    kind: ExprKind::Delete(Box::new(a)),
8884                    line,
8885                })
8886            }
8887            "exists" => {
8888                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8889                    return Ok(e);
8890                }
8891                let a = self.parse_postfix()?;
8892                Ok(Expr {
8893                    kind: ExprKind::Exists(Box::new(a)),
8894                    line,
8895                })
8896            }
8897            "keys" => {
8898                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8899                    return Ok(e);
8900                }
8901                let a = self.parse_one_arg_or_default()?;
8902                Ok(Expr {
8903                    kind: ExprKind::Keys(Box::new(a)),
8904                    line,
8905                })
8906            }
8907            "values" => {
8908                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8909                    return Ok(e);
8910                }
8911                let a = self.parse_one_arg_or_default()?;
8912                Ok(Expr {
8913                    kind: ExprKind::Values(Box::new(a)),
8914                    line,
8915                })
8916            }
8917            "each" => {
8918                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8919                    return Ok(e);
8920                }
8921                let a = self.parse_one_arg_or_default()?;
8922                Ok(Expr {
8923                    kind: ExprKind::Each(Box::new(a)),
8924                    line,
8925                })
8926            }
8927            "fore" | "e" | "ep" => {
8928                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
8929                if matches!(self.peek(), Token::LBrace) {
8930                    let (block, list) = self.parse_block_list()?;
8931                    Ok(Expr {
8932                        kind: ExprKind::ForEachExpr {
8933                            block,
8934                            list: Box::new(list),
8935                        },
8936                        line,
8937                    })
8938                } else if self.in_pipe_rhs() {
8939                    // `|> ep` — bare ep at end of pipe: default to `say $_`
8940                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
8941                    let is_terminal = matches!(
8942                        self.peek(),
8943                        Token::Semicolon
8944                            | Token::RParen
8945                            | Token::Eof
8946                            | Token::PipeForward
8947                            | Token::RBrace
8948                    );
8949                    let block = if name == "ep" && is_terminal {
8950                        vec![Statement {
8951                            label: None,
8952                            kind: StmtKind::Expression(Expr {
8953                                kind: ExprKind::Say {
8954                                    handle: None,
8955                                    args: vec![Expr {
8956                                        kind: ExprKind::ScalarVar("_".into()),
8957                                        line,
8958                                    }],
8959                                },
8960                                line,
8961                            }),
8962                            line,
8963                        }]
8964                    } else {
8965                        let expr = self.parse_assign_expr_stop_at_pipe()?;
8966                        let expr = Self::lift_bareword_to_topic_call(expr);
8967                        vec![Statement {
8968                            label: None,
8969                            kind: StmtKind::Expression(expr),
8970                            line,
8971                        }]
8972                    };
8973                    let list = self.pipe_placeholder_list(line);
8974                    Ok(Expr {
8975                        kind: ExprKind::ForEachExpr {
8976                            block,
8977                            list: Box::new(list),
8978                        },
8979                        line,
8980                    })
8981                } else {
8982                    // `fore EXPR, LIST` — comma form
8983                    let expr = self.parse_assign_expr()?;
8984                    let expr = Self::lift_bareword_to_topic_call(expr);
8985                    self.expect(&Token::Comma)?;
8986                    let list_parts = self.parse_list_until_terminator()?;
8987                    let list_expr = if list_parts.len() == 1 {
8988                        list_parts.into_iter().next().unwrap()
8989                    } else {
8990                        Expr {
8991                            kind: ExprKind::List(list_parts),
8992                            line,
8993                        }
8994                    };
8995                    let block = vec![Statement {
8996                        label: None,
8997                        kind: StmtKind::Expression(expr),
8998                        line,
8999                    }];
9000                    Ok(Expr {
9001                        kind: ExprKind::ForEachExpr {
9002                            block,
9003                            list: Box::new(list_expr),
9004                        },
9005                        line,
9006                    })
9007                }
9008            }
9009            "rev" => {
9010                // `rev` — context-aware reverse: string in scalar, list in list context.
9011                // Defaults to $_ when no argument given.
9012                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
9013                // RBrace means we're inside a block like `map { rev }` - use $_ default.
9014                let a = if self.in_pipe_rhs()
9015                    && matches!(
9016                        self.peek(),
9017                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
9018                    ) {
9019                    self.pipe_placeholder_list(line)
9020                } else {
9021                    self.parse_one_arg_or_default()?
9022                };
9023                Ok(Expr {
9024                    kind: ExprKind::Rev(Box::new(a)),
9025                    line,
9026                })
9027            }
9028            "reverse" => {
9029                if !crate::compat_mode() {
9030                    return Err(self.syntax_err(
9031                        "stryke uses `rev` instead of `reverse` (this is not Perl 5)",
9032                        line,
9033                    ));
9034                }
9035                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9036                let a = if self.in_pipe_rhs()
9037                    && matches!(
9038                        self.peek(),
9039                        Token::Semicolon
9040                            | Token::RBrace
9041                            | Token::RParen
9042                            | Token::Eof
9043                            | Token::PipeForward
9044                    ) {
9045                    self.pipe_placeholder_list(line)
9046                } else {
9047                    self.parse_one_arg()?
9048                };
9049                Ok(Expr {
9050                    kind: ExprKind::ReverseExpr(Box::new(a)),
9051                    line,
9052                })
9053            }
9054            "reversed" | "rv" => {
9055                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9056                let a = if self.in_pipe_rhs()
9057                    && matches!(
9058                        self.peek(),
9059                        Token::Semicolon
9060                            | Token::RBrace
9061                            | Token::RParen
9062                            | Token::Eof
9063                            | Token::PipeForward
9064                    ) {
9065                    self.pipe_placeholder_list(line)
9066                } else {
9067                    self.parse_one_arg()?
9068                };
9069                Ok(Expr {
9070                    kind: ExprKind::Rev(Box::new(a)),
9071                    line,
9072                })
9073            }
9074            "join" => {
9075                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9076                    return Ok(e);
9077                }
9078                let args = self.parse_builtin_args()?;
9079                if args.is_empty() {
9080                    return Err(self.syntax_err("join requires separator and list", line));
9081                }
9082                // `@list |> join(",")` — list slot is filled by the piped LHS.
9083                if args.len() < 2 && !self.in_pipe_rhs() {
9084                    return Err(self.syntax_err("join requires separator and list", line));
9085                }
9086                Ok(Expr {
9087                    kind: ExprKind::JoinExpr {
9088                        separator: Box::new(args[0].clone()),
9089                        list: Box::new(Expr {
9090                            kind: ExprKind::List(args[1..].to_vec()),
9091                            line,
9092                        }),
9093                    },
9094                    line,
9095                })
9096            }
9097            "split" => {
9098                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9099                    return Ok(e);
9100                }
9101                let args = self.parse_builtin_args()?;
9102                let pattern = args.first().cloned().unwrap_or(Expr {
9103                    kind: ExprKind::String(" ".into()),
9104                    line,
9105                });
9106                let string = args.get(1).cloned().unwrap_or(Expr {
9107                    kind: ExprKind::ScalarVar("_".into()),
9108                    line,
9109                });
9110                let limit = args.get(2).cloned().map(Box::new);
9111                Ok(Expr {
9112                    kind: ExprKind::SplitExpr {
9113                        pattern: Box::new(pattern),
9114                        string: Box::new(string),
9115                        limit,
9116                    },
9117                    line,
9118                })
9119            }
9120            "substr" => {
9121                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9122                    return Ok(e);
9123                }
9124                let args = self.parse_builtin_args()?;
9125                Ok(Expr {
9126                    kind: ExprKind::Substr {
9127                        string: Box::new(args[0].clone()),
9128                        offset: Box::new(args[1].clone()),
9129                        length: args.get(2).cloned().map(Box::new),
9130                        replacement: args.get(3).cloned().map(Box::new),
9131                    },
9132                    line,
9133                })
9134            }
9135            "index" => {
9136                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9137                    return Ok(e);
9138                }
9139                let args = self.parse_builtin_args()?;
9140                Ok(Expr {
9141                    kind: ExprKind::Index {
9142                        string: Box::new(args[0].clone()),
9143                        substr: Box::new(args[1].clone()),
9144                        position: args.get(2).cloned().map(Box::new),
9145                    },
9146                    line,
9147                })
9148            }
9149            "rindex" => {
9150                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9151                    return Ok(e);
9152                }
9153                let args = self.parse_builtin_args()?;
9154                Ok(Expr {
9155                    kind: ExprKind::Rindex {
9156                        string: Box::new(args[0].clone()),
9157                        substr: Box::new(args[1].clone()),
9158                        position: args.get(2).cloned().map(Box::new),
9159                    },
9160                    line,
9161                })
9162            }
9163            "sprintf" => {
9164                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9165                    return Ok(e);
9166                }
9167                let args = self.parse_builtin_args()?;
9168                let (first, rest) = args
9169                    .split_first()
9170                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
9171                Ok(Expr {
9172                    kind: ExprKind::Sprintf {
9173                        format: Box::new(first.clone()),
9174                        args: rest.to_vec(),
9175                    },
9176                    line,
9177                })
9178            }
9179            "map" | "flat_map" | "maps" | "flat_maps" => {
9180                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
9181                let stream = matches!(name.as_str(), "maps" | "flat_maps");
9182                if matches!(self.peek(), Token::LBrace) {
9183                    let (block, list) = self.parse_block_list()?;
9184                    Ok(Expr {
9185                        kind: ExprKind::MapExpr {
9186                            block,
9187                            list: Box::new(list),
9188                            flatten_array_refs,
9189                            stream,
9190                        },
9191                        line,
9192                    })
9193                } else {
9194                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9195                    // Lift bareword to FuncCall($_) so `map sha512, @list`
9196                    // calls sha512($_) for each element instead of stringifying.
9197                    let expr = Self::lift_bareword_to_topic_call(expr);
9198                    let list_expr = if self.in_pipe_rhs()
9199                        && matches!(
9200                            self.peek(),
9201                            Token::Semicolon
9202                                | Token::RBrace
9203                                | Token::RParen
9204                                | Token::Eof
9205                                | Token::PipeForward
9206                        ) {
9207                        self.pipe_placeholder_list(line)
9208                    } else {
9209                        self.expect(&Token::Comma)?;
9210                        let list_parts = self.parse_list_until_terminator()?;
9211                        if list_parts.len() == 1 {
9212                            list_parts.into_iter().next().unwrap()
9213                        } else {
9214                            Expr {
9215                                kind: ExprKind::List(list_parts),
9216                                line,
9217                            }
9218                        }
9219                    };
9220                    Ok(Expr {
9221                        kind: ExprKind::MapExprComma {
9222                            expr: Box::new(expr),
9223                            list: Box::new(list_expr),
9224                            flatten_array_refs,
9225                            stream,
9226                        },
9227                        line,
9228                    })
9229                }
9230            }
9231            "cond" => {
9232                if crate::compat_mode() {
9233                    return Err(self
9234                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
9235                }
9236                self.parse_cond_expr(line)
9237            }
9238            "match" => {
9239                if crate::compat_mode() {
9240                    return Err(self.syntax_err(
9241                        "algebraic `match` is a stryke extension (disabled by --compat)",
9242                        line,
9243                    ));
9244                }
9245                self.parse_algebraic_match_expr(line)
9246            }
9247            "grep" | "greps" | "filter" | "fi" | "find_all" => {
9248                let keyword = match name.as_str() {
9249                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
9250                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
9251                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
9252                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
9253                    _ => unreachable!(),
9254                };
9255                if matches!(self.peek(), Token::LBrace) {
9256                    let (block, list) = self.parse_block_list()?;
9257                    Ok(Expr {
9258                        kind: ExprKind::GrepExpr {
9259                            block,
9260                            list: Box::new(list),
9261                            keyword,
9262                        },
9263                        line,
9264                    })
9265                } else {
9266                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9267                    if self.in_pipe_rhs()
9268                        && matches!(
9269                            self.peek(),
9270                            Token::Semicolon
9271                                | Token::RBrace
9272                                | Token::RParen
9273                                | Token::Eof
9274                                | Token::PipeForward
9275                        )
9276                    {
9277                        // Pipe-RHS blockless form: `|> grep EXPR`
9278                        // For literals, desugar to `$_ eq/== EXPR` so
9279                        // `|> filter 't'` keeps only elements equal to 't'.
9280                        // For regexes, desugar to `$_ =~ EXPR`.
9281                        let list = self.pipe_placeholder_list(line);
9282                        let topic = Expr {
9283                            kind: ExprKind::ScalarVar("_".into()),
9284                            line,
9285                        };
9286                        let test = match &expr.kind {
9287                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
9288                                kind: ExprKind::BinOp {
9289                                    op: BinOp::NumEq,
9290                                    left: Box::new(topic),
9291                                    right: Box::new(expr),
9292                                },
9293                                line,
9294                            },
9295                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
9296                                kind: ExprKind::BinOp {
9297                                    op: BinOp::StrEq,
9298                                    left: Box::new(topic),
9299                                    right: Box::new(expr),
9300                                },
9301                                line,
9302                            },
9303                            ExprKind::Regex { .. } => Expr {
9304                                kind: ExprKind::BinOp {
9305                                    op: BinOp::BindMatch,
9306                                    left: Box::new(topic),
9307                                    right: Box::new(expr),
9308                                },
9309                                line,
9310                            },
9311                            _ => {
9312                                // Non-literal (e.g. `defined`): lift bareword to call
9313                                Self::lift_bareword_to_topic_call(expr)
9314                            }
9315                        };
9316                        let block = vec![Statement {
9317                            label: None,
9318                            kind: StmtKind::Expression(test),
9319                            line,
9320                        }];
9321                        Ok(Expr {
9322                            kind: ExprKind::GrepExpr {
9323                                block,
9324                                list: Box::new(list),
9325                                keyword,
9326                            },
9327                            line,
9328                        })
9329                    } else {
9330                        let expr = Self::lift_bareword_to_topic_call(expr);
9331                        self.expect(&Token::Comma)?;
9332                        let list_parts = self.parse_list_until_terminator()?;
9333                        let list_expr = if list_parts.len() == 1 {
9334                            list_parts.into_iter().next().unwrap()
9335                        } else {
9336                            Expr {
9337                                kind: ExprKind::List(list_parts),
9338                                line,
9339                            }
9340                        };
9341                        Ok(Expr {
9342                            kind: ExprKind::GrepExprComma {
9343                                expr: Box::new(expr),
9344                                list: Box::new(list_expr),
9345                                keyword,
9346                            },
9347                            line,
9348                        })
9349                    }
9350                }
9351            }
9352            "sort" => {
9353                use crate::ast::SortComparator;
9354                if matches!(self.peek(), Token::LBrace) {
9355                    let block = self.parse_block()?;
9356                    let _ = self.eat(&Token::Comma);
9357                    let list = if self.in_pipe_rhs()
9358                        && matches!(
9359                            self.peek(),
9360                            Token::Semicolon
9361                                | Token::RBrace
9362                                | Token::RParen
9363                                | Token::Eof
9364                                | Token::PipeForward
9365                        ) {
9366                        self.pipe_placeholder_list(line)
9367                    } else {
9368                        self.parse_expression()?
9369                    };
9370                    Ok(Expr {
9371                        kind: ExprKind::SortExpr {
9372                            cmp: Some(SortComparator::Block(block)),
9373                            list: Box::new(list),
9374                        },
9375                        line,
9376                    })
9377                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
9378                    // Blockless comparator: `sort $a <=> $b, @list`
9379                    let block = self.parse_block_or_bareword_cmp_block()?;
9380                    let _ = self.eat(&Token::Comma);
9381                    let list = if self.in_pipe_rhs()
9382                        && matches!(
9383                            self.peek(),
9384                            Token::Semicolon
9385                                | Token::RBrace
9386                                | Token::RParen
9387                                | Token::Eof
9388                                | Token::PipeForward
9389                        ) {
9390                        self.pipe_placeholder_list(line)
9391                    } else {
9392                        self.parse_expression()?
9393                    };
9394                    Ok(Expr {
9395                        kind: ExprKind::SortExpr {
9396                            cmp: Some(SortComparator::Block(block)),
9397                            list: Box::new(list),
9398                        },
9399                        line,
9400                    })
9401                } else if matches!(self.peek(), Token::ScalarVar(_)) {
9402                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized
9403                    self.suppress_indirect_paren_call =
9404                        self.suppress_indirect_paren_call.saturating_add(1);
9405                    let code = self.parse_assign_expr()?;
9406                    self.suppress_indirect_paren_call =
9407                        self.suppress_indirect_paren_call.saturating_sub(1);
9408                    let list = if matches!(self.peek(), Token::LParen) {
9409                        self.advance();
9410                        let e = self.parse_expression()?;
9411                        self.expect(&Token::RParen)?;
9412                        e
9413                    } else {
9414                        self.parse_expression()?
9415                    };
9416                    Ok(Expr {
9417                        kind: ExprKind::SortExpr {
9418                            cmp: Some(SortComparator::Code(Box::new(code))),
9419                            list: Box::new(list),
9420                        },
9421                        line,
9422                    })
9423                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9424                {
9425                    // Blockless comparator via bare sub name: `sort my_cmp @list`
9426                    let block = self.parse_block_or_bareword_cmp_block()?;
9427                    let _ = self.eat(&Token::Comma);
9428                    let list = if self.in_pipe_rhs()
9429                        && matches!(
9430                            self.peek(),
9431                            Token::Semicolon
9432                                | Token::RBrace
9433                                | Token::RParen
9434                                | Token::Eof
9435                                | Token::PipeForward
9436                        ) {
9437                        self.pipe_placeholder_list(line)
9438                    } else {
9439                        self.parse_expression()?
9440                    };
9441                    Ok(Expr {
9442                        kind: ExprKind::SortExpr {
9443                            cmp: Some(SortComparator::Block(block)),
9444                            list: Box::new(list),
9445                        },
9446                        line,
9447                    })
9448                } else {
9449                    // Bare `sort` with no comparator and no list: only allowed
9450                    // as the RHS of `|>`, where the list comes from the LHS.
9451                    let list = if self.in_pipe_rhs()
9452                        && matches!(
9453                            self.peek(),
9454                            Token::Semicolon
9455                                | Token::RBrace
9456                                | Token::RParen
9457                                | Token::Eof
9458                                | Token::PipeForward
9459                        ) {
9460                        self.pipe_placeholder_list(line)
9461                    } else {
9462                        self.parse_expression()?
9463                    };
9464                    Ok(Expr {
9465                        kind: ExprKind::SortExpr {
9466                            cmp: None,
9467                            list: Box::new(list),
9468                        },
9469                        line,
9470                    })
9471                }
9472            }
9473            "reduce" | "fold" | "inject" => {
9474                let (block, list) = self.parse_block_list()?;
9475                Ok(Expr {
9476                    kind: ExprKind::ReduceExpr {
9477                        block,
9478                        list: Box::new(list),
9479                    },
9480                    line,
9481                })
9482            }
9483            // Parallel extensions
9484            "pmap" => {
9485                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9486                Ok(Expr {
9487                    kind: ExprKind::PMapExpr {
9488                        block,
9489                        list: Box::new(list),
9490                        progress: progress.map(Box::new),
9491                        flat_outputs: false,
9492                        on_cluster: None,
9493                        stream: false,
9494                    },
9495                    line,
9496                })
9497            }
9498            "pmap_on" => {
9499                let (cluster, block, list, progress) =
9500                    self.parse_cluster_block_then_list_optional_progress()?;
9501                Ok(Expr {
9502                    kind: ExprKind::PMapExpr {
9503                        block,
9504                        list: Box::new(list),
9505                        progress: progress.map(Box::new),
9506                        flat_outputs: false,
9507                        on_cluster: Some(Box::new(cluster)),
9508                        stream: false,
9509                    },
9510                    line,
9511                })
9512            }
9513            "pflat_map" => {
9514                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9515                Ok(Expr {
9516                    kind: ExprKind::PMapExpr {
9517                        block,
9518                        list: Box::new(list),
9519                        progress: progress.map(Box::new),
9520                        flat_outputs: true,
9521                        on_cluster: None,
9522                        stream: false,
9523                    },
9524                    line,
9525                })
9526            }
9527            "pflat_map_on" => {
9528                let (cluster, block, list, progress) =
9529                    self.parse_cluster_block_then_list_optional_progress()?;
9530                Ok(Expr {
9531                    kind: ExprKind::PMapExpr {
9532                        block,
9533                        list: Box::new(list),
9534                        progress: progress.map(Box::new),
9535                        flat_outputs: true,
9536                        on_cluster: Some(Box::new(cluster)),
9537                        stream: false,
9538                    },
9539                    line,
9540                })
9541            }
9542            "pmaps" => {
9543                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9544                Ok(Expr {
9545                    kind: ExprKind::PMapExpr {
9546                        block,
9547                        list: Box::new(list),
9548                        progress: progress.map(Box::new),
9549                        flat_outputs: false,
9550                        on_cluster: None,
9551                        stream: true,
9552                    },
9553                    line,
9554                })
9555            }
9556            "pflat_maps" => {
9557                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9558                Ok(Expr {
9559                    kind: ExprKind::PMapExpr {
9560                        block,
9561                        list: Box::new(list),
9562                        progress: progress.map(Box::new),
9563                        flat_outputs: true,
9564                        on_cluster: None,
9565                        stream: true,
9566                    },
9567                    line,
9568                })
9569            }
9570            "pgreps" => {
9571                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9572                Ok(Expr {
9573                    kind: ExprKind::PGrepExpr {
9574                        block,
9575                        list: Box::new(list),
9576                        progress: progress.map(Box::new),
9577                        stream: true,
9578                    },
9579                    line,
9580                })
9581            }
9582            "pmap_chunked" => {
9583                let chunk_size = self.parse_assign_expr()?;
9584                let block = self.parse_block_or_bareword_block()?;
9585                self.eat(&Token::Comma);
9586                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9587                Ok(Expr {
9588                    kind: ExprKind::PMapChunkedExpr {
9589                        chunk_size: Box::new(chunk_size),
9590                        block,
9591                        list: Box::new(list),
9592                        progress: progress.map(Box::new),
9593                    },
9594                    line,
9595                })
9596            }
9597            "pgrep" => {
9598                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9599                Ok(Expr {
9600                    kind: ExprKind::PGrepExpr {
9601                        block,
9602                        list: Box::new(list),
9603                        progress: progress.map(Box::new),
9604                        stream: false,
9605                    },
9606                    line,
9607                })
9608            }
9609            "pfor" => {
9610                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9611                Ok(Expr {
9612                    kind: ExprKind::PForExpr {
9613                        block,
9614                        list: Box::new(list),
9615                        progress: progress.map(Box::new),
9616                    },
9617                    line,
9618                })
9619            }
9620            "par_lines" | "par_walk" => {
9621                let args = self.parse_builtin_args()?;
9622                if args.len() < 2 {
9623                    return Err(
9624                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9625                    );
9626                }
9627
9628                if name == "par_lines" {
9629                    Ok(Expr {
9630                        kind: ExprKind::ParLinesExpr {
9631                            path: Box::new(args[0].clone()),
9632                            callback: Box::new(args[1].clone()),
9633                            progress: None,
9634                        },
9635                        line,
9636                    })
9637                } else {
9638                    Ok(Expr {
9639                        kind: ExprKind::ParWalkExpr {
9640                            path: Box::new(args[0].clone()),
9641                            callback: Box::new(args[1].clone()),
9642                            progress: None,
9643                        },
9644                        line,
9645                    })
9646                }
9647            }
9648            "pwatch" | "watch" => {
9649                let args = self.parse_builtin_args()?;
9650                if args.len() < 2 {
9651                    return Err(
9652                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9653                    );
9654                }
9655                Ok(Expr {
9656                    kind: ExprKind::PwatchExpr {
9657                        path: Box::new(args[0].clone()),
9658                        callback: Box::new(args[1].clone()),
9659                    },
9660                    line,
9661                })
9662            }
9663            "fan" => {
9664                // fan { BLOCK }            — no count, block body
9665                // fan COUNT { BLOCK }      — count + block body
9666                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
9667                // fan COUNT EXPR;          — count + blockless body
9668                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
9669                let (count, block) = self.parse_fan_count_and_block(line)?;
9670                let progress = self.parse_fan_optional_progress("fan")?;
9671                Ok(Expr {
9672                    kind: ExprKind::FanExpr {
9673                        count,
9674                        block,
9675                        progress,
9676                        capture: false,
9677                    },
9678                    line,
9679                })
9680            }
9681            "fan_cap" => {
9682                let (count, block) = self.parse_fan_count_and_block(line)?;
9683                let progress = self.parse_fan_optional_progress("fan_cap")?;
9684                Ok(Expr {
9685                    kind: ExprKind::FanExpr {
9686                        count,
9687                        block,
9688                        progress,
9689                        capture: true,
9690                    },
9691                    line,
9692                })
9693            }
9694            "async" => {
9695                if !matches!(self.peek(), Token::LBrace) {
9696                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
9697                }
9698                let block = self.parse_block()?;
9699                Ok(Expr {
9700                    kind: ExprKind::AsyncBlock { body: block },
9701                    line,
9702                })
9703            }
9704            "spawn" => {
9705                if !matches!(self.peek(), Token::LBrace) {
9706                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
9707                }
9708                let block = self.parse_block()?;
9709                Ok(Expr {
9710                    kind: ExprKind::SpawnBlock { body: block },
9711                    line,
9712                })
9713            }
9714            "trace" => {
9715                if !matches!(self.peek(), Token::LBrace) {
9716                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
9717                }
9718                let block = self.parse_block()?;
9719                Ok(Expr {
9720                    kind: ExprKind::Trace { body: block },
9721                    line,
9722                })
9723            }
9724            "timer" => {
9725                let block = self.parse_block_or_bareword_block_no_args()?;
9726                Ok(Expr {
9727                    kind: ExprKind::Timer { body: block },
9728                    line,
9729                })
9730            }
9731            "bench" => {
9732                let block = self.parse_block_or_bareword_block_no_args()?;
9733                let times = Box::new(self.parse_expression()?);
9734                Ok(Expr {
9735                    kind: ExprKind::Bench { body: block, times },
9736                    line,
9737                })
9738            }
9739            "spinner" => {
9740                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
9741                let (message, body) = if matches!(self.peek(), Token::LBrace) {
9742                    let body = self.parse_block()?;
9743                    (
9744                        Box::new(Expr {
9745                            kind: ExprKind::String("working".to_string()),
9746                            line,
9747                        }),
9748                        body,
9749                    )
9750                } else {
9751                    let msg = self.parse_assign_expr()?;
9752                    let body = self.parse_block()?;
9753                    (Box::new(msg), body)
9754                };
9755                Ok(Expr {
9756                    kind: ExprKind::Spinner { message, body },
9757                    line,
9758                })
9759            }
9760            "thread" | "t" => {
9761                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
9762                // `t` is a short alias for `thread`
9763                // Each stage is either:
9764                //   - `ident` — bare function call
9765                //   - `ident { block }` — function with block arg
9766                //   - `ident arg1 arg2 { block }` — function with args and optional block
9767                //   - `fn { block }` — standalone anonymous block
9768                //   - `>{ block }` — shorthand for standalone anonymous block
9769                // Desugars to: EXPR |> stage1 |> stage2 |> ...
9770                self.parse_thread_macro(line, false)
9771            }
9772            "retry" => {
9773                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
9774                // An optional comma before `times` is allowed in both forms.
9775                let body = if matches!(self.peek(), Token::LBrace) {
9776                    self.parse_block()?
9777                } else {
9778                    let bw_line = self.peek_line();
9779                    let Token::Ident(ref name) = self.peek().clone() else {
9780                        return Err(self
9781                            .syntax_err("retry: expected block or bareword function name", line));
9782                    };
9783                    let name = name.clone();
9784                    self.advance();
9785                    vec![Statement::new(
9786                        StmtKind::Expression(Expr {
9787                            kind: ExprKind::FuncCall { name, args: vec![] },
9788                            line: bw_line,
9789                        }),
9790                        bw_line,
9791                    )]
9792                };
9793                self.eat(&Token::Comma);
9794                match self.peek() {
9795                    Token::Ident(ref s) if s == "times" => {
9796                        self.advance();
9797                    }
9798                    _ => {
9799                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
9800                    }
9801                }
9802                self.expect(&Token::FatArrow)?;
9803                let times = Box::new(self.parse_assign_expr()?);
9804                let mut backoff = RetryBackoff::None;
9805                if self.eat(&Token::Comma) {
9806                    match self.peek() {
9807                        Token::Ident(ref s) if s == "backoff" => {
9808                            self.advance();
9809                        }
9810                        _ => {
9811                            return Err(
9812                                self.syntax_err("retry: expected `backoff =>` after comma", line)
9813                            );
9814                        }
9815                    }
9816                    self.expect(&Token::FatArrow)?;
9817                    let Token::Ident(mode) = self.peek().clone() else {
9818                        return Err(self.syntax_err(
9819                            "retry: expected backoff mode (none, linear, exponential)",
9820                            line,
9821                        ));
9822                    };
9823                    backoff = match mode.as_str() {
9824                        "none" => RetryBackoff::None,
9825                        "linear" => RetryBackoff::Linear,
9826                        "exponential" => RetryBackoff::Exponential,
9827                        _ => {
9828                            return Err(
9829                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
9830                            );
9831                        }
9832                    };
9833                    self.advance();
9834                }
9835                Ok(Expr {
9836                    kind: ExprKind::RetryBlock {
9837                        body,
9838                        times,
9839                        backoff,
9840                    },
9841                    line,
9842                })
9843            }
9844            "rate_limit" => {
9845                self.expect(&Token::LParen)?;
9846                let max = Box::new(self.parse_assign_expr()?);
9847                self.expect(&Token::Comma)?;
9848                let window = Box::new(self.parse_assign_expr()?);
9849                self.expect(&Token::RParen)?;
9850                let body = self.parse_block_or_bareword_block_no_args()?;
9851                let slot = self.alloc_rate_limit_slot();
9852                Ok(Expr {
9853                    kind: ExprKind::RateLimitBlock {
9854                        slot,
9855                        max,
9856                        window,
9857                        body,
9858                    },
9859                    line,
9860                })
9861            }
9862            "every" => {
9863                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
9864                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
9865                let has_paren = self.eat(&Token::LParen);
9866                let interval = Box::new(self.parse_assign_expr()?);
9867                if has_paren {
9868                    self.expect(&Token::RParen)?;
9869                }
9870                let body = if matches!(self.peek(), Token::LBrace) {
9871                    self.parse_block()?
9872                } else {
9873                    let bline = self.peek_line();
9874                    let expr = self.parse_assign_expr()?;
9875                    vec![Statement::new(StmtKind::Expression(expr), bline)]
9876                };
9877                Ok(Expr {
9878                    kind: ExprKind::EveryBlock { interval, body },
9879                    line,
9880                })
9881            }
9882            "gen" => {
9883                if !matches!(self.peek(), Token::LBrace) {
9884                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
9885                }
9886                let body = self.parse_block()?;
9887                Ok(Expr {
9888                    kind: ExprKind::GenBlock { body },
9889                    line,
9890                })
9891            }
9892            "yield" => {
9893                let e = self.parse_assign_expr()?;
9894                Ok(Expr {
9895                    kind: ExprKind::Yield(Box::new(e)),
9896                    line,
9897                })
9898            }
9899            "await" => {
9900                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9901                    return Ok(e);
9902                }
9903                // `await` defaults to `$_` so `map { await } @tasks` works
9904                // (Perl-style topic-defaulting unary).
9905                let a = self.parse_one_arg_or_default()?;
9906                Ok(Expr {
9907                    kind: ExprKind::Await(Box::new(a)),
9908                    line,
9909                })
9910            }
9911            "slurp" | "cat" | "c" => {
9912                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9913                    return Ok(e);
9914                }
9915                let a = self.parse_one_arg_or_default()?;
9916                Ok(Expr {
9917                    kind: ExprKind::Slurp(Box::new(a)),
9918                    line,
9919                })
9920            }
9921            "capture" => {
9922                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9923                    return Ok(e);
9924                }
9925                let a = self.parse_one_arg()?;
9926                Ok(Expr {
9927                    kind: ExprKind::Capture(Box::new(a)),
9928                    line,
9929                })
9930            }
9931            "fetch_url" => {
9932                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9933                    return Ok(e);
9934                }
9935                let a = self.parse_one_arg()?;
9936                Ok(Expr {
9937                    kind: ExprKind::FetchUrl(Box::new(a)),
9938                    line,
9939                })
9940            }
9941            "pchannel" => {
9942                let capacity = if self.eat(&Token::LParen) {
9943                    if matches!(self.peek(), Token::RParen) {
9944                        self.advance();
9945                        None
9946                    } else {
9947                        let e = self.parse_expression()?;
9948                        self.expect(&Token::RParen)?;
9949                        Some(Box::new(e))
9950                    }
9951                } else {
9952                    None
9953                };
9954                Ok(Expr {
9955                    kind: ExprKind::Pchannel { capacity },
9956                    line,
9957                })
9958            }
9959            "psort" => {
9960                if matches!(self.peek(), Token::LBrace)
9961                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
9962                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9963                {
9964                    let block = self.parse_block_or_bareword_cmp_block()?;
9965                    self.eat(&Token::Comma);
9966                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9967                    Ok(Expr {
9968                        kind: ExprKind::PSortExpr {
9969                            cmp: Some(block),
9970                            list: Box::new(list),
9971                            progress: progress.map(Box::new),
9972                        },
9973                        line,
9974                    })
9975                } else {
9976                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9977                    Ok(Expr {
9978                        kind: ExprKind::PSortExpr {
9979                            cmp: None,
9980                            list: Box::new(list),
9981                            progress: progress.map(Box::new),
9982                        },
9983                        line,
9984                    })
9985                }
9986            }
9987            "preduce" => {
9988                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9989                Ok(Expr {
9990                    kind: ExprKind::PReduceExpr {
9991                        block,
9992                        list: Box::new(list),
9993                        progress: progress.map(Box::new),
9994                    },
9995                    line,
9996                })
9997            }
9998            "preduce_init" => {
9999                let (init, block, list, progress) =
10000                    self.parse_init_block_then_list_optional_progress()?;
10001                Ok(Expr {
10002                    kind: ExprKind::PReduceInitExpr {
10003                        init: Box::new(init),
10004                        block,
10005                        list: Box::new(list),
10006                        progress: progress.map(Box::new),
10007                    },
10008                    line,
10009                })
10010            }
10011            "pmap_reduce" => {
10012                let map_block = self.parse_block_or_bareword_block()?;
10013                // After the map block, expect either a `{ REDUCE }` block, or
10014                // after an eaten comma, a blockless reduce expr (`$a + $b`).
10015                let reduce_block = if matches!(self.peek(), Token::LBrace) {
10016                    self.parse_block()?
10017                } else {
10018                    // comma separates blockless map from blockless reduce
10019                    self.expect(&Token::Comma)?;
10020                    self.parse_block_or_bareword_cmp_block()?
10021                };
10022                self.eat(&Token::Comma);
10023                let line = self.peek_line();
10024                if let Token::Ident(ref kw) = self.peek().clone() {
10025                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10026                        self.advance();
10027                        self.expect(&Token::FatArrow)?;
10028                        let prog = self.parse_assign_expr()?;
10029                        return Ok(Expr {
10030                            kind: ExprKind::PMapReduceExpr {
10031                                map_block,
10032                                reduce_block,
10033                                list: Box::new(Expr {
10034                                    kind: ExprKind::List(vec![]),
10035                                    line,
10036                                }),
10037                                progress: Some(Box::new(prog)),
10038                            },
10039                            line,
10040                        });
10041                    }
10042                }
10043                if matches!(
10044                    self.peek(),
10045                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10046                ) {
10047                    return Ok(Expr {
10048                        kind: ExprKind::PMapReduceExpr {
10049                            map_block,
10050                            reduce_block,
10051                            list: Box::new(Expr {
10052                                kind: ExprKind::List(vec![]),
10053                                line,
10054                            }),
10055                            progress: None,
10056                        },
10057                        line,
10058                    });
10059                }
10060                let mut parts = vec![self.parse_assign_expr()?];
10061                loop {
10062                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10063                        break;
10064                    }
10065                    if matches!(
10066                        self.peek(),
10067                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10068                    ) {
10069                        break;
10070                    }
10071                    if let Token::Ident(ref kw) = self.peek().clone() {
10072                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10073                            self.advance();
10074                            self.expect(&Token::FatArrow)?;
10075                            let prog = self.parse_assign_expr()?;
10076                            return Ok(Expr {
10077                                kind: ExprKind::PMapReduceExpr {
10078                                    map_block,
10079                                    reduce_block,
10080                                    list: Box::new(merge_expr_list(parts)),
10081                                    progress: Some(Box::new(prog)),
10082                                },
10083                                line,
10084                            });
10085                        }
10086                    }
10087                    parts.push(self.parse_assign_expr()?);
10088                }
10089                Ok(Expr {
10090                    kind: ExprKind::PMapReduceExpr {
10091                        map_block,
10092                        reduce_block,
10093                        list: Box::new(merge_expr_list(parts)),
10094                        progress: None,
10095                    },
10096                    line,
10097                })
10098            }
10099            "puniq" => {
10100                if self.pipe_supplies_slurped_list_operand() {
10101                    return Ok(Expr {
10102                        kind: ExprKind::FuncCall {
10103                            name: "puniq".to_string(),
10104                            args: vec![],
10105                        },
10106                        line,
10107                    });
10108                }
10109                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10110                let mut args = vec![list];
10111                if let Some(p) = progress {
10112                    args.push(p);
10113                }
10114                Ok(Expr {
10115                    kind: ExprKind::FuncCall {
10116                        name: "puniq".to_string(),
10117                        args,
10118                    },
10119                    line,
10120                })
10121            }
10122            "pfirst" => {
10123                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10124                let cr = Expr {
10125                    kind: ExprKind::CodeRef {
10126                        params: vec![],
10127                        body: block,
10128                    },
10129                    line,
10130                };
10131                let mut args = vec![cr, list];
10132                if let Some(p) = progress {
10133                    args.push(p);
10134                }
10135                Ok(Expr {
10136                    kind: ExprKind::FuncCall {
10137                        name: "pfirst".to_string(),
10138                        args,
10139                    },
10140                    line,
10141                })
10142            }
10143            "pany" => {
10144                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10145                let cr = Expr {
10146                    kind: ExprKind::CodeRef {
10147                        params: vec![],
10148                        body: block,
10149                    },
10150                    line,
10151                };
10152                let mut args = vec![cr, list];
10153                if let Some(p) = progress {
10154                    args.push(p);
10155                }
10156                Ok(Expr {
10157                    kind: ExprKind::FuncCall {
10158                        name: "pany".to_string(),
10159                        args,
10160                    },
10161                    line,
10162                })
10163            }
10164            "uniq" | "distinct" => {
10165                if self.pipe_supplies_slurped_list_operand() {
10166                    return Ok(Expr {
10167                        kind: ExprKind::FuncCall {
10168                            name: name.clone(),
10169                            args: vec![],
10170                        },
10171                        line,
10172                    });
10173                }
10174                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10175                if progress.is_some() {
10176                    return Err(self.syntax_err(
10177                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
10178                        line,
10179                    ));
10180                }
10181                Ok(Expr {
10182                    kind: ExprKind::FuncCall {
10183                        name: name.clone(),
10184                        args: vec![list],
10185                    },
10186                    line,
10187                })
10188            }
10189            "flatten" => {
10190                if self.pipe_supplies_slurped_list_operand() {
10191                    return Ok(Expr {
10192                        kind: ExprKind::FuncCall {
10193                            name: "flatten".to_string(),
10194                            args: vec![],
10195                        },
10196                        line,
10197                    });
10198                }
10199                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10200                if progress.is_some() {
10201                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
10202                }
10203                Ok(Expr {
10204                    kind: ExprKind::FuncCall {
10205                        name: "flatten".to_string(),
10206                        args: vec![list],
10207                    },
10208                    line,
10209                })
10210            }
10211            "set" => {
10212                if self.pipe_supplies_slurped_list_operand() {
10213                    return Ok(Expr {
10214                        kind: ExprKind::FuncCall {
10215                            name: "set".to_string(),
10216                            args: vec![],
10217                        },
10218                        line,
10219                    });
10220                }
10221                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10222                if progress.is_some() {
10223                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
10224                }
10225                Ok(Expr {
10226                    kind: ExprKind::FuncCall {
10227                        name: "set".to_string(),
10228                        args: vec![list],
10229                    },
10230                    line,
10231                })
10232            }
10233            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
10234            // Defaults to `$_` when no arg is given, like `length`. See
10235            // `builtin_file_size` in builtins.rs for the runtime behavior.
10236            "size" => {
10237                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10238                    return Ok(e);
10239                }
10240                if self.pipe_supplies_slurped_list_operand() {
10241                    return Ok(Expr {
10242                        kind: ExprKind::FuncCall {
10243                            name: "size".to_string(),
10244                            args: vec![],
10245                        },
10246                        line,
10247                    });
10248                }
10249                let a = self.parse_one_arg_or_default()?;
10250                Ok(Expr {
10251                    kind: ExprKind::FuncCall {
10252                        name: "size".to_string(),
10253                        args: vec![a],
10254                    },
10255                    line,
10256                })
10257            }
10258            "list_count" | "list_size" | "count" | "len" | "cnt" => {
10259                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10260                    return Ok(e);
10261                }
10262                if self.pipe_supplies_slurped_list_operand() {
10263                    return Ok(Expr {
10264                        kind: ExprKind::FuncCall {
10265                            name: name.clone(),
10266                            args: vec![],
10267                        },
10268                        line,
10269                    });
10270                }
10271                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10272                if progress.is_some() {
10273                    return Err(self.syntax_err(
10274                        "`progress =>` is not supported for list_count / list_size / count / cnt",
10275                        line,
10276                    ));
10277                }
10278                Ok(Expr {
10279                    kind: ExprKind::FuncCall {
10280                        name: name.clone(),
10281                        args: vec![list],
10282                    },
10283                    line,
10284                })
10285            }
10286            "shuffle" | "shuffled" => {
10287                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10288                    return Ok(e);
10289                }
10290                if self.pipe_supplies_slurped_list_operand() {
10291                    return Ok(Expr {
10292                        kind: ExprKind::FuncCall {
10293                            name: "shuffle".to_string(),
10294                            args: vec![],
10295                        },
10296                        line,
10297                    });
10298                }
10299                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10300                if progress.is_some() {
10301                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
10302                }
10303                Ok(Expr {
10304                    kind: ExprKind::FuncCall {
10305                        name: "shuffle".to_string(),
10306                        args: vec![list],
10307                    },
10308                    line,
10309                })
10310            }
10311            "chunked" => {
10312                let mut parts = Vec::new();
10313                if self.eat(&Token::LParen) {
10314                    if !matches!(self.peek(), Token::RParen) {
10315                        parts.push(self.parse_assign_expr()?);
10316                        while self.eat(&Token::Comma) {
10317                            if matches!(self.peek(), Token::RParen) {
10318                                break;
10319                            }
10320                            parts.push(self.parse_assign_expr()?);
10321                        }
10322                    }
10323                    self.expect(&Token::RParen)?;
10324                } else {
10325                    // Paren-less `chunked N`: `|>` is a hard terminator, not
10326                    // an operator inside the arg (see
10327                    // `parse_assign_expr_stop_at_pipe`).
10328                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
10329                    loop {
10330                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10331                            break;
10332                        }
10333                        if matches!(
10334                            self.peek(),
10335                            Token::Semicolon
10336                                | Token::RBrace
10337                                | Token::RParen
10338                                | Token::Eof
10339                                | Token::PipeForward
10340                        ) {
10341                            break;
10342                        }
10343                        if self.peek_is_postfix_stmt_modifier_keyword() {
10344                            break;
10345                        }
10346                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
10347                    }
10348                }
10349                if parts.len() == 1 {
10350                    let n = parts.pop().unwrap();
10351                    return Ok(Expr {
10352                        kind: ExprKind::FuncCall {
10353                            name: "chunked".to_string(),
10354                            args: vec![n],
10355                        },
10356                        line,
10357                    });
10358                }
10359                if parts.is_empty() {
10360                    return Ok(Expr {
10361                        kind: ExprKind::FuncCall {
10362                            name: "chunked".to_string(),
10363                            args: parts,
10364                        },
10365                        line,
10366                    });
10367                }
10368                if parts.len() == 2 {
10369                    let n = parts.pop().unwrap();
10370                    let list = parts.pop().unwrap();
10371                    return Ok(Expr {
10372                        kind: ExprKind::FuncCall {
10373                            name: "chunked".to_string(),
10374                            args: vec![list, n],
10375                        },
10376                        line,
10377                    });
10378                }
10379                Err(self.syntax_err(
10380                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
10381                    line,
10382                ))
10383            }
10384            "windowed" => {
10385                let mut parts = Vec::new();
10386                if self.eat(&Token::LParen) {
10387                    if !matches!(self.peek(), Token::RParen) {
10388                        parts.push(self.parse_assign_expr()?);
10389                        while self.eat(&Token::Comma) {
10390                            if matches!(self.peek(), Token::RParen) {
10391                                break;
10392                            }
10393                            parts.push(self.parse_assign_expr()?);
10394                        }
10395                    }
10396                    self.expect(&Token::RParen)?;
10397                } else {
10398                    // Paren-less `windowed N`: same `|>`-terminator rule as
10399                    // `chunked` above.
10400                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
10401                    loop {
10402                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10403                            break;
10404                        }
10405                        if matches!(
10406                            self.peek(),
10407                            Token::Semicolon
10408                                | Token::RBrace
10409                                | Token::RParen
10410                                | Token::Eof
10411                                | Token::PipeForward
10412                        ) {
10413                            break;
10414                        }
10415                        if self.peek_is_postfix_stmt_modifier_keyword() {
10416                            break;
10417                        }
10418                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
10419                    }
10420                }
10421                if parts.len() == 1 {
10422                    let n = parts.pop().unwrap();
10423                    return Ok(Expr {
10424                        kind: ExprKind::FuncCall {
10425                            name: "windowed".to_string(),
10426                            args: vec![n],
10427                        },
10428                        line,
10429                    });
10430                }
10431                if parts.is_empty() {
10432                    return Ok(Expr {
10433                        kind: ExprKind::FuncCall {
10434                            name: "windowed".to_string(),
10435                            args: parts,
10436                        },
10437                        line,
10438                    });
10439                }
10440                if parts.len() == 2 {
10441                    let n = parts.pop().unwrap();
10442                    let list = parts.pop().unwrap();
10443                    return Ok(Expr {
10444                        kind: ExprKind::FuncCall {
10445                            name: "windowed".to_string(),
10446                            args: vec![list, n],
10447                        },
10448                        line,
10449                    });
10450                }
10451                Err(self.syntax_err(
10452                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
10453                    line,
10454                ))
10455            }
10456            "any" | "all" | "none" => {
10457                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10458                if progress.is_some() {
10459                    return Err(self.syntax_err(
10460                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
10461                        line,
10462                    ));
10463                }
10464                let cr = Expr {
10465                    kind: ExprKind::CodeRef {
10466                        params: vec![],
10467                        body: block,
10468                    },
10469                    line,
10470                };
10471                Ok(Expr {
10472                    kind: ExprKind::FuncCall {
10473                        name: name.clone(),
10474                        args: vec![cr, list],
10475                    },
10476                    line,
10477                })
10478            }
10479            // Ruby `detect` / `find` — same as `List::Util::first` (first element matching block).
10480            "first" | "detect" | "find" => {
10481                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10482                if progress.is_some() {
10483                    return Err(self.syntax_err(
10484                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
10485                        line,
10486                    ));
10487                }
10488                let cr = Expr {
10489                    kind: ExprKind::CodeRef {
10490                        params: vec![],
10491                        body: block,
10492                    },
10493                    line,
10494                };
10495                Ok(Expr {
10496                    kind: ExprKind::FuncCall {
10497                        name: "first".to_string(),
10498                        args: vec![cr, list],
10499                    },
10500                    line,
10501                })
10502            }
10503            "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
10504            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
10505                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10506                if progress.is_some() {
10507                    return Err(
10508                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
10509                    );
10510                }
10511                let cr = Expr {
10512                    kind: ExprKind::CodeRef {
10513                        params: vec![],
10514                        body: block,
10515                    },
10516                    line,
10517                };
10518                Ok(Expr {
10519                    kind: ExprKind::FuncCall {
10520                        name: name.to_string(),
10521                        args: vec![cr, list],
10522                    },
10523                    line,
10524                })
10525            }
10526            "group_by" | "chunk_by" => {
10527                if matches!(self.peek(), Token::LBrace) {
10528                    let (block, list) = self.parse_block_list()?;
10529                    let cr = Expr {
10530                        kind: ExprKind::CodeRef {
10531                            params: vec![],
10532                            body: block,
10533                        },
10534                        line,
10535                    };
10536                    Ok(Expr {
10537                        kind: ExprKind::FuncCall {
10538                            name: name.to_string(),
10539                            args: vec![cr, list],
10540                        },
10541                        line,
10542                    })
10543                } else {
10544                    let key_expr = self.parse_assign_expr()?;
10545                    self.expect(&Token::Comma)?;
10546                    let list_parts = self.parse_list_until_terminator()?;
10547                    let list_expr = if list_parts.len() == 1 {
10548                        list_parts.into_iter().next().unwrap()
10549                    } else {
10550                        Expr {
10551                            kind: ExprKind::List(list_parts),
10552                            line,
10553                        }
10554                    };
10555                    Ok(Expr {
10556                        kind: ExprKind::FuncCall {
10557                            name: name.to_string(),
10558                            args: vec![key_expr, list_expr],
10559                        },
10560                        line,
10561                    })
10562                }
10563            }
10564            "with_index" => {
10565                if self.pipe_supplies_slurped_list_operand() {
10566                    return Ok(Expr {
10567                        kind: ExprKind::FuncCall {
10568                            name: "with_index".to_string(),
10569                            args: vec![],
10570                        },
10571                        line,
10572                    });
10573                }
10574                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10575                if progress.is_some() {
10576                    return Err(
10577                        self.syntax_err("`progress =>` is not supported for with_index", line)
10578                    );
10579                }
10580                Ok(Expr {
10581                    kind: ExprKind::FuncCall {
10582                        name: "with_index".to_string(),
10583                        args: vec![list],
10584                    },
10585                    line,
10586                })
10587            }
10588            "pcache" => {
10589                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10590                Ok(Expr {
10591                    kind: ExprKind::PcacheExpr {
10592                        block,
10593                        list: Box::new(list),
10594                        progress: progress.map(Box::new),
10595                    },
10596                    line,
10597                })
10598            }
10599            "pselect" => {
10600                let paren = self.eat(&Token::LParen);
10601                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
10602                if paren {
10603                    self.expect(&Token::RParen)?;
10604                }
10605                if receivers.is_empty() {
10606                    return Err(self.syntax_err("pselect needs at least one receiver", line));
10607                }
10608                Ok(Expr {
10609                    kind: ExprKind::PselectExpr {
10610                        receivers,
10611                        timeout: timeout.map(Box::new),
10612                    },
10613                    line,
10614                })
10615            }
10616            "open" => {
10617                let paren = matches!(self.peek(), Token::LParen);
10618                if paren {
10619                    self.advance();
10620                }
10621                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
10622                    self.advance();
10623                    let name = self.parse_scalar_var_name()?;
10624                    self.expect(&Token::Comma)?;
10625                    let mode = self.parse_assign_expr()?;
10626                    let file = if self.eat(&Token::Comma) {
10627                        Some(self.parse_assign_expr()?)
10628                    } else {
10629                        None
10630                    };
10631                    if paren {
10632                        self.expect(&Token::RParen)?;
10633                    }
10634                    Ok(Expr {
10635                        kind: ExprKind::Open {
10636                            handle: Box::new(Expr {
10637                                kind: ExprKind::OpenMyHandle { name },
10638                                line,
10639                            }),
10640                            mode: Box::new(mode),
10641                            file: file.map(Box::new),
10642                        },
10643                        line,
10644                    })
10645                } else {
10646                    let args = if paren {
10647                        self.parse_arg_list()?
10648                    } else {
10649                        self.parse_list_until_terminator()?
10650                    };
10651                    if paren {
10652                        self.expect(&Token::RParen)?;
10653                    }
10654                    if args.len() < 2 {
10655                        return Err(self.syntax_err("open requires at least 2 arguments", line));
10656                    }
10657                    Ok(Expr {
10658                        kind: ExprKind::Open {
10659                            handle: Box::new(args[0].clone()),
10660                            mode: Box::new(args[1].clone()),
10661                            file: args.get(2).cloned().map(Box::new),
10662                        },
10663                        line,
10664                    })
10665                }
10666            }
10667            "close" => {
10668                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10669                    return Ok(e);
10670                }
10671                let a = self.parse_one_arg_or_default()?;
10672                Ok(Expr {
10673                    kind: ExprKind::Close(Box::new(a)),
10674                    line,
10675                })
10676            }
10677            "opendir" => {
10678                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10679                    return Ok(e);
10680                }
10681                let args = self.parse_builtin_args()?;
10682                if args.len() != 2 {
10683                    return Err(self.syntax_err("opendir requires two arguments", line));
10684                }
10685                Ok(Expr {
10686                    kind: ExprKind::Opendir {
10687                        handle: Box::new(args[0].clone()),
10688                        path: Box::new(args[1].clone()),
10689                    },
10690                    line,
10691                })
10692            }
10693            "readdir" => {
10694                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10695                    return Ok(e);
10696                }
10697                let a = self.parse_one_arg()?;
10698                Ok(Expr {
10699                    kind: ExprKind::Readdir(Box::new(a)),
10700                    line,
10701                })
10702            }
10703            "closedir" => {
10704                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10705                    return Ok(e);
10706                }
10707                let a = self.parse_one_arg()?;
10708                Ok(Expr {
10709                    kind: ExprKind::Closedir(Box::new(a)),
10710                    line,
10711                })
10712            }
10713            "rewinddir" => {
10714                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10715                    return Ok(e);
10716                }
10717                let a = self.parse_one_arg()?;
10718                Ok(Expr {
10719                    kind: ExprKind::Rewinddir(Box::new(a)),
10720                    line,
10721                })
10722            }
10723            "telldir" => {
10724                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10725                    return Ok(e);
10726                }
10727                let a = self.parse_one_arg()?;
10728                Ok(Expr {
10729                    kind: ExprKind::Telldir(Box::new(a)),
10730                    line,
10731                })
10732            }
10733            "seekdir" => {
10734                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10735                    return Ok(e);
10736                }
10737                let args = self.parse_builtin_args()?;
10738                if args.len() != 2 {
10739                    return Err(self.syntax_err("seekdir requires two arguments", line));
10740                }
10741                Ok(Expr {
10742                    kind: ExprKind::Seekdir {
10743                        handle: Box::new(args[0].clone()),
10744                        position: Box::new(args[1].clone()),
10745                    },
10746                    line,
10747                })
10748            }
10749            "eof" => {
10750                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10751                    return Ok(e);
10752                }
10753                if matches!(self.peek(), Token::LParen) {
10754                    self.advance();
10755                    if matches!(self.peek(), Token::RParen) {
10756                        self.advance();
10757                        Ok(Expr {
10758                            kind: ExprKind::Eof(None),
10759                            line,
10760                        })
10761                    } else {
10762                        let a = self.parse_expression()?;
10763                        self.expect(&Token::RParen)?;
10764                        Ok(Expr {
10765                            kind: ExprKind::Eof(Some(Box::new(a))),
10766                            line,
10767                        })
10768                    }
10769                } else {
10770                    Ok(Expr {
10771                        kind: ExprKind::Eof(None),
10772                        line,
10773                    })
10774                }
10775            }
10776            "system" => {
10777                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10778                    return Ok(e);
10779                }
10780                let args = self.parse_builtin_args()?;
10781                Ok(Expr {
10782                    kind: ExprKind::System(args),
10783                    line,
10784                })
10785            }
10786            "exec" => {
10787                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10788                    return Ok(e);
10789                }
10790                let args = self.parse_builtin_args()?;
10791                Ok(Expr {
10792                    kind: ExprKind::Exec(args),
10793                    line,
10794                })
10795            }
10796            "eval" => {
10797                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10798                    return Ok(e);
10799                }
10800                let a = if matches!(self.peek(), Token::LBrace) {
10801                    let block = self.parse_block()?;
10802                    Expr {
10803                        kind: ExprKind::CodeRef {
10804                            params: vec![],
10805                            body: block,
10806                        },
10807                        line,
10808                    }
10809                } else {
10810                    self.parse_one_arg_or_default()?
10811                };
10812                Ok(Expr {
10813                    kind: ExprKind::Eval(Box::new(a)),
10814                    line,
10815                })
10816            }
10817            "do" => {
10818                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10819                    return Ok(e);
10820                }
10821                let a = self.parse_one_arg()?;
10822                Ok(Expr {
10823                    kind: ExprKind::Do(Box::new(a)),
10824                    line,
10825                })
10826            }
10827            "require" => {
10828                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10829                    return Ok(e);
10830                }
10831                let a = self.parse_one_arg()?;
10832                Ok(Expr {
10833                    kind: ExprKind::Require(Box::new(a)),
10834                    line,
10835                })
10836            }
10837            "exit" => {
10838                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10839                    return Ok(e);
10840                }
10841                if matches!(
10842                    self.peek(),
10843                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
10844                ) {
10845                    Ok(Expr {
10846                        kind: ExprKind::Exit(None),
10847                        line,
10848                    })
10849                } else {
10850                    let a = self.parse_one_arg()?;
10851                    Ok(Expr {
10852                        kind: ExprKind::Exit(Some(Box::new(a))),
10853                        line,
10854                    })
10855                }
10856            }
10857            "chdir" => {
10858                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10859                    return Ok(e);
10860                }
10861                let a = self.parse_one_arg_or_default()?;
10862                Ok(Expr {
10863                    kind: ExprKind::Chdir(Box::new(a)),
10864                    line,
10865                })
10866            }
10867            "mkdir" => {
10868                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10869                    return Ok(e);
10870                }
10871                let args = self.parse_builtin_args()?;
10872                Ok(Expr {
10873                    kind: ExprKind::Mkdir {
10874                        path: Box::new(args[0].clone()),
10875                        mode: args.get(1).cloned().map(Box::new),
10876                    },
10877                    line,
10878                })
10879            }
10880            "unlink" | "rm" => {
10881                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10882                    return Ok(e);
10883                }
10884                let args = self.parse_builtin_args()?;
10885                Ok(Expr {
10886                    kind: ExprKind::Unlink(args),
10887                    line,
10888                })
10889            }
10890            "rename" => {
10891                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10892                    return Ok(e);
10893                }
10894                let args = self.parse_builtin_args()?;
10895                if args.len() != 2 {
10896                    return Err(self.syntax_err("rename requires two arguments", line));
10897                }
10898                Ok(Expr {
10899                    kind: ExprKind::Rename {
10900                        old: Box::new(args[0].clone()),
10901                        new: Box::new(args[1].clone()),
10902                    },
10903                    line,
10904                })
10905            }
10906            "chmod" => {
10907                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10908                    return Ok(e);
10909                }
10910                let args = self.parse_builtin_args()?;
10911                if args.len() < 2 {
10912                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
10913                }
10914                Ok(Expr {
10915                    kind: ExprKind::Chmod(args),
10916                    line,
10917                })
10918            }
10919            "chown" => {
10920                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10921                    return Ok(e);
10922                }
10923                let args = self.parse_builtin_args()?;
10924                if args.len() < 3 {
10925                    return Err(
10926                        self.syntax_err("chown requires uid, gid, and at least one file", line)
10927                    );
10928                }
10929                Ok(Expr {
10930                    kind: ExprKind::Chown(args),
10931                    line,
10932                })
10933            }
10934            "stat" => {
10935                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10936                    return Ok(e);
10937                }
10938                let args = self.parse_builtin_args()?;
10939                let arg = if args.len() == 1 {
10940                    args[0].clone()
10941                } else if args.is_empty() {
10942                    Expr {
10943                        kind: ExprKind::ScalarVar("_".into()),
10944                        line,
10945                    }
10946                } else {
10947                    return Err(self.syntax_err("stat requires zero or one argument", line));
10948                };
10949                Ok(Expr {
10950                    kind: ExprKind::Stat(Box::new(arg)),
10951                    line,
10952                })
10953            }
10954            "lstat" => {
10955                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10956                    return Ok(e);
10957                }
10958                let args = self.parse_builtin_args()?;
10959                let arg = if args.len() == 1 {
10960                    args[0].clone()
10961                } else if args.is_empty() {
10962                    Expr {
10963                        kind: ExprKind::ScalarVar("_".into()),
10964                        line,
10965                    }
10966                } else {
10967                    return Err(self.syntax_err("lstat requires zero or one argument", line));
10968                };
10969                Ok(Expr {
10970                    kind: ExprKind::Lstat(Box::new(arg)),
10971                    line,
10972                })
10973            }
10974            "link" => {
10975                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10976                    return Ok(e);
10977                }
10978                let args = self.parse_builtin_args()?;
10979                if args.len() != 2 {
10980                    return Err(self.syntax_err("link requires two arguments", line));
10981                }
10982                Ok(Expr {
10983                    kind: ExprKind::Link {
10984                        old: Box::new(args[0].clone()),
10985                        new: Box::new(args[1].clone()),
10986                    },
10987                    line,
10988                })
10989            }
10990            "symlink" => {
10991                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10992                    return Ok(e);
10993                }
10994                let args = self.parse_builtin_args()?;
10995                if args.len() != 2 {
10996                    return Err(self.syntax_err("symlink requires two arguments", line));
10997                }
10998                Ok(Expr {
10999                    kind: ExprKind::Symlink {
11000                        old: Box::new(args[0].clone()),
11001                        new: Box::new(args[1].clone()),
11002                    },
11003                    line,
11004                })
11005            }
11006            "readlink" => {
11007                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11008                    return Ok(e);
11009                }
11010                let args = self.parse_builtin_args()?;
11011                let arg = if args.len() == 1 {
11012                    args[0].clone()
11013                } else if args.is_empty() {
11014                    Expr {
11015                        kind: ExprKind::ScalarVar("_".into()),
11016                        line,
11017                    }
11018                } else {
11019                    return Err(self.syntax_err("readlink requires zero or one argument", line));
11020                };
11021                Ok(Expr {
11022                    kind: ExprKind::Readlink(Box::new(arg)),
11023                    line,
11024                })
11025            }
11026            "files" => {
11027                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11028                    return Ok(e);
11029                }
11030                let args = self.parse_builtin_args()?;
11031                Ok(Expr {
11032                    kind: ExprKind::Files(args),
11033                    line,
11034                })
11035            }
11036            "filesf" | "f" => {
11037                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11038                    return Ok(e);
11039                }
11040                let args = self.parse_builtin_args()?;
11041                Ok(Expr {
11042                    kind: ExprKind::Filesf(args),
11043                    line,
11044                })
11045            }
11046            "fr" => {
11047                let args = self.parse_builtin_args()?;
11048                Ok(Expr {
11049                    kind: ExprKind::FilesfRecursive(args),
11050                    line,
11051                })
11052            }
11053            "dirs" | "d" => {
11054                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11055                    return Ok(e);
11056                }
11057                let args = self.parse_builtin_args()?;
11058                Ok(Expr {
11059                    kind: ExprKind::Dirs(args),
11060                    line,
11061                })
11062            }
11063            "dr" => {
11064                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11065                    return Ok(e);
11066                }
11067                let args = self.parse_builtin_args()?;
11068                Ok(Expr {
11069                    kind: ExprKind::DirsRecursive(args),
11070                    line,
11071                })
11072            }
11073            "sym_links" => {
11074                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11075                    return Ok(e);
11076                }
11077                let args = self.parse_builtin_args()?;
11078                Ok(Expr {
11079                    kind: ExprKind::SymLinks(args),
11080                    line,
11081                })
11082            }
11083            "sockets" => {
11084                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11085                    return Ok(e);
11086                }
11087                let args = self.parse_builtin_args()?;
11088                Ok(Expr {
11089                    kind: ExprKind::Sockets(args),
11090                    line,
11091                })
11092            }
11093            "pipes" => {
11094                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11095                    return Ok(e);
11096                }
11097                let args = self.parse_builtin_args()?;
11098                Ok(Expr {
11099                    kind: ExprKind::Pipes(args),
11100                    line,
11101                })
11102            }
11103            "block_devices" => {
11104                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11105                    return Ok(e);
11106                }
11107                let args = self.parse_builtin_args()?;
11108                Ok(Expr {
11109                    kind: ExprKind::BlockDevices(args),
11110                    line,
11111                })
11112            }
11113            "char_devices" => {
11114                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11115                    return Ok(e);
11116                }
11117                let args = self.parse_builtin_args()?;
11118                Ok(Expr {
11119                    kind: ExprKind::CharDevices(args),
11120                    line,
11121                })
11122            }
11123            "exe" | "executables" => {
11124                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11125                    return Ok(e);
11126                }
11127                let args = self.parse_builtin_args()?;
11128                Ok(Expr {
11129                    kind: ExprKind::Executables(args),
11130                    line,
11131                })
11132            }
11133            "glob" => {
11134                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11135                    return Ok(e);
11136                }
11137                let args = self.parse_builtin_args()?;
11138                Ok(Expr {
11139                    kind: ExprKind::Glob(args),
11140                    line,
11141                })
11142            }
11143            "glob_par" => {
11144                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11145                    return Ok(e);
11146                }
11147                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11148                Ok(Expr {
11149                    kind: ExprKind::GlobPar { args, progress },
11150                    line,
11151                })
11152            }
11153            "par_sed" => {
11154                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11155                    return Ok(e);
11156                }
11157                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11158                Ok(Expr {
11159                    kind: ExprKind::ParSed { args, progress },
11160                    line,
11161                })
11162            }
11163            "bless" => {
11164                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11165                    return Ok(e);
11166                }
11167                let args = self.parse_builtin_args()?;
11168                Ok(Expr {
11169                    kind: ExprKind::Bless {
11170                        ref_expr: Box::new(args[0].clone()),
11171                        class: args.get(1).cloned().map(Box::new),
11172                    },
11173                    line,
11174                })
11175            }
11176            "caller" => {
11177                if matches!(self.peek(), Token::LParen) {
11178                    self.advance();
11179                    if matches!(self.peek(), Token::RParen) {
11180                        self.advance();
11181                        Ok(Expr {
11182                            kind: ExprKind::Caller(None),
11183                            line,
11184                        })
11185                    } else {
11186                        let a = self.parse_expression()?;
11187                        self.expect(&Token::RParen)?;
11188                        Ok(Expr {
11189                            kind: ExprKind::Caller(Some(Box::new(a))),
11190                            line,
11191                        })
11192                    }
11193                } else {
11194                    Ok(Expr {
11195                        kind: ExprKind::Caller(None),
11196                        line,
11197                    })
11198                }
11199            }
11200            "wantarray" => {
11201                if matches!(self.peek(), Token::LParen) {
11202                    self.advance();
11203                    self.expect(&Token::RParen)?;
11204                }
11205                Ok(Expr {
11206                    kind: ExprKind::Wantarray,
11207                    line,
11208                })
11209            }
11210            "sub" => {
11211                // In non-compat mode, `fn {}` is not valid — must use `fn {}`
11212                if !crate::compat_mode() {
11213                    return Err(self.syntax_err(
11214                        "stryke uses `fn {}` instead of `fn {}` (this is not Perl 5)",
11215                        line,
11216                    ));
11217                }
11218                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
11219                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11220                let body = self.parse_block()?;
11221                Ok(Expr {
11222                    kind: ExprKind::CodeRef { params, body },
11223                    line,
11224                })
11225            }
11226            "fn" => {
11227                // Anonymous fn — stryke syntax for anonymous subroutines
11228                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11229                let body = self.parse_block()?;
11230                Ok(Expr {
11231                    kind: ExprKind::CodeRef { params, body },
11232                    line,
11233                })
11234            }
11235            _ => {
11236                // Generic function call
11237                // Check for fat arrow (bareword string in hash)
11238                if matches!(self.peek(), Token::FatArrow) {
11239                    return Ok(Expr {
11240                        kind: ExprKind::String(name),
11241                        line,
11242                    });
11243                }
11244                // Bare `_` in expression position → topic variable `$_`.
11245                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
11246                if name == "_" {
11247                    return Ok(Expr {
11248                        kind: ExprKind::ScalarVar("_".to_string()),
11249                        line,
11250                    });
11251                }
11252                // Function call with optional parens
11253                if matches!(self.peek(), Token::LParen) {
11254                    self.advance();
11255                    let args = self.parse_arg_list()?;
11256                    self.expect(&Token::RParen)?;
11257                    Ok(Expr {
11258                        kind: ExprKind::FuncCall { name, args },
11259                        line,
11260                    })
11261                } else if self.peek().is_term_start()
11262                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
11263                        && matches!(self.peek_at(1), Token::Ident(_)))
11264                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
11265                    && !(matches!(self.peek(), Token::LBrace)
11266                        && self.peek_line() > self.prev_line())
11267                {
11268                    // Perl allows func arg without parens
11269                    // Guard: `sub <name> { }` is a named sub declaration (new
11270                    // statement), not an argument to the preceding call.
11271                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
11272                    // barewords (used by thread macro so `t Color::Red p` treats
11273                    // `p` as a stage, not an argument to the enum variant), but
11274                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
11275                    // Guard: `{` on a new line is a new statement (hashref/block),
11276                    // not an argument to the preceding bareword call.
11277                    let args = self.parse_list_until_terminator()?;
11278                    Ok(Expr {
11279                        kind: ExprKind::FuncCall { name, args },
11280                        line,
11281                    })
11282                } else {
11283                    // No parens, no visible arguments — emit a Bareword.
11284                    // At runtime, Bareword tries sub resolution first (zero-arg
11285                    // call) and falls back to a string value.  stryke extension
11286                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
11287                    // with `$_` injection separately.
11288                    Ok(Expr {
11289                        kind: ExprKind::Bareword(name),
11290                        line,
11291                    })
11292                }
11293            }
11294        }
11295    }
11296
11297    fn parse_print_like(
11298        &mut self,
11299        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
11300    ) -> PerlResult<Expr> {
11301        let line = self.peek_line();
11302        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
11303        let handle = if let Token::Ident(ref h) = self.peek().clone() {
11304            if h.chars().all(|c| c.is_uppercase() || c == '_')
11305                && !matches!(self.peek(), Token::LParen)
11306            {
11307                let h = h.clone();
11308                let saved = self.pos;
11309                self.advance();
11310                // Verify next token is a term start (not operator)
11311                if self.peek().is_term_start()
11312                    || matches!(
11313                        self.peek(),
11314                        Token::DoubleString(_) | Token::BacktickString(_) | Token::SingleString(_)
11315                    )
11316                {
11317                    Some(h)
11318                } else {
11319                    self.pos = saved;
11320                    None
11321                }
11322            } else {
11323                None
11324            }
11325        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
11326            // `print $fh "msg"` — scalar variable as indirect filehandle.
11327            // Treat as handle when the next token (after $var) is a term-start or
11328            // string literal *without* a preceding comma/operator, matching Perl's
11329            // indirect-object heuristic.
11330            // Exclude `$_` — it's virtually always the topic variable, not a handle.
11331            // Exclude `[` and `{` — those are array/hash subscripts on the variable
11332            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
11333            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
11334            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
11335            let v = v.clone();
11336            if v == "_" {
11337                None
11338            } else {
11339                let saved = self.pos;
11340                self.advance();
11341                let next = self.peek().clone();
11342                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
11343                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
11344                if !is_stmt_modifier
11345                    && !matches!(next, Token::LBracket | Token::LBrace)
11346                    && (next.is_term_start()
11347                        || matches!(
11348                            next,
11349                            Token::DoubleString(_)
11350                                | Token::BacktickString(_)
11351                                | Token::SingleString(_)
11352                        ))
11353                {
11354                    // Next token looks like a print argument — $var is the handle.
11355                    Some(format!("${v}"))
11356                } else {
11357                    self.pos = saved;
11358                    None
11359                }
11360            }
11361        } else {
11362            None
11363        };
11364        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
11365        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
11366        // are given, prints $_." (Same convention as the topic-default unary
11367        // builtins handled in `parse_one_arg_or_default`.)
11368        let args =
11369            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
11370                let line_topic = self.peek_line();
11371                self.advance(); // (
11372                self.advance(); // )
11373                vec![Expr {
11374                    kind: ExprKind::ScalarVar("_".into()),
11375                    line: line_topic,
11376                }]
11377            } else {
11378                self.parse_list_until_terminator()?
11379            };
11380        Ok(Expr {
11381            kind: make(handle, args),
11382            line,
11383        })
11384    }
11385
11386    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
11387        let block = self.parse_block()?;
11388        let block_end_line = self.prev_line();
11389        self.eat(&Token::Comma);
11390        // On the RHS of `|>`, the list operand is supplied by the piped LHS
11391        // and will be substituted at desugar time — accept a placeholder when
11392        // we're at a terminator here or on a new line (implicit semicolon).
11393        if self.in_pipe_rhs()
11394            && (matches!(
11395                self.peek(),
11396                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11397            ) || self.peek_line() > block_end_line)
11398        {
11399            let line = self.peek_line();
11400            return Ok((block, self.pipe_placeholder_list(line)));
11401        }
11402        let list = self.parse_expression()?;
11403        Ok((block, list))
11404    }
11405
11406    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
11407    /// When `paren` is true, stops at `)` as well as normal terminators.
11408    fn parse_comma_expr_list_with_timeout_tail(
11409        &mut self,
11410        paren: bool,
11411    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
11412        let mut parts = vec![self.parse_assign_expr()?];
11413        loop {
11414            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11415                break;
11416            }
11417            if paren && matches!(self.peek(), Token::RParen) {
11418                break;
11419            }
11420            if matches!(
11421                self.peek(),
11422                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11423            ) {
11424                break;
11425            }
11426            if self.peek_is_postfix_stmt_modifier_keyword() {
11427                break;
11428            }
11429            if let Token::Ident(ref kw) = self.peek().clone() {
11430                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
11431                    self.advance();
11432                    self.expect(&Token::FatArrow)?;
11433                    let t = self.parse_assign_expr()?;
11434                    return Ok((parts, Some(t)));
11435                }
11436            }
11437            parts.push(self.parse_assign_expr()?);
11438        }
11439        Ok((parts, None))
11440    }
11441
11442    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
11443    fn parse_init_block_then_list_optional_progress(
11444        &mut self,
11445    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
11446        let init = self.parse_assign_expr()?;
11447        self.expect(&Token::Comma)?;
11448        let block = self.parse_block_or_bareword_block()?;
11449        self.eat(&Token::Comma);
11450        let line = self.peek_line();
11451        if let Token::Ident(ref kw) = self.peek().clone() {
11452            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11453                self.advance();
11454                self.expect(&Token::FatArrow)?;
11455                let prog = self.parse_assign_expr()?;
11456                return Ok((
11457                    init,
11458                    block,
11459                    Expr {
11460                        kind: ExprKind::List(vec![]),
11461                        line,
11462                    },
11463                    Some(prog),
11464                ));
11465            }
11466        }
11467        if matches!(
11468            self.peek(),
11469            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11470        ) {
11471            return Ok((
11472                init,
11473                block,
11474                Expr {
11475                    kind: ExprKind::List(vec![]),
11476                    line,
11477                },
11478                None,
11479            ));
11480        }
11481        let mut parts = vec![self.parse_assign_expr()?];
11482        loop {
11483            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11484                break;
11485            }
11486            if matches!(
11487                self.peek(),
11488                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11489            ) {
11490                break;
11491            }
11492            if self.peek_is_postfix_stmt_modifier_keyword() {
11493                break;
11494            }
11495            if let Token::Ident(ref kw) = self.peek().clone() {
11496                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11497                    self.advance();
11498                    self.expect(&Token::FatArrow)?;
11499                    let prog = self.parse_assign_expr()?;
11500                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
11501                }
11502            }
11503            parts.push(self.parse_assign_expr()?);
11504        }
11505        Ok((init, block, merge_expr_list(parts), None))
11506    }
11507
11508    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
11509    fn parse_cluster_block_then_list_optional_progress(
11510        &mut self,
11511    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
11512        let cluster = self.parse_assign_expr()?;
11513        let block = self.parse_block_or_bareword_block()?;
11514        self.eat(&Token::Comma);
11515        let line = self.peek_line();
11516        if let Token::Ident(ref kw) = self.peek().clone() {
11517            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11518                self.advance();
11519                self.expect(&Token::FatArrow)?;
11520                let prog = self.parse_assign_expr_stop_at_pipe()?;
11521                return Ok((
11522                    cluster,
11523                    block,
11524                    Expr {
11525                        kind: ExprKind::List(vec![]),
11526                        line,
11527                    },
11528                    Some(prog),
11529                ));
11530            }
11531        }
11532        let empty_list_ok = matches!(
11533            self.peek(),
11534            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11535        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
11536        if empty_list_ok {
11537            return Ok((
11538                cluster,
11539                block,
11540                Expr {
11541                    kind: ExprKind::List(vec![]),
11542                    line,
11543                },
11544                None,
11545            ));
11546        }
11547        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11548        loop {
11549            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11550                break;
11551            }
11552            if matches!(
11553                self.peek(),
11554                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11555            ) {
11556                break;
11557            }
11558            if self.peek_is_postfix_stmt_modifier_keyword() {
11559                break;
11560            }
11561            if let Token::Ident(ref kw) = self.peek().clone() {
11562                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11563                    self.advance();
11564                    self.expect(&Token::FatArrow)?;
11565                    let prog = self.parse_assign_expr_stop_at_pipe()?;
11566                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
11567                }
11568            }
11569            parts.push(self.parse_assign_expr_stop_at_pipe()?);
11570        }
11571        Ok((cluster, block, merge_expr_list(parts), None))
11572    }
11573
11574    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
11575    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
11576    ///
11577    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
11578    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
11579    /// stage — individual list parts and the progress value parse through
11580    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
11581    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
11582    fn parse_block_then_list_optional_progress(
11583        &mut self,
11584    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
11585        let block = self.parse_block_or_bareword_block()?;
11586        self.eat(&Token::Comma);
11587        let line = self.peek_line();
11588        if let Token::Ident(ref kw) = self.peek().clone() {
11589            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11590                self.advance();
11591                self.expect(&Token::FatArrow)?;
11592                let prog = self.parse_assign_expr_stop_at_pipe()?;
11593                return Ok((
11594                    block,
11595                    Expr {
11596                        kind: ExprKind::List(vec![]),
11597                        line,
11598                    },
11599                    Some(prog),
11600                ));
11601            }
11602        }
11603        // An empty list operand is allowed when the next token terminates the
11604        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
11605        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
11606        // terminator — left-associative chaining leaves the outer `|>` for
11607        // the enclosing pipe-forward loop.
11608        let empty_list_ok = matches!(
11609            self.peek(),
11610            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11611        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
11612        if empty_list_ok {
11613            return Ok((
11614                block,
11615                Expr {
11616                    kind: ExprKind::List(vec![]),
11617                    line,
11618                },
11619                None,
11620            ));
11621        }
11622        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11623        loop {
11624            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11625                break;
11626            }
11627            if matches!(
11628                self.peek(),
11629                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11630            ) {
11631                break;
11632            }
11633            if self.peek_is_postfix_stmt_modifier_keyword() {
11634                break;
11635            }
11636            if let Token::Ident(ref kw) = self.peek().clone() {
11637                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11638                    self.advance();
11639                    self.expect(&Token::FatArrow)?;
11640                    let prog = self.parse_assign_expr_stop_at_pipe()?;
11641                    return Ok((block, merge_expr_list(parts), Some(prog)));
11642                }
11643            }
11644            parts.push(self.parse_assign_expr_stop_at_pipe()?);
11645        }
11646        Ok((block, merge_expr_list(parts), None))
11647    }
11648
11649    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
11650    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
11651        // `fan { BLOCK }` — no count
11652        if matches!(self.peek(), Token::LBrace) {
11653            let block = self.parse_block()?;
11654            return Ok((None, block));
11655        }
11656        let saved = self.pos;
11657        // Not a brace — first expr could be count or body
11658        let first = self.parse_postfix()?;
11659        if matches!(self.peek(), Token::LBrace) {
11660            // `fan COUNT { BLOCK }`
11661            let block = self.parse_block()?;
11662            Ok((Some(Box::new(first)), block))
11663        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
11664            || (matches!(self.peek(), Token::Comma)
11665                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
11666        {
11667            // `fan EXPR;` — no count, first is the body
11668            let block = self.bareword_to_no_arg_block(first);
11669            Ok((None, block))
11670        } else if matches!(first.kind, ExprKind::Integer(_)) {
11671            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
11672            self.eat(&Token::Comma);
11673            let body = self.parse_fan_blockless_body(line)?;
11674            Ok((Some(Box::new(first)), body))
11675        } else {
11676            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
11677            // — backtrack and re-parse as a full body expression.
11678            self.pos = saved;
11679            let body = self.parse_fan_blockless_body(line)?;
11680            Ok((None, body))
11681        }
11682    }
11683
11684    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
11685    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
11686        if matches!(self.peek(), Token::LBrace) {
11687            return self.parse_block();
11688        }
11689        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
11690        if let Token::Ident(ref name) = self.peek().clone() {
11691            if matches!(
11692                self.peek_at(1),
11693                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11694            ) {
11695                let name = name.clone();
11696                self.advance();
11697                let body = Expr {
11698                    kind: ExprKind::FuncCall { name, args: vec![] },
11699                    line,
11700                };
11701                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11702            }
11703        }
11704        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
11705        let expr = self.parse_assign_expr_stop_at_pipe()?;
11706        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11707    }
11708
11709    /// Wrap a parsed expression as a single-statement block, converting bare
11710    /// identifiers to zero-arg calls (`work` → `work()`).
11711    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
11712        let line = expr.line;
11713        let body = match &expr.kind {
11714            ExprKind::Bareword(name) => Expr {
11715                kind: ExprKind::FuncCall {
11716                    name: name.clone(),
11717                    args: vec![],
11718                },
11719                line,
11720            },
11721            _ => expr,
11722        };
11723        vec![Statement::new(StmtKind::Expression(body), line)]
11724    }
11725
11726    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
11727    ///
11728    /// When the next token is `{`, delegates to [`Self::parse_block`].
11729    /// Otherwise parses a single postfix expression and wraps it as a call
11730    /// with `$_` as argument (for barewords) or a plain expression statement:
11731    ///
11732    /// - Bareword `foo` → `{ foo($_) }`
11733    /// - Other expr     → `{ EXPR }`
11734    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
11735        if matches!(self.peek(), Token::LBrace) {
11736            return self.parse_block();
11737        }
11738        let line = self.peek_line();
11739        // A lone identifier followed by a list-terminator is a bare sub name:
11740        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
11741        if let Token::Ident(ref name) = self.peek().clone() {
11742            if matches!(
11743                self.peek_at(1),
11744                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11745            ) {
11746                let name = name.clone();
11747                self.advance();
11748                let body = Expr {
11749                    kind: ExprKind::FuncCall {
11750                        name,
11751                        args: vec![Expr {
11752                            kind: ExprKind::ScalarVar("_".to_string()),
11753                            line,
11754                        }],
11755                    },
11756                    line,
11757                };
11758                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11759            }
11760        }
11761        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
11762        let expr = self.parse_assign_expr_stop_at_pipe()?;
11763        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11764    }
11765
11766    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
11767    /// bare function takes no args (body runs stand-alone, not per-element).
11768    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
11769    /// greedily swallow subsequent tokens as function arguments.
11770    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
11771        if matches!(self.peek(), Token::LBrace) {
11772            return self.parse_block();
11773        }
11774        let line = self.peek_line();
11775        if let Token::Ident(ref name) = self.peek().clone() {
11776            if matches!(
11777                self.peek_at(1),
11778                Token::Comma
11779                    | Token::Semicolon
11780                    | Token::RBrace
11781                    | Token::Eof
11782                    | Token::PipeForward
11783                    | Token::Integer(_)
11784            ) {
11785                let name = name.clone();
11786                self.advance();
11787                let body = Expr {
11788                    kind: ExprKind::FuncCall { name, args: vec![] },
11789                    line,
11790                };
11791                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11792            }
11793        }
11794        let expr = self.parse_postfix()?;
11795        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11796    }
11797
11798    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
11799    /// treated as a bare sub name (e.g. inside `sort`).
11800    /// True for any bareword the parser treats as a known builtin / keyword —
11801    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
11802    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
11803    /// as a comparator name if it *isn't* a known bareword). Previously named
11804    /// `is_perl_keyword`, which was misleading.
11805    fn is_known_bareword(name: &str) -> bool {
11806        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
11807    }
11808
11809    /// True iff `name` appears as any spelling (primary *or* alias) in a
11810    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
11811    /// up in the parser-level keyword lists but are still callable at
11812    /// runtime — so `map { tj }` can default to `tj($_)` the same way
11813    /// `map { to_json }` does.
11814    fn is_try_builtin_name(name: &str) -> bool {
11815        crate::builtins::BUILTIN_ARMS
11816            .iter()
11817            .any(|arm| arm.contains(&name))
11818    }
11819
11820    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
11821    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
11822    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
11823    /// is derived from this list by `build.rs`.
11824    fn is_perl5_core(name: &str) -> bool {
11825        matches!(
11826            name,
11827            // ── array / list ────────────────────────────────────────────
11828            "map" | "grep" | "sort" | "reverse" | "join" | "split"
11829            | "push" | "pop" | "shift" | "unshift" | "splice"
11830            | "pack" | "unpack"
11831            // ── hash ────────────────────────────────────────────────────
11832            | "keys" | "values" | "each"
11833            // ── string ──────────────────────────────────────────────────
11834            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
11835            | "lc" | "uc" | "lcfirst" | "ucfirst"
11836            | "length" | "substr" | "index" | "rindex"
11837            | "sprintf" | "printf" | "print" | "say"
11838            | "pos" | "quotemeta" | "study"
11839            // ── numeric ─────────────────────────────────────────────────
11840            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
11841            | "exp" | "log" | "rand" | "srand"
11842            // ── time ────────────────────────────────────────────────────
11843            | "time" | "localtime" | "gmtime"
11844            // ── type / reflection ───────────────────────────────────────
11845            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
11846            | "caller" | "delete" | "exists" | "bless" | "prototype"
11847            | "tie" | "untie" | "tied"
11848            // ── io ──────────────────────────────────────────────────────
11849            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
11850            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
11851            | "format" | "formline" | "select" | "vec"
11852            | "sysopen" | "sysread" | "sysseek" | "syswrite"
11853            // ── filesystem ──────────────────────────────────────────────
11854            | "stat" | "lstat" | "rename" | "unlink" | "utime"
11855            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
11856            | "glob" | "opendir" | "readdir" | "closedir"
11857            | "link" | "readlink" | "symlink"
11858            // ── ipc ─────────────────────────────────────────────────────
11859            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
11860            // ── sysv ipc ────────────────────────────────────────────────
11861            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
11862            | "semctl" | "semget" | "semop"
11863            | "shmctl" | "shmget" | "shmread" | "shmwrite"
11864            // ── process / system ────────────────────────────────────────
11865            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
11866            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
11867            | "chroot" | "times" | "umask" | "reset"
11868            | "getpgrp" | "setpgrp" | "getppid"
11869            | "getpriority" | "setpriority"
11870            // ── socket ──────────────────────────────────────────────────
11871            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
11872            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
11873            | "getpeername" | "getsockname"
11874            // ── posix metadata ──────────────────────────────────────────
11875            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
11876            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
11877            | "getlogin"
11878            | "gethostbyname" | "gethostbyaddr" | "gethostent"
11879            | "getnetbyname" | "getnetent"
11880            | "getprotobyname" | "getprotoent"
11881            | "getservbyname" | "getservent"
11882            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
11883            | "endpwent" | "endgrent"
11884            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
11885            // ── control flow ────────────────────────────────────────────
11886            | "return" | "do" | "eval" | "require"
11887            | "my" | "our" | "local" | "use" | "no"
11888            | "sub" | "if" | "unless" | "while" | "until"
11889            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
11890            | "not" | "and" | "or"
11891            // ── quoting ─────────────────────────────────────────────────
11892            | "qw" | "qq" | "q"
11893            // ── phase blocks ────────────────────────────────────────────
11894            | "BEGIN" | "END"
11895        )
11896    }
11897
11898    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
11899    /// Used by `--compat` to reject extensions at parse time.
11900    fn stryke_extension_name(name: &str) -> Option<&str> {
11901        match name {
11902            // ── parallel ────────────────────────────────────────────────────
11903            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
11904            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
11905            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
11906            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
11907            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
11908            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
11909            | "pmaps" | "pflat_maps" | "pgreps"
11910            // ── functional / iterator ───────────────────────────────────────
11911            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
11912            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
11913            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "flatten" | "set"
11914            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
11915            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
11916            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
11917            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
11918            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
11919            | "zip_with" | "count_by" | "skip" | "first_or"
11920            // ── pipeline / string helpers ───────────────────────────────────
11921            | "input" | "lines" | "words" | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
11922            | "punctuation" | "punct"
11923            | "sentences" | "sents"
11924            | "paragraphs" | "paras" | "sections" | "sects"
11925            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
11926            | "trim" | "avg" | "stddev"
11927            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
11928            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
11929            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
11930            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
11931            | "to_html" | "to_markdown" | "to_table" | "xopen"
11932            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
11933            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
11934            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
11935            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
11936            | "to_hash" | "to_set"
11937            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
11938            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
11939            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
11940            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
11941            | "inc" | "dec" | "elapsed"
11942            // ── filesystem extensions ───────────────────────────────────────
11943            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
11944            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
11945            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
11946            | "copy" | "move" | "spurt" | "read_bytes" | "which"
11947            | "getcwd" | "touch" | "gethostname" | "uname"
11948            // ── data / network ──────────────────────────────────────────────
11949            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
11950            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
11951            | "par_fetch" | "par_csv_read" | "par_pipeline"
11952            | "json_encode" | "json_decode" | "json_jq"
11953            | "http_request" | "serve" | "ssh"
11954            | "html_parse" | "css_select" | "xml_parse" | "xpath"
11955            | "smtp_send"
11956            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
11957            | "net_public_ip" | "net_dns" | "net_reverse_dns"
11958            | "net_ping" | "net_port_open" | "net_ports_scan"
11959            | "net_latency" | "net_download" | "net_headers"
11960            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
11961            // ── git ─────────────────────────────────────────────────────────
11962            | "git_log" | "git_status" | "git_diff" | "git_branches"
11963            | "git_tags" | "git_blame" | "git_authors" | "git_files"
11964            | "git_show" | "git_root"
11965            // ── audio / media ───────────────────────────────────────────────
11966            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
11967            // ── pdf ─────────────────────────────────────────────────────────
11968            | "to_pdf" | "pdf_text" | "pdf_pages"
11969            // ── serialization (stryke-only encoders) ────────────────────────
11970            | "toml_encode" | "toml_decode"
11971            | "yaml_encode" | "yaml_decode"
11972            | "xml_encode" | "xml_decode"
11973            // ── crypto / encoding ───────────────────────────────────────────
11974            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
11975            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
11976            | "shake128" | "shake256"
11977            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
11978            | "uuid" | "crc32"
11979            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
11980            | "ripemd160" | "rmd160" | "md4"
11981            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
11982            | "murmur3" | "murmur3_32" | "murmur3_128"
11983            | "siphash" | "siphash_keyed"
11984            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
11985            | "poly1305" | "poly1305_mac"
11986            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
11987            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
11988            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
11989            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
11990            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
11991            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
11992            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
11993            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
11994            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
11995            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
11996            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
11997            | "secretbox" | "secretbox_seal" | "secretbox_open"
11998            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
11999            | "nacl_box_open" | "box_open"
12000            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
12001            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
12002            | "barcode_ean13" | "ean13" | "barcode_svg"
12003            | "argon2_hash" | "argon2" | "argon2_verify"
12004            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
12005            | "scrypt_hash" | "scrypt" | "scrypt_verify"
12006            | "pbkdf2" | "pbkdf2_derive"
12007            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
12008            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
12009            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
12010            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
12011            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
12012            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
12013            | "ecdsa_p256_verify" | "p256_verify"
12014            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
12015            | "ecdsa_p384_verify" | "p384_verify"
12016            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
12017            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
12018            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
12019            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
12020            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
12021            | "ed25519_verify" | "ed_verify"
12022            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
12023            | "base64_encode" | "base64_decode"
12024            | "hex_encode" | "hex_decode"
12025            | "url_encode" | "url_decode"
12026            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
12027            | "brotli" | "br" | "brotli_decode" | "ubr"
12028            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
12029            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
12030            | "lz4" | "lz4_decode" | "unlz4"
12031            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
12032            | "lzw" | "lzw_decode" | "unlzw"
12033            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
12034            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
12035            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
12036            // ── special math functions ────────────────────────────────────────
12037            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
12038            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
12039            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
12040            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
12041            | "gammaincc_reg" | "gamma_ur"
12042            // ── date / time ─────────────────────────────────────────────────
12043            | "datetime_utc" | "datetime_now_tz"
12044            | "datetime_format_tz" | "datetime_add_seconds"
12045            | "datetime_from_epoch"
12046            | "datetime_parse_rfc3339" | "datetime_parse_local"
12047            | "datetime_strftime"
12048            | "dateseq" | "dategrep" | "dateround" | "datesort"
12049            // ── jwt ─────────────────────────────────────────────────────────
12050            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
12051            // ── logging ─────────────────────────────────────────────────────
12052            | "log_info" | "log_warn" | "log_error"
12053            | "log_debug" | "log_trace" | "log_json" | "log_level"
12054            // ── concurrency / timing ────────────────────────────────────────
12055            | "async" | "spawn" | "trace" | "timer" | "bench"
12056            | "eval_timeout" | "retry" | "rate_limit" | "every"
12057            | "gen" | "watch"
12058            // ── testing framework ────────────────────────────────────────────
12059            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
12060            | "assert_true" | "assert_false"
12061            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
12062            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
12063            | "test_run"
12064            // ── system info ─────────────────────────────────────────────────
12065            | "mounts" | "du" | "du_tree" | "process_list"
12066            | "thread_count" | "pool_info" | "par_bench"
12067            // ── I/O extensions ──────────────────────────────────────────────
12068            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
12069            | "stdin"
12070            // ── internal ────────────────────────────────────────────────────
12071            | "__stryke_rust_compile"
12072            // ── short aliases ───────────────────────────────────────────────
12073            | "p" | "rev"
12074            // ── trivial numeric / predicate builtins ────────────────────────
12075            | "even" | "odd" | "zero" | "nonzero"
12076            | "positive" | "pos_n" | "negative" | "neg_n"
12077            | "sign" | "negate" | "double" | "triple" | "half"
12078            | "identity" | "id"
12079            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
12080            | "gcd" | "lcm" | "min2" | "max2"
12081            | "log2" | "log10" | "hypot"
12082            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
12083            | "pow2" | "abs_diff"
12084            | "factorial" | "fact" | "fibonacci" | "fib"
12085            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
12086            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
12087            | "median" | "mode_val" | "variance"
12088            // ── trivial string ops ──────────────────────────────────────────
12089            | "is_empty" | "is_blank" | "is_numeric"
12090            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
12091            | "is_space" | "is_whitespace"
12092            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
12093            | "capitalize" | "cap" | "swap_case" | "repeat"
12094            | "title_case" | "title" | "squish"
12095            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
12096            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
12097            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
12098            // ── trivial type predicates ─────────────────────────────────────
12099            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
12100            | "is_code" | "is_coderef" | "is_ref"
12101            | "is_undef" | "is_defined" | "is_def"
12102            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
12103            // ── hash helpers ────────────────────────────────────────────────
12104            | "invert" | "merge_hash"
12105            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
12106            // ── boolean combinators ─────────────────────────────────────────
12107            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
12108            // ── collection helpers (trivial) ────────────────────────────────
12109            | "riffle" | "intersperse" | "every_nth"
12110            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
12111            // ── base conversion ─────────────────────────────────────────────
12112            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
12113            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
12114            | "bits_count" | "popcount" | "leading_zeros" | "lz"
12115            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
12116            // ── bit ops ─────────────────────────────────────────────────────
12117            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
12118            | "shift_left" | "shl" | "shift_right" | "shr"
12119            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
12120            // ── unit conversions: temperature ───────────────────────────────
12121            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
12122            // ── unit conversions: distance ──────────────────────────────────
12123            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
12124            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
12125            | "yards_to_m" | "m_to_yards"
12126            // ── unit conversions: mass ──────────────────────────────────────
12127            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
12128            | "stone_to_kg" | "kg_to_stone"
12129            // ── unit conversions: digital ───────────────────────────────────
12130            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
12131            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
12132            | "kb_to_mb" | "mb_to_gb"
12133            | "bits_to_bytes" | "bytes_to_bits"
12134            // ── unit conversions: time ──────────────────────────────────────
12135            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
12136            | "seconds_to_hours" | "hours_to_seconds"
12137            | "seconds_to_days" | "days_to_seconds"
12138            | "minutes_to_hours" | "hours_to_minutes"
12139            | "hours_to_days" | "days_to_hours"
12140            // ── date helpers ────────────────────────────────────────────────
12141            | "is_leap_year" | "is_leap" | "days_in_month"
12142            | "month_name" | "month_short"
12143            | "weekday_name" | "weekday_short" | "quarter_of"
12144            // ── now / timestamp ─────────────────────────────────────────────
12145            | "now_ms" | "now_us" | "now_ns"
12146            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
12147            // ── color / ANSI ────────────────────────────────────────────────
12148            | "rgb_to_hex" | "hex_to_rgb"
12149            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
12150            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
12151            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
12152            | "strip_ansi"
12153            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
12154            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
12155            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
12156            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
12157            | "bright_magenta" | "bright_cyan" | "bright_white"
12158            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
12159            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
12160            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
12161            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
12162            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
12163            | "white_bold" | "bold_white"
12164            | "blink" | "rapid_blink" | "hidden" | "overline"
12165            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
12166            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
12167            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
12168            // ── network / validation ────────────────────────────────────────
12169            | "ipv4_to_int" | "int_to_ipv4"
12170            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
12171            // ── path helpers ────────────────────────────────────────────────
12172            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
12173            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
12174            // ── functional primitives ───────────────────────────────────────
12175            | "const_fn" | "always_true" | "always_false"
12176            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
12177            // ── more list helpers ───────────────────────────────────────────
12178            | "count_eq" | "count_ne" | "all_eq"
12179            | "all_distinct" | "all_unique" | "has_duplicates"
12180            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
12181            // ── string quote / escape ───────────────────────────────────────
12182            | "quote" | "single_quote" | "unquote"
12183            | "extract_between" | "ellipsis"
12184            // ── random ──────────────────────────────────────────────────────
12185            | "coin_flip" | "dice_roll"
12186            | "random_int" | "random_float" | "random_bool"
12187            | "random_choice" | "random_between"
12188            | "random_string" | "random_alpha" | "random_digit"
12189            // ── system introspection ────────────────────────────────────────
12190            | "os_name" | "os_arch" | "num_cpus"
12191            | "pid" | "ppid" | "uid" | "gid"
12192            | "username" | "home_dir" | "temp_dir"
12193            | "mem_total" | "mem_free" | "mem_used"
12194            | "swap_total" | "swap_free" | "swap_used"
12195            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
12196            | "load_avg" | "sys_uptime" | "page_size"
12197            | "os_version" | "os_family" | "endianness" | "pointer_width"
12198            | "proc_mem" | "rss"
12199            // ── collection more ─────────────────────────────────────────────
12200            | "transpose" | "unzip"
12201            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
12202            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
12203            // ── trig / math (batch 2) ───────────────────────────────────────
12204            | "tan" | "asin" | "acos" | "atan"
12205            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
12206            | "sqr" | "cube_fn"
12207            | "mod_op" | "ceil_div" | "floor_div"
12208            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
12209            | "degrees" | "radians"
12210            | "min_abs" | "max_abs"
12211            | "saturate" | "sat01" | "wrap_around"
12212            // ── string (batch 2) ────────────────────────────────────────────
12213            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
12214            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
12215            | "first_word" | "last_word"
12216            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
12217            | "lowercase" | "uppercase"
12218            | "pascal_case" | "pc_case"
12219            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
12220            | "is_palindrome" | "hamming_distance"
12221            | "longest_common_prefix" | "lcp"
12222            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
12223            | "replace_first" | "replace_all_str"
12224            | "contains_any" | "contains_all"
12225            | "starts_with_any" | "ends_with_any"
12226            // ── predicates (batch 2) ────────────────────────────────────────
12227            | "is_pair" | "is_triple"
12228            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
12229            | "is_empty_arr" | "is_empty_hash"
12230            | "is_subset" | "is_superset" | "is_permutation"
12231            // ── collection (batch 2) ────────────────────────────────────────
12232            | "first_eq" | "last_eq"
12233            | "index_of" | "last_index_of" | "positions_of"
12234            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
12235            | "distinct_count" | "longest" | "shortest"
12236            | "array_union" | "list_union"
12237            | "array_intersection" | "list_intersection"
12238            | "array_difference" | "list_difference"
12239            | "symmetric_diff" | "group_of_n" | "chunk_n"
12240            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
12241            // ── hash ops (batch 2) ──────────────────────────────────────────
12242            | "pick_keys" | "pick" | "omit_keys" | "omit"
12243            | "map_keys_fn" | "map_values_fn"
12244            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
12245            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
12246            // ── date (batch 2) ──────────────────────────────────────────────
12247            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
12248            // ── json helpers ────────────────────────────────────────────────
12249            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
12250            // ── process / env ───────────────────────────────────────────────
12251            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
12252            | "argc" | "script_name"
12253            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
12254            // ── id helpers ──────────────────────────────────────────────────
12255            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
12256            // ── url / email parts ───────────────────────────────────────────
12257            | "email_domain" | "email_local"
12258            | "url_host" | "url_path" | "url_query" | "url_scheme"
12259            // ── file stat / path ────────────────────────────────────────────
12260            | "file_size" | "fsize" | "file_mtime" | "mtime"
12261            | "file_atime" | "atime" | "file_ctime" | "ctime"
12262            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
12263            | "path_is_abs" | "path_is_rel"
12264            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
12265            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
12266            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
12267            | "reverse_list" | "list_reverse"
12268            | "without" | "without_nth" | "take_last" | "drop_last"
12269            | "pairwise" | "zipmap"
12270            | "format_bytes" | "human_bytes"
12271            | "format_duration" | "human_duration"
12272            | "format_number" | "group_number"
12273            | "format_percent" | "pad_number"
12274            | "spaceship" | "cmp_num" | "cmp_str"
12275            | "compare_versions" | "version_cmp"
12276            | "hash_insert" | "hash_update" | "hash_delete"
12277            | "matches_regex" | "re_match"
12278            | "count_regex_matches" | "regex_extract"
12279            | "regex_split_str" | "regex_replace_str"
12280            | "shuffle_chars" | "random_char" | "nth_word"
12281            | "head_lines" | "tail_lines" | "count_substring"
12282            | "is_valid_hex" | "hex_upper" | "hex_lower"
12283            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
12284            | "us_to_ns" | "ns_to_us"
12285            | "liters_to_gallons" | "gallons_to_liters"
12286            | "liters_to_ml" | "ml_to_liters"
12287            | "cups_to_ml" | "ml_to_cups"
12288            | "newtons_to_lbf" | "lbf_to_newtons"
12289            | "joules_to_cal" | "cal_to_joules"
12290            | "watts_to_hp" | "hp_to_watts"
12291            | "pascals_to_psi" | "psi_to_pascals"
12292            | "bar_to_pascals" | "pascals_to_bar"
12293            // ── algebraic match ─────────────────────────────────────────────
12294            | "match"
12295            // ── clojure stdlib (only names not matched above) ─────────────────
12296            | "fst" | "rest" | "rst" | "second" | "snd"
12297            | "last_clj" | "lastc" | "butlast" | "bl"
12298            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
12299            | "cons" | "conj"
12300            | "peek_clj" | "pkc" | "pop_clj" | "popc"
12301            | "some" | "not_any" | "not_every"
12302            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
12303            | "fnil" | "juxt"
12304            | "memoize" | "memo" | "curry" | "once"
12305            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
12306            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
12307            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
12308            | "reductions" | "rdcs"
12309            | "partition_by" | "pby" | "partition_all" | "pall"
12310            | "split_at" | "spat" | "split_with" | "spw"
12311            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
12312            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
12313            | "apply" | "appl"
12314            // ── python/ruby stdlib ───────────────────────────────────────────
12315            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
12316            | "zip_longest" | "zipl" | "combinations" | "comb" | "permutations" | "perm"
12317            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
12318            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
12319            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
12320            | "each_slice" | "eslice" | "each_cons" | "econs"
12321            | "one" | "none_match" | "nonem"
12322            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
12323            | "minmax" | "mmx" | "minmax_by" | "mmxb"
12324            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
12325            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
12326            | "sum_by" | "sumb" | "uniq_by" | "uqb"
12327            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
12328            | "step" | "upto" | "downto"
12329            // ── javascript array/object methods ─────────────────────────────
12330            | "find_last" | "fndl" | "find_last_index" | "fndli"
12331            | "at_index" | "ati" | "replace_at" | "repa"
12332            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
12333            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
12334            | "object_keys" | "okeys" | "object_values" | "ovals"
12335            | "object_entries" | "oents" | "object_from_entries" | "ofents"
12336            // ── haskell list functions ──────────────────────────────────────
12337            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
12338            | "nub" | "sort_on" | "srton"
12339            | "intersperse_val" | "isp" | "intercalate" | "ical"
12340            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
12341            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
12342            // ── rust iterator methods ───────────────────────────────────────
12343            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
12344            | "partition_either" | "peith" | "try_fold" | "tfld"
12345            | "map_while" | "mapw" | "inspect" | "insp"
12346            // ── ruby enumerable extras ──────────────────────────────────────
12347            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
12348            // ── go/general functional utilities ─────────────────────────────
12349            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
12350            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
12351            | "lines_from" | "lfrm" | "unlines" | "unlns"
12352            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
12353            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
12354            | "interpose" | "ipos" | "partition_n" | "partn"
12355            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
12356            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
12357            // ── additional missing stdlib functions ─────────────────────────
12358            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
12359            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
12360            | "each_with_object" | "ewo" | "reduce_right" | "redr"
12361            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
12362            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
12363            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
12364            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
12365            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
12366            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
12367            | "union_list" | "unionl" | "intersect_list" | "intl"
12368            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
12369            // ── Extended stdlib: Text Processing ─────────────────────────────
12370            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
12371            | "split_regex" | "splre" | "replace_regex" | "replre"
12372            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
12373            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
12374            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
12375            | "pluralize" | "plur" | "ordinalize" | "ordn"
12376            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
12377            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
12378            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
12379            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
12380            // ── Extended stdlib: Advanced Numeric ────────────────────────────
12381            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
12382            | "dot_product" | "dotp" | "cross_product" | "crossp"
12383            | "matrix_mul" | "matmul" | "mm"
12384            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
12385            | "distance" | "dist" | "manhattan_distance" | "mdist"
12386            | "covariance" | "cov" | "correlation" | "corr"
12387            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
12388            | "in_range" | "inrng" | "wrap_range" | "wrprng"
12389            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
12390            // ── Extended stdlib: Date/Time ───────────────────────────────────
12391            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
12392            | "diff_days" | "diffd" | "diff_hours" | "diffh"
12393            | "start_of_day" | "sod" | "end_of_day" | "eod"
12394            | "start_of_hour" | "soh" | "start_of_minute" | "som"
12395            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
12396            | "urle" | "urld"
12397            | "html_encode" | "htmle" | "html_decode" | "htmld"
12398            | "adler32" | "adl32" | "fnv1a" | "djb2"
12399            // ── Extended stdlib: Validation ──────────────────────────────────
12400            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
12401            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
12402            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
12403            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
12404            // ── Extended stdlib: Collection Advanced ─────────────────────────
12405            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
12406            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
12407            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
12408            | "partition_point" | "ppt" | "lower_bound" | "lbound"
12409            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
12410            // ── Extended stdlib: Matrix Operations ───────────────────────────
12411            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
12412            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
12413            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
12414            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
12415            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
12416            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
12417            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
12418            // ── Extended stdlib: Graph Algorithms ────────────────────────────
12419            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
12420            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
12421            | "connected_components_graph" | "ccgraph"
12422            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
12423            // ── Extended stdlib: Data Validation ─────────────────────────────
12424            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
12425            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
12426            | "is_hostname_valid" | "ishost"
12427            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
12428            | "is_iso_datetime" | "isisodtm"
12429            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
12430            // ── Extended stdlib: String Utilities Novel ──────────────────────
12431            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
12432            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
12433            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
12434            | "find_all_indices" | "fndalli"
12435            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
12436            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
12437            // ── Extended stdlib: Math Novel ──────────────────────────────────
12438            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
12439            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
12440            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
12441            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
12442            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
12443            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
12444            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
12445            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
12446            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
12447            | "longest_run" | "lrun" | "longest_increasing" | "linc"
12448            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
12449            | "majority_element" | "majority" | "kth_largest" | "kthl"
12450            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
12451            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
12452            // ── Extended stdlib batch 3: Set Operations ──────────────────────
12453            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
12454            | "overlap_coefficient" | "overlapcoef"
12455            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
12456            // ── Extended stdlib batch 3: Advanced String ─────────────────────
12457            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
12458            | "hamdist" | "jaro_similarity" | "jarosim"
12459            | "longest_common_substring" | "lcsub"
12460            | "longest_common_subsequence" | "lcseq"
12461            | "count_words" | "wcount" | "count_lines" | "lcount"
12462            | "count_chars" | "ccount" | "count_bytes" | "bcount"
12463            // ── Extended stdlib batch 3: More Math ───────────────────────────
12464            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
12465            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
12466            | "mobius" | "mob" | "is_squarefree" | "issqfr"
12467            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
12468            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
12469            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
12470            | "day_of_year" | "doy" | "week_of_year" | "woy"
12471            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
12472            | "age_in_years" | "ageyrs"
12473            // ── functional combinators ──────────────────────────────────────
12474
12475            | "when_true" | "when_false" | "if_else" | "clamp_fn"
12476            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
12477            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
12478            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
12479            | "coalesce" | "default_to" | "fallback"
12480            | "apply_list" | "zip_apply" | "scan"
12481            | "keep_if" | "reject_if" | "group_consecutive"
12482            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
12483
12484            // ── matrix / linear algebra ─────────────────────────────────────
12485
12486
12487            | "matrix_multiply" | "mat_mul"
12488            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
12489
12490
12491
12492            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
12493            | "linspace" | "arange"
12494            // ── more regex ──────────────────────────────────────────────────
12495            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
12496            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
12497            // ── more process / system ───────────────────────────────────────
12498            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
12499            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
12500            // ── data structure helpers ───────────────────────────────────────
12501            | "stack_new" | "queue_new" | "lru_new"
12502            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
12503            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
12504            // ── trivial numeric helpers (batch 4) ─────────────────────────────
12505            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
12506            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
12507            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
12508            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
12509            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
12510            // ── math / physics constants ──────────────────────────────────────
12511            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
12512            | "planck" | "speed_of_light" | "sqrt2"
12513            // ── physics formulas ──────────────────────────────────────────────
12514            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
12515            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
12516            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
12517            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
12518            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
12519            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
12520            // ── math functions ────────────────────────────────────────────────
12521            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
12522            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
12523            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
12524            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
12525            | "sigmoid" | "signum" | "square_root"
12526            // ── sequences ─────────────────────────────────────────────────────
12527            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
12528            | "squares_seq" | "triangular_seq"
12529            // ── string helpers (batch 4) ──────────────────────────────────────
12530            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
12531            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
12532            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
12533            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
12534            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
12535            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
12536            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
12537            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
12538            | "xor_strings"
12539            // ── list helpers (batch 4) ─────────────────────────────────────────
12540            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
12541            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
12542            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
12543            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
12544            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
12545            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
12546            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
12547            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
12548            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
12549            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
12550            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
12551            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
12552            | "wrap_index" | "digits_of"
12553            // ── predicates (batch 4) ──────────────────────────────────────────
12554            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
12555            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
12556            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
12557            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
12558            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
12559            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
12560            // ── counters (batch 4) ────────────────────────────────────────────
12561            | "count_digits" | "count_letters" | "count_lower" | "count_match"
12562            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
12563            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
12564            | "truthy_count" | "undef_count"
12565            // ── conversion / utility (batch 4) ────────────────────────────────
12566            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
12567            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
12568            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
12569            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
12570            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
12571            | "range_exclusive" | "range_inclusive"
12572            // ── math / numeric (uncategorized batch) ────────────────────────────
12573            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
12574            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
12575            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
12576            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
12577            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
12578            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
12579            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
12580            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
12581            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
12582            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
12583            | "tribonacci" | "weighted_mean" | "winsorize"
12584            // ── statistics (extended) ─────────────────────────────────────────
12585            | "chi_square_stat" | "describe" | "five_number_summary"
12586            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
12587            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
12588            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
12589            | "z_score" | "z_scores"
12590            // ── number theory / primes ──────────────────────────────────────────
12591            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
12592            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
12593            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
12594            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
12595            // ── geometry / physics ──────────────────────────────────────────────
12596            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
12597            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
12598            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
12599            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
12600            // ── geometry (extended) ───────────────────────────────────────────
12601            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
12602            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
12603            | "frustum_volume" | "haversine_distance" | "line_intersection"
12604            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
12605            | "reflect_point" | "scale_point" | "sector_area"
12606            | "torus_surface" | "torus_volume" | "translate_point"
12607            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
12608            // ── constants ───────────────────────────────────────────────────────
12609            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
12610            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
12611            | "sol" | "tau"
12612            // ── finance ─────────────────────────────────────────────────────────
12613            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
12614            // ── finance (extended) ────────────────────────────────────────────
12615            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
12616            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
12617            | "discounted_payback" | "duration" | "irr"
12618            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
12619            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
12620            | "wacc" | "xirr"
12621            // ── string processing (uncategorized batch) ─────────────────────────
12622            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
12623            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
12624            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
12625            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
12626            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
12627            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
12628            // ── encoding / phonetics ────────────────────────────────────────────
12629            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
12630            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
12631            | "to_emoji_num"
12632            // ── roman numerals ──────────────────────────────────────────────────
12633            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
12634            // ── base / gray code ────────────────────────────────────────────────
12635            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
12636            // ── color operations ────────────────────────────────────────────────
12637            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
12638            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
12639            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
12640            | "rgb_to_hsl" | "rgb_to_hsv"
12641            // ── matrix operations (uncategorized batch) ─────────────────────────
12642            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
12643            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
12644            | "matrix_transpose"
12645            // ── array / list operations (uncategorized batch) ───────────────────
12646            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
12647            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
12648            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
12649            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
12650            | "zero_crossings"
12651            // ── DSP / signal (extended) ───────────────────────────────────────
12652            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
12653            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
12654            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
12655            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
12656            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
12657            // ── validation predicates (uncategorized batch) ─────────────────────
12658            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
12659            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
12660            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
12661            // ── algorithms / puzzles ────────────────────────────────────────────
12662            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
12663            | "sierpinski" | "tower_of_hanoi" | "truth_table"
12664            // ── misc / utility ──────────────────────────────────────────────────
12665            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
12666            // ── math formulas ───────────────────────────────────────────────────
12667            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
12668            | "geometric_series" | "stirling_approx"
12669            | "double_factorial" | "rising_factorial" | "falling_factorial"
12670            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
12671            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
12672            | "map_range"
12673            // ── physics formulas ────────────────────────────────────────────────
12674            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
12675            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
12676            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
12677            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
12678            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
12679            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
12680            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
12681            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
12682            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
12683            | "projectile_range" | "projectile_max_height" | "projectile_time"
12684            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
12685            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
12686            | "lens_power" | "thin_lens" | "magnification_lens"
12687            // ── math constants ──────────────────────────────────────────────────
12688            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
12689            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
12690            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
12691            // ── physics constants ───────────────────────────────────────────────
12692            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
12693            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
12694            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
12695            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
12696            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
12697            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
12698            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
12699            // ── linear algebra (extended) ──────────────────────────────────
12700            | "matrix_solve" | "msolve" | "solve"
12701            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
12702            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
12703            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
12704            | "matrix_pinv" | "mpinv" | "pinv"
12705            | "matrix_cholesky" | "mchol" | "cholesky"
12706            | "matrix_det_general" | "mdetg" | "det"
12707            // ── statistics tests (extended) ────────────────────────────────
12708            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
12709            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
12710            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
12711            | "confidence_interval" | "ci"
12712            // ── distributions (extended) ──────────────────────────────────
12713            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
12714            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
12715            | "t_pdf" | "tpdf" | "student_pdf"
12716            | "f_pdf" | "fpdf" | "fisher_pdf"
12717            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
12718            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
12719            | "pareto_pdf" | "paretopdf"
12720            // ── interpolation & curve fitting ─────────────────────────────
12721            | "lagrange_interp" | "lagrange" | "linterp"
12722            | "cubic_spline" | "cspline" | "spline"
12723            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
12724            // ── numerical integration & differentiation ───────────────────
12725            | "trapz" | "trapezoid" | "simpson" | "simps"
12726            | "numerical_diff" | "numdiff" | "diff_array"
12727            | "cumtrapz" | "cumulative_trapz"
12728            // ── optimization / root finding ────────────────────────────────
12729            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
12730            | "golden_section" | "golden" | "gss"
12731            // ── ODE solvers ───────────────────────────────────────────────
12732            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
12733            // ── graph algorithms (extended) ────────────────────────────────
12734            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
12735            | "floyd_warshall" | "floydwarshall" | "apsp"
12736            | "prim_mst" | "mst" | "prim"
12737            // ── trig extensions ───────────────────────────────────────────
12738            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
12739            // ── ML activation functions ───────────────────────────────────
12740            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
12741            | "silu" | "swish" | "mish" | "softplus"
12742            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
12743            // ── special functions ─────────────────────────────────────────
12744            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
12745            | "lambert_w" | "lambertw" | "productlog"
12746            // ── number theory (extended) ──────────────────────────────────
12747            | "mod_exp" | "modexp" | "powmod"
12748            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
12749            | "miller_rabin" | "millerrabin" | "is_probable_prime"
12750            // ── combinatorics (extended) ──────────────────────────────────
12751            | "derangements" | "stirling2" | "stirling_second"
12752            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
12753            // ── physics (new) ─────────────────────────────────────────────
12754            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
12755            // ── financial greeks & risk ───────────────────────────────────
12756            | "bs_delta" | "bsdelta" | "option_delta"
12757            | "bs_gamma" | "bsgamma" | "option_gamma"
12758            | "bs_vega" | "bsvega" | "option_vega"
12759            | "bs_theta" | "bstheta" | "option_theta"
12760            | "bs_rho" | "bsrho" | "option_rho"
12761            | "bond_duration" | "mac_duration"
12762            // ── DSP extensions ────────────────────────────────────────────
12763            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
12764            // ── encoding extensions ───────────────────────────────────────
12765            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
12766            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
12767            // ── R base: distributions ─────────────────────────────────────
12768            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
12769            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
12770            // ── R base: matrix ops ────────────────────────────────────────
12771            | "rbind" | "cbind"
12772            | "row_sums" | "rowSums" | "col_sums" | "colSums"
12773            | "row_means" | "rowMeans" | "col_means" | "colMeans"
12774            | "outer_product" | "outer" | "crossprod" | "tcrossprod"
12775            | "nrow" | "ncol" | "prop_table" | "proptable"
12776            // ── R base: vector ops ────────────────────────────────────────
12777            | "cummax" | "cummin" | "scale_vec" | "scale"
12778            | "which_fn" | "tabulate"
12779            | "duplicated" | "duped" | "rev_vec"
12780            | "seq_fn" | "rep_fn" | "rep"
12781            | "cut_bins" | "cut" | "find_interval" | "findInterval"
12782            | "ecdf_fn" | "ecdf" | "density_est" | "density"
12783            | "embed_ts" | "embed"
12784            // ── R base: stats tests ───────────────────────────────────────
12785            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
12786            | "wilcox_test" | "wilcox" | "mann_whitney"
12787            | "prop_test" | "proptest" | "binom_test" | "binomtest"
12788            // ── R base: apply / functional ────────────────────────────────
12789            | "sapply" | "tapply" | "do_call" | "docall"
12790            // ── R base: ML / clustering ───────────────────────────────────
12791            | "kmeans" | "prcomp" | "pca"
12792            // ── R base: random generators ─────────────────────────────────
12793            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
12794            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
12795            | "rweibull" | "rlnorm" | "rcauchy"
12796            // ── R base: quantile functions ────────────────────────────────
12797            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
12798            // ── R base: additional CDFs ───────────────────────────────────
12799            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
12800            // ── R base: additional PMFs ───────────────────────────────────
12801            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
12802            // ── R base: smoothing / interpolation ─────────────────────────
12803            | "lowess" | "loess" | "approx_fn" | "approx"
12804            // ── R base: linear models ─────────────────────────────────────
12805            | "lm_fit" | "lm"
12806            // ── R base: remaining quantiles ───────────────────────────────
12807            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
12808            | "qbinom" | "qpois"
12809            // ── R base: time series ───────────────────────────────────────
12810            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
12811            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
12812            // ── R base: regression diagnostics ────────────────────────────
12813            | "predict_lm" | "predict" | "confint_lm" | "confint"
12814            // ── R base: multivariate stats ────────────────────────────────
12815            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
12816            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
12817            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
12818            // ── SVG plotting ──────────────────────────────────────────────
12819            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
12820            | "plot_svg" | "hist_svg" | "histogram_svg"
12821            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
12822            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
12823            | "donut_svg" | "donut" | "area_svg" | "area_chart"
12824            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
12825            | "candlestick_svg" | "candlestick" | "ohlc"
12826            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
12827            | "stacked_bar_svg" | "stacked_bar"
12828            | "wordcloud_svg" | "wordcloud" | "wcloud"
12829            | "treemap_svg" | "treemap"
12830            | "pvw"
12831            // ── Cyberpunk terminal art ────────────────────────────────
12832            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
12833            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
12834            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
12835            => Some(name),
12836            _ => None,
12837        }
12838    }
12839
12840    /// Reserved hash names that cannot be shadowed by user declarations.
12841    /// These are stryke's reflection hashes populated from builtins metadata.
12842    fn is_reserved_hash_name(name: &str) -> bool {
12843        matches!(
12844            name,
12845            "b" | "pc"
12846                | "e"
12847                | "a"
12848                | "d"
12849                | "c"
12850                | "p"
12851                | "all"
12852                | "stryke::builtins"
12853                | "stryke::perl_compats"
12854                | "stryke::extensions"
12855                | "stryke::aliases"
12856                | "stryke::descriptions"
12857                | "stryke::categories"
12858                | "stryke::primaries"
12859                | "stryke::all"
12860        )
12861    }
12862
12863    /// Check if a UDF name shadows a stryke builtin and error if so.
12864    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
12865    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> PerlResult<()> {
12866        if Self::is_known_bareword(name) || Self::is_try_builtin_name(name) {
12867            return Err(self.syntax_err(
12868                format!(
12869"`{name}` is a stryke builtin and cannot be redefined (this is not Perl 5; use `fn` not `sub`, or pass --compat)"
12870                ),
12871                line,
12872            ));
12873        }
12874        Ok(())
12875    }
12876
12877    /// Check if a hash name shadows a reserved stryke hash and error if so.
12878    /// Called only in non-compat mode.
12879    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> PerlResult<()> {
12880        if Self::is_reserved_hash_name(name) {
12881            return Err(self.syntax_err(
12882                format!(
12883"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
12884                ),
12885                line,
12886            ));
12887        }
12888        Ok(())
12889    }
12890
12891    /// Validate assignment to %hash in non-compat mode.
12892    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
12893    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
12894        match &value.kind {
12895            ExprKind::Integer(_) | ExprKind::Float(_) => {
12896                return Err(self.syntax_err(
12897                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
12898                    line,
12899                ));
12900            }
12901            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
12902                return Err(self.syntax_err(
12903                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
12904                    line,
12905                ));
12906            }
12907            ExprKind::ArrayRef(_) => {
12908                return Err(self.syntax_err(
12909                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
12910                    line,
12911                ));
12912            }
12913            ExprKind::ScalarRef(inner) => {
12914                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
12915                    return Err(self.syntax_err(
12916                        "cannot assign \\@array to hash — use %h = @array for even-length list",
12917                        line,
12918                    ));
12919                }
12920                if matches!(inner.kind, ExprKind::HashVar(_)) {
12921                    return Err(self.syntax_err(
12922                        "cannot assign \\%hash to hash — use %h = %other directly",
12923                        line,
12924                    ));
12925                }
12926            }
12927            ExprKind::HashRef(_) => {
12928                return Err(self.syntax_err(
12929                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
12930                    line,
12931                ));
12932            }
12933            ExprKind::CodeRef { .. } => {
12934                return Err(self.syntax_err("cannot assign coderef to hash", line));
12935            }
12936            ExprKind::Undef => {
12937                return Err(
12938                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
12939                );
12940            }
12941            ExprKind::List(items)
12942                if items.len() % 2 != 0
12943                    && !items.iter().any(|e| {
12944                        matches!(
12945                            e.kind,
12946                            ExprKind::ArrayVar(_)
12947                                | ExprKind::HashVar(_)
12948                                | ExprKind::FuncCall { .. }
12949                                | ExprKind::Deref { .. }
12950                                | ExprKind::ScalarVar(_)
12951                        )
12952                    }) =>
12953            {
12954                return Err(self.syntax_err(
12955                        format!(
12956                            "odd-length list ({} elements) in hash assignment — missing value for last key",
12957                            items.len()
12958                        ),
12959                        line,
12960                    ));
12961            }
12962            _ => {}
12963        }
12964        Ok(())
12965    }
12966
12967    /// Validate assignment to @array in non-compat mode.
12968    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
12969    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
12970    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
12971    fn validate_array_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
12972        if let ExprKind::Undef = &value.kind {
12973            return Err(
12974                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
12975            );
12976        }
12977        Ok(())
12978    }
12979
12980    /// Validate assignment to $scalar in non-compat mode.
12981    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
12982    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
12983        if let ExprKind::List(items) = &value.kind {
12984            if items.len() > 1 {
12985                return Err(self.syntax_err(
12986                    format!(
12987                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
12988                        items.len()
12989                    ),
12990                    line,
12991                ));
12992            }
12993        }
12994        Ok(())
12995    }
12996
12997    /// Validate an assignment based on target type (in non-compat mode only).
12998    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> PerlResult<()> {
12999        if crate::compat_mode() {
13000            return Ok(());
13001        }
13002        match &target.kind {
13003            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
13004            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
13005            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
13006            _ => Ok(()),
13007        }
13008    }
13009
13010    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
13011    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
13012    /// Also accepts a bare function name: `psort my_cmp, @list`.
13013    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
13014        if matches!(self.peek(), Token::LBrace) {
13015            return self.parse_block();
13016        }
13017        let line = self.peek_line();
13018        // Bare sub name: `psort my_cmp, @list`
13019        if let Token::Ident(ref name) = self.peek().clone() {
13020            if matches!(
13021                self.peek_at(1),
13022                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13023            ) {
13024                let name = name.clone();
13025                self.advance();
13026                let body = Expr {
13027                    kind: ExprKind::FuncCall {
13028                        name,
13029                        args: vec![
13030                            Expr {
13031                                kind: ExprKind::ScalarVar("a".to_string()),
13032                                line,
13033                            },
13034                            Expr {
13035                                kind: ExprKind::ScalarVar("b".to_string()),
13036                                line,
13037                            },
13038                        ],
13039                    },
13040                    line,
13041                };
13042                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13043            }
13044        }
13045        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
13046        let expr = self.parse_assign_expr_stop_at_pipe()?;
13047        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13048    }
13049
13050    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
13051    fn parse_fan_optional_progress(
13052        &mut self,
13053        which: &'static str,
13054    ) -> PerlResult<Option<Box<Expr>>> {
13055        let line = self.peek_line();
13056        if self.eat(&Token::Comma) {
13057            match self.peek() {
13058                Token::Ident(ref kw)
13059                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
13060                {
13061                    self.advance();
13062                    self.expect(&Token::FatArrow)?;
13063                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
13064                }
13065                _ => {
13066                    return Err(self.syntax_err(
13067                        format!("{which}: expected `progress => EXPR` after comma"),
13068                        line,
13069                    ));
13070                }
13071            }
13072        }
13073        if let Token::Ident(ref kw) = self.peek().clone() {
13074            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13075                self.advance();
13076                self.expect(&Token::FatArrow)?;
13077                return Ok(Some(Box::new(self.parse_assign_expr()?)));
13078            }
13079        }
13080        Ok(None)
13081    }
13082
13083    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
13084    /// (for `pmap_chunked`, `psort`, etc.).
13085    ///
13086    /// Paren-less — individual parts parse through
13087    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
13088    /// the enclosing pipe-forward loop (left-associative chaining).
13089    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
13090        // On the RHS of `|>`, list-taking builtins may be written bare with no
13091        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
13092        // When the next token is a list-terminator, yield an empty placeholder
13093        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
13094        // desugar time, so the placeholder is never evaluated.
13095        if self.in_pipe_rhs()
13096            && matches!(
13097                self.peek(),
13098                Token::Semicolon
13099                    | Token::RBrace
13100                    | Token::RParen
13101                    | Token::Eof
13102                    | Token::PipeForward
13103                    | Token::Comma
13104            )
13105        {
13106            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
13107        }
13108        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13109        loop {
13110            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13111                break;
13112            }
13113            if matches!(
13114                self.peek(),
13115                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13116            ) {
13117                break;
13118            }
13119            if self.peek_is_postfix_stmt_modifier_keyword() {
13120                break;
13121            }
13122            if let Token::Ident(ref kw) = self.peek().clone() {
13123                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13124                    self.advance();
13125                    self.expect(&Token::FatArrow)?;
13126                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13127                    return Ok((merge_expr_list(parts), Some(prog)));
13128                }
13129            }
13130            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13131        }
13132        Ok((merge_expr_list(parts), None))
13133    }
13134
13135    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
13136        if matches!(self.peek(), Token::LParen) {
13137            self.advance();
13138            let expr = self.parse_expression()?;
13139            self.expect(&Token::RParen)?;
13140            Ok(expr)
13141        } else {
13142            self.parse_assign_expr_stop_at_pipe()
13143        }
13144    }
13145
13146    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
13147        // Default to `$_` when the next token cannot start an argument expression
13148        // because it has lower precedence than a named unary operator. Perl 5
13149        // named unary precedence sits above ternary / comparison / logical / bitwise
13150        // / assignment / list ops; everything below should terminate the implicit
13151        // argument and let the surrounding expression continue.
13152        // See `perldoc perlop` ("Named Unary Operators").
13153        if matches!(
13154            self.peek(),
13155            // Statement / list / call boundaries
13156            Token::Semicolon
13157                | Token::RBrace
13158                | Token::RParen
13159                | Token::RBracket
13160                | Token::Eof
13161                | Token::Comma
13162                | Token::FatArrow
13163                | Token::PipeForward
13164            // Ternary `? :`
13165                | Token::Question
13166                | Token::Colon
13167            // Comparison / equality (numeric + string)
13168                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
13169                | Token::NumLe | Token::NumGe | Token::Spaceship
13170                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
13171                | Token::StrLe | Token::StrGe | Token::StrCmp
13172            // Logical (symbolic and word forms) + defined-or
13173                | Token::LogAnd | Token::LogOr | Token::LogNot
13174                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
13175                | Token::DefinedOr
13176            // Range (lower precedence than named unary)
13177                | Token::Range | Token::RangeExclusive
13178            // Assignment (any compound form)
13179                | Token::Assign | Token::PlusAssign | Token::MinusAssign
13180                | Token::MulAssign | Token::DivAssign | Token::ModAssign
13181                | Token::PowAssign | Token::DotAssign | Token::AndAssign
13182                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
13183                | Token::ShiftLeftAssign | Token::ShiftRightAssign
13184                | Token::BitAndAssign | Token::BitOrAssign
13185        ) {
13186            return Ok(Expr {
13187                kind: ExprKind::ScalarVar("_".into()),
13188                line: self.peek_line(),
13189            });
13190        }
13191        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
13192        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
13193        // Perl accepts both `length` and `length()` as `length($_)`.
13194        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
13195            let line = self.peek_line();
13196            self.advance(); // (
13197            self.advance(); // )
13198            return Ok(Expr {
13199                kind: ExprKind::ScalarVar("_".into()),
13200                line,
13201            });
13202        }
13203        self.parse_one_arg()
13204    }
13205
13206    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
13207    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
13208        let line = self.prev_line(); // line where shift/pop keyword was
13209        if matches!(self.peek(), Token::LParen) {
13210            self.advance();
13211            if matches!(self.peek(), Token::RParen) {
13212                self.advance();
13213                return Ok(Expr {
13214                    kind: ExprKind::ArrayVar("_".into()),
13215                    line: self.peek_line(),
13216                });
13217            }
13218            let expr = self.parse_expression()?;
13219            self.expect(&Token::RParen)?;
13220            return Ok(expr);
13221        }
13222        // Implicit semicolon: if next token is on a different line, don't consume it
13223        if matches!(
13224            self.peek(),
13225            Token::Semicolon
13226                | Token::RBrace
13227                | Token::RParen
13228                | Token::Eof
13229                | Token::Comma
13230                | Token::PipeForward
13231        ) || self.peek_line() > line
13232        {
13233            Ok(Expr {
13234                kind: ExprKind::ArrayVar("_".into()),
13235                line,
13236            })
13237        } else {
13238            self.parse_assign_expr()
13239        }
13240    }
13241
13242    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
13243        if matches!(self.peek(), Token::LParen) {
13244            self.advance();
13245            let args = self.parse_arg_list()?;
13246            self.expect(&Token::RParen)?;
13247            Ok(args)
13248        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
13249            // In thread context, don't consume barewords as arguments
13250            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
13251            Ok(vec![])
13252        } else {
13253            self.parse_list_until_terminator()
13254        }
13255    }
13256
13257    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
13258    /// should be treated as an auto-quoted string (hash key), not a function call.
13259    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
13260    #[inline]
13261    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
13262        if matches!(self.peek(), Token::FatArrow) {
13263            Some(Expr {
13264                kind: ExprKind::String(name.to_string()),
13265                line,
13266            })
13267        } else {
13268            None
13269        }
13270    }
13271
13272    /// Parse a hash subscript key inside `{…}`.
13273    ///
13274    /// Perl auto-quotes a single bareword before `}`, even for keywords:
13275    /// `$h{print}`, `$r->{f}` etc. all yield the string key.
13276    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
13277        let line = self.peek_line();
13278        if let Token::Ident(ref k) = self.peek().clone() {
13279            if matches!(self.peek_at(1), Token::RBrace) {
13280                let s = k.clone();
13281                self.advance();
13282                return Ok(Expr {
13283                    kind: ExprKind::String(s),
13284                    line,
13285                });
13286            }
13287        }
13288        self.parse_expression()
13289    }
13290
13291    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
13292    #[inline]
13293    fn peek_is_glob_par_progress_kw(&self) -> bool {
13294        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
13295            && matches!(self.peek_at(1), Token::FatArrow)
13296    }
13297
13298    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
13299    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
13300        let mut args = Vec::new();
13301        loop {
13302            if matches!(self.peek(), Token::RParen | Token::Eof) {
13303                break;
13304            }
13305            if self.peek_is_glob_par_progress_kw() {
13306                break;
13307            }
13308            args.push(self.parse_assign_expr()?);
13309            match self.peek() {
13310                Token::RParen => break,
13311                Token::Comma => {
13312                    self.advance();
13313                    if matches!(self.peek(), Token::RParen) {
13314                        break;
13315                    }
13316                    if self.peek_is_glob_par_progress_kw() {
13317                        break;
13318                    }
13319                }
13320                _ => {
13321                    return Err(self.syntax_err(
13322                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
13323                        self.peek_line(),
13324                    ));
13325                }
13326            }
13327        }
13328        Ok(args)
13329    }
13330
13331    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
13332    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
13333        let mut args = Vec::new();
13334        loop {
13335            if matches!(
13336                self.peek(),
13337                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13338            ) {
13339                break;
13340            }
13341            if self.peek_is_postfix_stmt_modifier_keyword() {
13342                break;
13343            }
13344            if self.peek_is_glob_par_progress_kw() {
13345                break;
13346            }
13347            args.push(self.parse_assign_expr()?);
13348            if !self.eat(&Token::Comma) {
13349                break;
13350            }
13351            if self.peek_is_glob_par_progress_kw() {
13352                break;
13353            }
13354        }
13355        Ok(args)
13356    }
13357
13358    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
13359    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
13360        if matches!(self.peek(), Token::LParen) {
13361            self.advance();
13362            let args = self.parse_pattern_list_until_rparen_or_progress()?;
13363            let progress = if self.peek_is_glob_par_progress_kw() {
13364                self.advance();
13365                self.expect(&Token::FatArrow)?;
13366                Some(Box::new(self.parse_assign_expr()?))
13367            } else {
13368                None
13369            };
13370            self.expect(&Token::RParen)?;
13371            Ok((args, progress))
13372        } else {
13373            let args = self.parse_pattern_list_glob_par_bare()?;
13374            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
13375            let progress = if self.peek_is_glob_par_progress_kw() {
13376                self.advance();
13377                self.expect(&Token::FatArrow)?;
13378                Some(Box::new(self.parse_assign_expr()?))
13379            } else {
13380                None
13381            };
13382            Ok((args, progress))
13383        }
13384    }
13385
13386    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
13387        let mut args = Vec::new();
13388        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
13389        // so shadow any outer paren-less-arg suppression from
13390        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
13391        let saved_no_pf = self.no_pipe_forward_depth;
13392        self.no_pipe_forward_depth = 0;
13393        while !matches!(
13394            self.peek(),
13395            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
13396        ) {
13397            let arg = match self.parse_assign_expr() {
13398                Ok(e) => e,
13399                Err(err) => {
13400                    self.no_pipe_forward_depth = saved_no_pf;
13401                    return Err(err);
13402                }
13403            };
13404            args.push(arg);
13405            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13406                break;
13407            }
13408        }
13409        self.no_pipe_forward_depth = saved_no_pf;
13410        Ok(args)
13411    }
13412
13413    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
13414    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
13415    /// no-arg method call; we must not consume that `+` as the start of a first argument.
13416    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
13417        let mut args = Vec::new();
13418        let call_line = self.prev_line();
13419        loop {
13420            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
13421            // hash argument to `next` (paren-less method call has no args here).
13422            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
13423                break;
13424            }
13425            if matches!(
13426                self.peek(),
13427                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13428            ) {
13429                break;
13430            }
13431            if let Token::Ident(ref kw) = self.peek().clone() {
13432                if matches!(
13433                    kw.as_str(),
13434                    "if" | "unless" | "while" | "until" | "for" | "foreach"
13435                ) {
13436                    break;
13437                }
13438            }
13439            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
13440            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
13441            if args.is_empty()
13442                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
13443            {
13444                break;
13445            }
13446            // Implicit semicolon: if no args collected yet and next token is on a different
13447            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
13448            if args.is_empty() && self.peek_line() > call_line {
13449                break;
13450            }
13451            args.push(self.parse_assign_expr()?);
13452            if !self.eat(&Token::Comma) {
13453                break;
13454            }
13455        }
13456        Ok(args)
13457    }
13458
13459    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
13460    /// the whole `->meth` expression).
13461    fn peek_method_arg_infix_terminator(&self) -> bool {
13462        matches!(
13463            self.peek(),
13464            Token::Plus
13465                | Token::Minus
13466                | Token::Star
13467                | Token::Slash
13468                | Token::Percent
13469                | Token::Power
13470                | Token::Dot
13471                | Token::X
13472                | Token::NumEq
13473                | Token::NumNe
13474                | Token::NumLt
13475                | Token::NumGt
13476                | Token::NumLe
13477                | Token::NumGe
13478                | Token::Spaceship
13479                | Token::StrEq
13480                | Token::StrNe
13481                | Token::StrLt
13482                | Token::StrGt
13483                | Token::StrLe
13484                | Token::StrGe
13485                | Token::StrCmp
13486                | Token::LogAnd
13487                | Token::LogOr
13488                | Token::LogAndWord
13489                | Token::LogOrWord
13490                | Token::DefinedOr
13491                | Token::BitAnd
13492                | Token::BitOr
13493                | Token::BitXor
13494                | Token::ShiftLeft
13495                | Token::ShiftRight
13496                | Token::Range
13497                | Token::RangeExclusive
13498                | Token::BindMatch
13499                | Token::BindNotMatch
13500                | Token::Arrow
13501                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
13502                | Token::Question
13503                | Token::Colon
13504                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
13505                | Token::Assign
13506                | Token::PlusAssign
13507                | Token::MinusAssign
13508                | Token::MulAssign
13509                | Token::DivAssign
13510                | Token::ModAssign
13511                | Token::PowAssign
13512                | Token::DotAssign
13513                | Token::AndAssign
13514                | Token::OrAssign
13515                | Token::XorAssign
13516                | Token::DefinedOrAssign
13517                | Token::ShiftLeftAssign
13518                | Token::ShiftRightAssign
13519                | Token::BitAndAssign
13520                | Token::BitOrAssign
13521        )
13522    }
13523
13524    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
13525        let mut args = Vec::new();
13526        // Line of the last consumed token (the keyword / function name that
13527        // triggered this arg parse).  Used for implicit-semicolon: if no args
13528        // have been parsed yet and the next token is on a *different* line,
13529        // treat the newline as a statement boundary and stop.
13530        let call_line = self.prev_line();
13531        loop {
13532            if matches!(
13533                self.peek(),
13534                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13535            ) {
13536                break;
13537            }
13538            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
13539            if let Token::Ident(ref kw) = self.peek().clone() {
13540                if matches!(
13541                    kw.as_str(),
13542                    "if" | "unless" | "while" | "until" | "for" | "foreach"
13543                ) {
13544                    break;
13545                }
13546            }
13547            // Implicit semicolons: if no args have been collected yet and the
13548            // next token is on a different line from the call keyword, treat
13549            // the newline as a statement boundary.  This prevents paren-less
13550            // calls (`say`, `print`, user subs) from greedily swallowing the
13551            // *next* statement when the author omitted a semicolon.
13552            // After a comma continuation, multi-line arg lists still work.
13553            if args.is_empty() && self.peek_line() > call_line {
13554                break;
13555            }
13556            // Paren-less builtin args: `|>` terminates the whole call list, so
13557            // individual args must not absorb a following `|>`.
13558            args.push(self.parse_assign_expr_stop_at_pipe()?);
13559            if !self.eat(&Token::Comma) {
13560                break;
13561            }
13562        }
13563        Ok(args)
13564    }
13565
13566    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
13567        let mut pairs = Vec::new();
13568        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
13569            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
13570            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
13571            let line = self.peek_line();
13572            let key = if let Token::Ident(ref name) = self.peek().clone() {
13573                if matches!(self.peek_at(1), Token::FatArrow) {
13574                    self.advance();
13575                    Expr {
13576                        kind: ExprKind::String(name.clone()),
13577                        line,
13578                    }
13579                } else {
13580                    self.parse_assign_expr()?
13581                }
13582            } else {
13583                self.parse_assign_expr()?
13584            };
13585            // If the key expression is a hash/array variable and is followed by `}` or `,`
13586            // with no `=>`, treat the whole thing as a hash-from-expression construction.
13587            // This handles `{ %a }`, `{ %a, key => val }`, etc.
13588            if matches!(self.peek(), Token::RBrace | Token::Comma)
13589                && matches!(
13590                    key.kind,
13591                    ExprKind::HashVar(_)
13592                        | ExprKind::Deref {
13593                            kind: Sigil::Hash,
13594                            ..
13595                        }
13596                )
13597            {
13598                // Synthesize a pair whose key/value is spread from the hash expression.
13599                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
13600                // The evaluator will flatten this.
13601                let sentinel_key = Expr {
13602                    kind: ExprKind::String("__HASH_SPREAD__".into()),
13603                    line,
13604                };
13605                pairs.push((sentinel_key, key));
13606                self.eat(&Token::Comma);
13607                continue;
13608            }
13609            // Expect => or , after key
13610            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
13611                let val = self.parse_assign_expr()?;
13612                pairs.push((key, val));
13613                self.eat(&Token::Comma);
13614            } else {
13615                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
13616            }
13617        }
13618        self.expect(&Token::RBrace)?;
13619        Ok(pairs)
13620    }
13621
13622    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
13623    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
13624    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
13625    /// navigates. Caller expects and consumes `term` itself.
13626    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
13627        let mut pairs = Vec::new();
13628        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
13629            && !matches!(self.peek(), Token::Eof)
13630        {
13631            let line = self.peek_line();
13632            let key = if let Token::Ident(ref name) = self.peek().clone() {
13633                if matches!(self.peek_at(1), Token::FatArrow) {
13634                    self.advance();
13635                    Expr {
13636                        kind: ExprKind::String(name.clone()),
13637                        line,
13638                    }
13639                } else {
13640                    self.parse_assign_expr()?
13641                }
13642            } else {
13643                self.parse_assign_expr()?
13644            };
13645            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
13646                let val = self.parse_assign_expr()?;
13647                pairs.push((key, val));
13648                self.eat(&Token::Comma);
13649            } else {
13650                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
13651            }
13652        }
13653        Ok(pairs)
13654    }
13655
13656    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
13657    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
13658    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
13659    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
13660    /// Each step wraps the current expression in an `ArrowDeref`.
13661    fn interp_chain_subscripts(
13662        &self,
13663        chars: &[char],
13664        i: &mut usize,
13665        mut base: Expr,
13666        line: usize,
13667    ) -> Expr {
13668        loop {
13669            // Optional `->` connector
13670            let (after, requires_subscript) =
13671                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
13672                    (*i + 2, true)
13673                } else {
13674                    (*i, false)
13675                };
13676            if after >= chars.len() {
13677                break;
13678            }
13679            match chars[after] {
13680                '[' => {
13681                    *i = after + 1;
13682                    let mut idx_str = String::new();
13683                    while *i < chars.len() && chars[*i] != ']' {
13684                        idx_str.push(chars[*i]);
13685                        *i += 1;
13686                    }
13687                    if *i < chars.len() {
13688                        *i += 1;
13689                    }
13690                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
13691                        Expr {
13692                            kind: ExprKind::ScalarVar(rest.to_string()),
13693                            line,
13694                        }
13695                    } else if let Ok(n) = idx_str.parse::<i64>() {
13696                        Expr {
13697                            kind: ExprKind::Integer(n),
13698                            line,
13699                        }
13700                    } else {
13701                        Expr {
13702                            kind: ExprKind::String(idx_str),
13703                            line,
13704                        }
13705                    };
13706                    base = Expr {
13707                        kind: ExprKind::ArrowDeref {
13708                            expr: Box::new(base),
13709                            index: Box::new(idx_expr),
13710                            kind: DerefKind::Array,
13711                        },
13712                        line,
13713                    };
13714                }
13715                '{' => {
13716                    *i = after + 1;
13717                    let mut key = String::new();
13718                    let mut depth = 1usize;
13719                    while *i < chars.len() && depth > 0 {
13720                        if chars[*i] == '{' {
13721                            depth += 1;
13722                        } else if chars[*i] == '}' {
13723                            depth -= 1;
13724                            if depth == 0 {
13725                                break;
13726                            }
13727                        }
13728                        key.push(chars[*i]);
13729                        *i += 1;
13730                    }
13731                    if *i < chars.len() {
13732                        *i += 1;
13733                    }
13734                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
13735                        Expr {
13736                            kind: ExprKind::ScalarVar(rest.to_string()),
13737                            line,
13738                        }
13739                    } else {
13740                        Expr {
13741                            kind: ExprKind::String(key),
13742                            line,
13743                        }
13744                    };
13745                    base = Expr {
13746                        kind: ExprKind::ArrowDeref {
13747                            expr: Box::new(base),
13748                            index: Box::new(key_expr),
13749                            kind: DerefKind::Hash,
13750                        },
13751                        line,
13752                    };
13753                }
13754                _ => {
13755                    if requires_subscript {
13756                        // `->method()` etc — not interpolated, leave for literal output.
13757                    }
13758                    break;
13759                }
13760            }
13761        }
13762        base
13763    }
13764
13765    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
13766        // Parse $var and @var inside double-quoted strings
13767        let mut parts = Vec::new();
13768        let mut literal = String::new();
13769        let chars: Vec<char> = s.chars().collect();
13770        let mut i = 0;
13771
13772        'istr: while i < chars.len() {
13773            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
13774                literal.push('$');
13775                i += 1;
13776                continue;
13777            }
13778            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
13779            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
13780                literal.push('\\');
13781                i += 1;
13782                // i now points at '$' — fall through to $ handling below
13783            }
13784            if chars[i] == '$' && i + 1 < chars.len() {
13785                if !literal.is_empty() {
13786                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13787                }
13788                i += 1; // past `$`
13789                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
13790                while i < chars.len() && chars[i].is_whitespace() {
13791                    i += 1;
13792                }
13793                if i >= chars.len() {
13794                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
13795                }
13796                // `$#name` — last index of `@name` (Perl `$#array`).
13797                if chars[i] == '#' {
13798                    i += 1;
13799                    let mut sname = String::from("#");
13800                    while i < chars.len()
13801                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
13802                    {
13803                        sname.push(chars[i]);
13804                        i += 1;
13805                    }
13806                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
13807                        sname.push_str("::");
13808                        i += 2;
13809                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13810                            sname.push(chars[i]);
13811                            i += 1;
13812                        }
13813                    }
13814                    parts.push(StringPart::ScalarVar(sname));
13815                    continue;
13816                }
13817                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
13818                // between) and the second `$` is not followed by a word character or digit (`$$x`
13819                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
13820                if chars[i] == '$' {
13821                    let next_c = chars.get(i + 1).copied();
13822                    let is_pid = match next_c {
13823                        None => true,
13824                        Some(c)
13825                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
13826                        {
13827                            true
13828                        }
13829                        _ => false,
13830                    };
13831                    if is_pid {
13832                        parts.push(StringPart::ScalarVar("$$".to_string()));
13833                        i += 1; // consume second `$`
13834                        continue;
13835                    }
13836                    i += 1; // skip second `$` — same as a single `$` before the identifier
13837                }
13838                if chars[i] == '{' {
13839                    // `${…}` — braced variable OR expression interpolation.
13840                    //   `${name}`              → ScalarVar(name)        (Perl standard)
13841                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
13842                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
13843                    // stryke's prior `#{expr}` form remains supported elsewhere.
13844                    i += 1;
13845                    let mut inner = String::new();
13846                    let mut depth = 1usize;
13847                    while i < chars.len() && depth > 0 {
13848                        match chars[i] {
13849                            '{' => depth += 1,
13850                            '}' => {
13851                                depth -= 1;
13852                                if depth == 0 {
13853                                    break;
13854                                }
13855                            }
13856                            _ => {}
13857                        }
13858                        inner.push(chars[i]);
13859                        i += 1;
13860                    }
13861                    if i < chars.len() {
13862                        i += 1; // skip closing }
13863                    }
13864
13865                    // Distinguish "name" from "expression". If trimmed inner starts with
13866                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
13867                    // expression and emit a scalar deref. Otherwise, plain variable name.
13868                    let trimmed = inner.trim();
13869                    let is_expr = trimmed.starts_with('$')
13870                        || trimmed.starts_with('\\')
13871                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
13872                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
13873                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
13874                    let mut base: Expr = if is_expr {
13875                        // Re-parse the inner content as a Perl expression. Wrap in
13876                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
13877                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
13878                        match parse_expression_from_str(trimmed, "<interp>") {
13879                            Ok(e) => Expr {
13880                                kind: ExprKind::Deref {
13881                                    expr: Box::new(e),
13882                                    kind: Sigil::Scalar,
13883                                },
13884                                line,
13885                            },
13886                            Err(_) => Expr {
13887                                kind: ExprKind::ScalarVar(inner.clone()),
13888                                line,
13889                            },
13890                        }
13891                    } else {
13892                        // Treat as a plain (possibly qualified) variable name.
13893                        Expr {
13894                            kind: ExprKind::ScalarVar(inner),
13895                            line,
13896                        }
13897                    };
13898
13899                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
13900                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
13901                    // chains thereafter.
13902                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13903                    parts.push(StringPart::Expr(base));
13904                } else if chars[i] == '^' {
13905                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
13906                    let mut name = String::from("^");
13907                    i += 1;
13908                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13909                        name.push(chars[i]);
13910                        i += 1;
13911                    }
13912                    if i < chars.len() && chars[i] == '{' {
13913                        i += 1; // skip {
13914                        let mut key = String::new();
13915                        let mut depth = 1;
13916                        while i < chars.len() && depth > 0 {
13917                            if chars[i] == '{' {
13918                                depth += 1;
13919                            } else if chars[i] == '}' {
13920                                depth -= 1;
13921                                if depth == 0 {
13922                                    break;
13923                                }
13924                            }
13925                            key.push(chars[i]);
13926                            i += 1;
13927                        }
13928                        if i < chars.len() {
13929                            i += 1;
13930                        }
13931                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
13932                            Expr {
13933                                kind: ExprKind::ScalarVar(rest.to_string()),
13934                                line,
13935                            }
13936                        } else {
13937                            Expr {
13938                                kind: ExprKind::String(key),
13939                                line,
13940                            }
13941                        };
13942                        parts.push(StringPart::Expr(Expr {
13943                            kind: ExprKind::HashElement {
13944                                hash: name,
13945                                key: Box::new(key_expr),
13946                            },
13947                            line,
13948                        }));
13949                    } else if i < chars.len() && chars[i] == '[' {
13950                        i += 1;
13951                        let mut idx_str = String::new();
13952                        while i < chars.len() && chars[i] != ']' {
13953                            idx_str.push(chars[i]);
13954                            i += 1;
13955                        }
13956                        if i < chars.len() {
13957                            i += 1;
13958                        }
13959                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
13960                            Expr {
13961                                kind: ExprKind::ScalarVar(rest.to_string()),
13962                                line,
13963                            }
13964                        } else if let Ok(n) = idx_str.parse::<i64>() {
13965                            Expr {
13966                                kind: ExprKind::Integer(n),
13967                                line,
13968                            }
13969                        } else {
13970                            Expr {
13971                                kind: ExprKind::String(idx_str),
13972                                line,
13973                            }
13974                        };
13975                        parts.push(StringPart::Expr(Expr {
13976                            kind: ExprKind::ArrayElement {
13977                                array: name,
13978                                index: Box::new(idx_expr),
13979                            },
13980                            line,
13981                        }));
13982                    } else {
13983                        parts.push(StringPart::ScalarVar(name));
13984                    }
13985                } else if chars[i].is_alphabetic() || chars[i] == '_' {
13986                    let mut name = String::new();
13987                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13988                        name.push(chars[i]);
13989                        i += 1;
13990                    }
13991                    // `$_<`, `$_<<`, … — outer topic (stryke extension); only for bare `_`.
13992                    if name == "_" {
13993                        while i < chars.len() && chars[i] == '<' {
13994                            name.push('<');
13995                            i += 1;
13996                        }
13997                    }
13998                    // Build the base expression, then thread arrow-deref chains
13999                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
14000                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
14001                    // correctly inside double-quoted strings (Perl convention).
14002                    let mut base = if i < chars.len() && chars[i] == '{' {
14003                        // $hash{key}
14004                        i += 1; // skip {
14005                        let mut key = String::new();
14006                        let mut depth = 1;
14007                        while i < chars.len() && depth > 0 {
14008                            if chars[i] == '{' {
14009                                depth += 1;
14010                            } else if chars[i] == '}' {
14011                                depth -= 1;
14012                                if depth == 0 {
14013                                    break;
14014                                }
14015                            }
14016                            key.push(chars[i]);
14017                            i += 1;
14018                        }
14019                        if i < chars.len() {
14020                            i += 1;
14021                        } // skip }
14022                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
14023                            Expr {
14024                                kind: ExprKind::ScalarVar(rest.to_string()),
14025                                line,
14026                            }
14027                        } else {
14028                            Expr {
14029                                kind: ExprKind::String(key),
14030                                line,
14031                            }
14032                        };
14033                        Expr {
14034                            kind: ExprKind::HashElement {
14035                                hash: name,
14036                                key: Box::new(key_expr),
14037                            },
14038                            line,
14039                        }
14040                    } else if i < chars.len() && chars[i] == '[' {
14041                        // $array[idx]
14042                        i += 1;
14043                        let mut idx_str = String::new();
14044                        while i < chars.len() && chars[i] != ']' {
14045                            idx_str.push(chars[i]);
14046                            i += 1;
14047                        }
14048                        if i < chars.len() {
14049                            i += 1;
14050                        }
14051                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
14052                            Expr {
14053                                kind: ExprKind::ScalarVar(rest.to_string()),
14054                                line,
14055                            }
14056                        } else if let Ok(n) = idx_str.parse::<i64>() {
14057                            Expr {
14058                                kind: ExprKind::Integer(n),
14059                                line,
14060                            }
14061                        } else {
14062                            Expr {
14063                                kind: ExprKind::String(idx_str),
14064                                line,
14065                            }
14066                        };
14067                        Expr {
14068                            kind: ExprKind::ArrayElement {
14069                                array: name,
14070                                index: Box::new(idx_expr),
14071                            },
14072                            line,
14073                        }
14074                    } else {
14075                        // Bare $name — defer to the chain-extension loop below.
14076                        Expr {
14077                            kind: ExprKind::ScalarVar(name),
14078                            line,
14079                        }
14080                    };
14081
14082                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
14083                    // implies `->` between consecutive subscripts (`$m[1][2]`
14084                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
14085                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14086                    parts.push(StringPart::Expr(base));
14087                } else if chars[i].is_ascii_digit() {
14088                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
14089                    if chars[i] == '0' {
14090                        i += 1;
14091                        if i < chars.len() && chars[i].is_ascii_digit() {
14092                            return Err(self.syntax_err(
14093                                "Numeric variables with more than one digit may not start with '0'",
14094                                line,
14095                            ));
14096                        }
14097                        parts.push(StringPart::ScalarVar("0".into()));
14098                    } else {
14099                        let start = i;
14100                        while i < chars.len() && chars[i].is_ascii_digit() {
14101                            i += 1;
14102                        }
14103                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
14104                    }
14105                } else {
14106                    let c = chars[i];
14107                    let probe = c.to_string();
14108                    if Interpreter::is_special_scalar_name_for_get(&probe)
14109                        || matches!(c, '\'' | '`')
14110                    {
14111                        i += 1;
14112                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
14113                        if i < chars.len() && chars[i] == '{' {
14114                            i += 1; // skip {
14115                            let mut key = String::new();
14116                            let mut depth = 1;
14117                            while i < chars.len() && depth > 0 {
14118                                if chars[i] == '{' {
14119                                    depth += 1;
14120                                } else if chars[i] == '}' {
14121                                    depth -= 1;
14122                                    if depth == 0 {
14123                                        break;
14124                                    }
14125                                }
14126                                key.push(chars[i]);
14127                                i += 1;
14128                            }
14129                            if i < chars.len() {
14130                                i += 1;
14131                            } // skip }
14132                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
14133                                Expr {
14134                                    kind: ExprKind::ScalarVar(rest.to_string()),
14135                                    line,
14136                                }
14137                            } else {
14138                                Expr {
14139                                    kind: ExprKind::String(key),
14140                                    line,
14141                                }
14142                            };
14143                            let mut base = Expr {
14144                                kind: ExprKind::HashElement {
14145                                    hash: probe,
14146                                    key: Box::new(key_expr),
14147                                },
14148                                line,
14149                            };
14150                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14151                            parts.push(StringPart::Expr(base));
14152                        } else {
14153                            // Check for arrow deref chain: `$@->{key}`, etc.
14154                            let mut base = Expr {
14155                                kind: ExprKind::ScalarVar(probe),
14156                                line,
14157                            };
14158                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14159                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
14160                                // No chain extension — use the simpler ScalarVar part
14161                                if let ExprKind::ScalarVar(name) = base.kind {
14162                                    parts.push(StringPart::ScalarVar(name));
14163                                }
14164                            } else {
14165                                parts.push(StringPart::Expr(base));
14166                            }
14167                        }
14168                    } else {
14169                        literal.push('$');
14170                        literal.push(c);
14171                        i += 1;
14172                    }
14173                }
14174            } else if chars[i] == '@' && i + 1 < chars.len() {
14175                let next = chars[i + 1];
14176                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
14177                if next == '$' {
14178                    if !literal.is_empty() {
14179                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14180                    }
14181                    i += 1; // past `@`
14182                    debug_assert_eq!(chars[i], '$');
14183                    i += 1; // past `$`
14184                    while i < chars.len() && chars[i].is_whitespace() {
14185                        i += 1;
14186                    }
14187                    if i >= chars.len() {
14188                        return Err(self.syntax_err(
14189                            "Expected variable or block after `@$` in double-quoted string",
14190                            line,
14191                        ));
14192                    }
14193                    let inner_expr = if chars[i] == '{' {
14194                        i += 1;
14195                        let start = i;
14196                        let mut depth = 1usize;
14197                        while i < chars.len() && depth > 0 {
14198                            match chars[i] {
14199                                '{' => depth += 1,
14200                                '}' => {
14201                                    depth -= 1;
14202                                    if depth == 0 {
14203                                        break;
14204                                    }
14205                                }
14206                                _ => {}
14207                            }
14208                            i += 1;
14209                        }
14210                        if depth != 0 {
14211                            return Err(self.syntax_err(
14212                                "Unterminated `${ ... }` after `@` in double-quoted string",
14213                                line,
14214                            ));
14215                        }
14216                        let inner: String = chars[start..i].iter().collect();
14217                        i += 1; // closing `}`
14218                        parse_expression_from_str(inner.trim(), "-e")?
14219                    } else {
14220                        let mut name = String::new();
14221                        if chars[i] == '^' {
14222                            name.push('^');
14223                            i += 1;
14224                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
14225                            {
14226                                name.push(chars[i]);
14227                                i += 1;
14228                            }
14229                        } else {
14230                            while i < chars.len()
14231                                && (chars[i].is_alphanumeric()
14232                                    || chars[i] == '_'
14233                                    || chars[i] == ':')
14234                            {
14235                                name.push(chars[i]);
14236                                i += 1;
14237                            }
14238                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14239                                name.push_str("::");
14240                                i += 2;
14241                                while i < chars.len()
14242                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
14243                                {
14244                                    name.push(chars[i]);
14245                                    i += 1;
14246                                }
14247                            }
14248                        }
14249                        if name.is_empty() {
14250                            return Err(self.syntax_err(
14251                                "Expected identifier after `@$` in double-quoted string",
14252                                line,
14253                            ));
14254                        }
14255                        Expr {
14256                            kind: ExprKind::ScalarVar(name),
14257                            line,
14258                        }
14259                    };
14260                    parts.push(StringPart::Expr(Expr {
14261                        kind: ExprKind::Deref {
14262                            expr: Box::new(inner_expr),
14263                            kind: Sigil::Array,
14264                        },
14265                        line,
14266                    }));
14267                    continue 'istr;
14268                }
14269                if next == '{' {
14270                    if !literal.is_empty() {
14271                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14272                    }
14273                    i += 2; // `@{`
14274                    let start = i;
14275                    let mut depth = 1usize;
14276                    while i < chars.len() && depth > 0 {
14277                        match chars[i] {
14278                            '{' => depth += 1,
14279                            '}' => {
14280                                depth -= 1;
14281                                if depth == 0 {
14282                                    break;
14283                                }
14284                            }
14285                            _ => {}
14286                        }
14287                        i += 1;
14288                    }
14289                    if depth != 0 {
14290                        return Err(
14291                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
14292                        );
14293                    }
14294                    let inner: String = chars[start..i].iter().collect();
14295                    i += 1; // closing `}`
14296                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
14297                    parts.push(StringPart::Expr(Expr {
14298                        kind: ExprKind::Deref {
14299                            expr: Box::new(inner_expr),
14300                            kind: Sigil::Array,
14301                        },
14302                        line,
14303                    }));
14304                    continue 'istr;
14305                }
14306                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
14307                    literal.push(chars[i]);
14308                    i += 1;
14309                } else {
14310                    if !literal.is_empty() {
14311                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14312                    }
14313                    i += 1;
14314                    let mut name = String::new();
14315                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
14316                        name.push(chars[i]);
14317                        i += 1;
14318                    } else {
14319                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
14320                            name.push(chars[i]);
14321                            i += 1;
14322                        }
14323                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14324                            name.push_str("::");
14325                            i += 2;
14326                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
14327                            {
14328                                name.push(chars[i]);
14329                                i += 1;
14330                            }
14331                        }
14332                    }
14333                    if i < chars.len() && chars[i] == '[' {
14334                        i += 1;
14335                        let start_inner = i;
14336                        let mut depth = 1usize;
14337                        while i < chars.len() && depth > 0 {
14338                            match chars[i] {
14339                                '[' => depth += 1,
14340                                ']' => depth -= 1,
14341                                _ => {}
14342                            }
14343                            if depth == 0 {
14344                                let inner: String = chars[start_inner..i].iter().collect();
14345                                i += 1; // closing ]
14346                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
14347                                parts.push(StringPart::Expr(Expr {
14348                                    kind: ExprKind::ArraySlice {
14349                                        array: name.clone(),
14350                                        indices,
14351                                    },
14352                                    line,
14353                                }));
14354                                continue 'istr;
14355                            }
14356                            i += 1;
14357                        }
14358                        return Err(self.syntax_err(
14359                            "Unterminated [ in array slice inside quoted string",
14360                            line,
14361                        ));
14362                    }
14363                    parts.push(StringPart::ArrayVar(name));
14364                }
14365            } else if chars[i] == '#'
14366                && i + 1 < chars.len()
14367                && chars[i + 1] == '{'
14368                && !crate::compat_mode()
14369            {
14370                // #{expr} — Ruby-style expression interpolation (stryke extension).
14371                if !literal.is_empty() {
14372                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14373                }
14374                i += 2; // skip `#{`
14375                let mut inner = String::new();
14376                let mut depth = 1usize;
14377                while i < chars.len() && depth > 0 {
14378                    match chars[i] {
14379                        '{' => depth += 1,
14380                        '}' => {
14381                            depth -= 1;
14382                            if depth == 0 {
14383                                break;
14384                            }
14385                        }
14386                        _ => {}
14387                    }
14388                    inner.push(chars[i]);
14389                    i += 1;
14390                }
14391                if i < chars.len() {
14392                    i += 1; // skip closing `}`
14393                }
14394                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
14395                parts.push(StringPart::Expr(expr));
14396            } else {
14397                literal.push(chars[i]);
14398                i += 1;
14399            }
14400        }
14401        if !literal.is_empty() {
14402            parts.push(StringPart::Literal(literal));
14403        }
14404
14405        if parts.len() == 1 {
14406            if let StringPart::Literal(s) = &parts[0] {
14407                return Ok(Expr {
14408                    kind: ExprKind::String(s.clone()),
14409                    line,
14410                });
14411            }
14412        }
14413        if parts.is_empty() {
14414            return Ok(Expr {
14415                kind: ExprKind::String(String::new()),
14416                line,
14417            });
14418        }
14419
14420        Ok(Expr {
14421            kind: ExprKind::InterpolatedString(parts),
14422            line,
14423        })
14424    }
14425
14426    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
14427        match &e.kind {
14428            ExprKind::String(s) => Ok(s.clone()),
14429            _ => Err(self.syntax_err(
14430                "overload key must be a string literal (e.g. '\"\"' or '+')",
14431                e.line,
14432            )),
14433        }
14434    }
14435
14436    fn expr_to_overload_sub(&self, e: &Expr) -> PerlResult<String> {
14437        match &e.kind {
14438            ExprKind::String(s) => Ok(s.clone()),
14439            ExprKind::Integer(n) => Ok(n.to_string()),
14440            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
14441            _ => Err(self.syntax_err(
14442                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
14443                e.line,
14444            )),
14445        }
14446    }
14447}
14448
14449fn merge_expr_list(parts: Vec<Expr>) -> Expr {
14450    if parts.len() == 1 {
14451        parts.into_iter().next().unwrap()
14452    } else {
14453        let line = parts.first().map(|e| e.line).unwrap_or(0);
14454        Expr {
14455            kind: ExprKind::List(parts),
14456            line,
14457        }
14458    }
14459}
14460
14461/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
14462pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
14463    let mut lexer = Lexer::new_with_file(s, file);
14464    let tokens = lexer.tokenize()?;
14465    let mut parser = Parser::new_with_file(tokens, file);
14466    let e = parser.parse_expression()?;
14467    if !parser.at_eof() {
14468        return Err(parser.syntax_err(
14469            "Extra tokens in embedded string expression",
14470            parser.peek_line(),
14471        ));
14472    }
14473    Ok(e)
14474}
14475
14476/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
14477pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
14478    let mut lexer = Lexer::new_with_file(s, file);
14479    let tokens = lexer.tokenize()?;
14480    let mut parser = Parser::new_with_file(tokens, file);
14481    let stmts = parser.parse_statements()?;
14482    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
14483    let inner = Expr {
14484        kind: ExprKind::CodeRef {
14485            params: vec![],
14486            body: stmts,
14487        },
14488        line: inner_line,
14489    };
14490    Ok(Expr {
14491        kind: ExprKind::Do(Box::new(inner)),
14492        line,
14493    })
14494}
14495
14496/// Comma-separated expressions on a `format` value line (below a picture line).
14497/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
14498pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
14499    let mut lexer = Lexer::new_with_file(s, file);
14500    let tokens = lexer.tokenize()?;
14501    let mut parser = Parser::new_with_file(tokens, file);
14502    parser.parse_arg_list()
14503}
14504
14505pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
14506    let trimmed = line.trim();
14507    if trimmed.is_empty() {
14508        return Ok(vec![]);
14509    }
14510    let mut lexer = Lexer::new(trimmed);
14511    let tokens = lexer.tokenize()?;
14512    let mut parser = Parser::new(tokens);
14513    let mut exprs = Vec::new();
14514    loop {
14515        if parser.at_eof() {
14516            break;
14517        }
14518        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
14519        exprs.push(parser.parse_assign_expr()?);
14520        if parser.eat(&Token::Comma) {
14521            continue;
14522        }
14523        if !parser.at_eof() {
14524            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
14525        }
14526        break;
14527    }
14528    Ok(exprs)
14529}
14530
14531#[cfg(test)]
14532mod tests {
14533    use super::*;
14534
14535    fn parse_ok(code: &str) -> Program {
14536        let mut lexer = Lexer::new(code);
14537        let tokens = lexer.tokenize().expect("tokenize");
14538        let mut parser = Parser::new(tokens);
14539        parser.parse_program().expect("parse")
14540    }
14541
14542    fn parse_err(code: &str) -> String {
14543        let mut lexer = Lexer::new(code);
14544        let tokens = match lexer.tokenize() {
14545            Ok(t) => t,
14546            Err(e) => return e.message,
14547        };
14548        let mut parser = Parser::new(tokens);
14549        parser.parse_program().unwrap_err().message
14550    }
14551
14552    #[test]
14553    fn parse_empty_program() {
14554        let p = parse_ok("");
14555        assert!(p.statements.is_empty());
14556    }
14557
14558    #[test]
14559    fn parse_semicolons_only() {
14560        let p = parse_ok(";;");
14561        assert!(p.statements.len() <= 3);
14562    }
14563
14564    #[test]
14565    fn parse_simple_scalar_assignment() {
14566        let p = parse_ok("$x = 1");
14567        assert_eq!(p.statements.len(), 1);
14568    }
14569
14570    #[test]
14571    fn parse_simple_array_assignment() {
14572        let p = parse_ok("@arr = (1, 2, 3)");
14573        assert_eq!(p.statements.len(), 1);
14574    }
14575
14576    #[test]
14577    fn parse_simple_hash_assignment() {
14578        let p = parse_ok("%h = (a => 1, b => 2)");
14579        assert_eq!(p.statements.len(), 1);
14580    }
14581
14582    #[test]
14583    fn parse_subroutine_decl() {
14584        let p = parse_ok("fn foo { 1 }");
14585        assert_eq!(p.statements.len(), 1);
14586        match &p.statements[0].kind {
14587            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
14588            _ => panic!("expected SubDecl"),
14589        }
14590    }
14591
14592    #[test]
14593    fn parse_subroutine_with_prototype() {
14594        let p = parse_ok("fn foo ($$) { 1 }");
14595        assert_eq!(p.statements.len(), 1);
14596        match &p.statements[0].kind {
14597            StmtKind::SubDecl { prototype, .. } => {
14598                assert!(prototype.is_some());
14599            }
14600            _ => panic!("expected SubDecl"),
14601        }
14602    }
14603
14604    #[test]
14605    fn parse_anonymous_fn() {
14606        let p = parse_ok("my $f = fn { 1 }");
14607        assert_eq!(p.statements.len(), 1);
14608    }
14609
14610    #[test]
14611    fn parse_if_statement() {
14612        let p = parse_ok("if (1) { 2 }");
14613        assert_eq!(p.statements.len(), 1);
14614        matches!(&p.statements[0].kind, StmtKind::If { .. });
14615    }
14616
14617    #[test]
14618    fn parse_if_elsif_else() {
14619        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
14620        assert_eq!(p.statements.len(), 1);
14621    }
14622
14623    #[test]
14624    fn parse_unless_statement() {
14625        let p = parse_ok("unless (0) { 1 }");
14626        assert_eq!(p.statements.len(), 1);
14627    }
14628
14629    #[test]
14630    fn parse_while_loop() {
14631        let p = parse_ok("while ($x) { $x-- }");
14632        assert_eq!(p.statements.len(), 1);
14633    }
14634
14635    #[test]
14636    fn parse_until_loop() {
14637        let p = parse_ok("until ($x) { $x++ }");
14638        assert_eq!(p.statements.len(), 1);
14639    }
14640
14641    #[test]
14642    fn parse_for_c_style() {
14643        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
14644        assert_eq!(p.statements.len(), 1);
14645    }
14646
14647    #[test]
14648    fn parse_foreach_loop() {
14649        let p = parse_ok("foreach my $x (@arr) { 1 }");
14650        assert_eq!(p.statements.len(), 1);
14651    }
14652
14653    #[test]
14654    fn parse_loop_with_label() {
14655        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
14656        assert_eq!(p.statements.len(), 1);
14657        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
14658    }
14659
14660    #[test]
14661    fn parse_begin_block() {
14662        let p = parse_ok("BEGIN { 1 }");
14663        assert_eq!(p.statements.len(), 1);
14664        matches!(&p.statements[0].kind, StmtKind::Begin(_));
14665    }
14666
14667    #[test]
14668    fn parse_end_block() {
14669        let p = parse_ok("END { 1 }");
14670        assert_eq!(p.statements.len(), 1);
14671        matches!(&p.statements[0].kind, StmtKind::End(_));
14672    }
14673
14674    #[test]
14675    fn parse_package_statement() {
14676        let p = parse_ok("package Foo::Bar");
14677        assert_eq!(p.statements.len(), 1);
14678        match &p.statements[0].kind {
14679            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
14680            _ => panic!("expected Package"),
14681        }
14682    }
14683
14684    #[test]
14685    fn parse_use_statement() {
14686        let p = parse_ok("use strict");
14687        assert_eq!(p.statements.len(), 1);
14688    }
14689
14690    #[test]
14691    fn parse_no_statement() {
14692        let p = parse_ok("no warnings");
14693        assert_eq!(p.statements.len(), 1);
14694    }
14695
14696    #[test]
14697    fn parse_require_bareword() {
14698        let p = parse_ok("require Foo::Bar");
14699        assert_eq!(p.statements.len(), 1);
14700    }
14701
14702    #[test]
14703    fn parse_require_string() {
14704        let p = parse_ok(r#"require "foo.pl""#);
14705        assert_eq!(p.statements.len(), 1);
14706    }
14707
14708    #[test]
14709    fn parse_eval_block() {
14710        let p = parse_ok("eval { 1 }");
14711        assert_eq!(p.statements.len(), 1);
14712    }
14713
14714    #[test]
14715    fn parse_eval_string() {
14716        let p = parse_ok(r#"eval "1 + 2""#);
14717        assert_eq!(p.statements.len(), 1);
14718    }
14719
14720    #[test]
14721    fn parse_qw_word_list() {
14722        let p = parse_ok("my @a = qw(foo bar baz)");
14723        assert_eq!(p.statements.len(), 1);
14724    }
14725
14726    #[test]
14727    fn parse_q_string() {
14728        let p = parse_ok("my $s = q{hello}");
14729        assert_eq!(p.statements.len(), 1);
14730    }
14731
14732    #[test]
14733    fn parse_qq_string() {
14734        let p = parse_ok(r#"my $s = qq(hello $x)"#);
14735        assert_eq!(p.statements.len(), 1);
14736    }
14737
14738    #[test]
14739    fn parse_regex_match() {
14740        let p = parse_ok(r#"$x =~ /foo/"#);
14741        assert_eq!(p.statements.len(), 1);
14742    }
14743
14744    #[test]
14745    fn parse_regex_substitution() {
14746        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
14747        assert_eq!(p.statements.len(), 1);
14748    }
14749
14750    #[test]
14751    fn parse_transliterate() {
14752        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
14753        assert_eq!(p.statements.len(), 1);
14754    }
14755
14756    #[test]
14757    fn parse_ternary_operator() {
14758        let p = parse_ok("my $x = $a ? 1 : 2");
14759        assert_eq!(p.statements.len(), 1);
14760    }
14761
14762    #[test]
14763    fn parse_arrow_method_call() {
14764        let p = parse_ok("$obj->method()");
14765        assert_eq!(p.statements.len(), 1);
14766    }
14767
14768    #[test]
14769    fn parse_arrow_deref_hash() {
14770        let p = parse_ok("$r->{key}");
14771        assert_eq!(p.statements.len(), 1);
14772    }
14773
14774    #[test]
14775    fn parse_arrow_deref_array() {
14776        let p = parse_ok("$r->[0]");
14777        assert_eq!(p.statements.len(), 1);
14778    }
14779
14780    #[test]
14781    fn parse_chained_arrow_deref() {
14782        let p = parse_ok("$r->{a}[0]{b}");
14783        assert_eq!(p.statements.len(), 1);
14784    }
14785
14786    #[test]
14787    fn parse_my_multiple_vars() {
14788        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
14789        assert_eq!(p.statements.len(), 1);
14790    }
14791
14792    #[test]
14793    fn parse_our_scalar() {
14794        let p = parse_ok("our $VERSION = '1.0'");
14795        assert_eq!(p.statements.len(), 1);
14796    }
14797
14798    #[test]
14799    fn parse_local_scalar() {
14800        let p = parse_ok("local $/ = undef");
14801        assert_eq!(p.statements.len(), 1);
14802    }
14803
14804    #[test]
14805    fn parse_state_variable() {
14806        let p = parse_ok("fn my_counter { state $n = 0; $n++ }");
14807        assert_eq!(p.statements.len(), 1);
14808    }
14809
14810    #[test]
14811    fn parse_postfix_if() {
14812        let p = parse_ok("print 1 if $x");
14813        assert_eq!(p.statements.len(), 1);
14814    }
14815
14816    #[test]
14817    fn parse_postfix_unless() {
14818        let p = parse_ok("die 'error' unless $ok");
14819        assert_eq!(p.statements.len(), 1);
14820    }
14821
14822    #[test]
14823    fn parse_postfix_while() {
14824        let p = parse_ok("$x++ while $x < 10");
14825        assert_eq!(p.statements.len(), 1);
14826    }
14827
14828    #[test]
14829    fn parse_postfix_for() {
14830        let p = parse_ok("print for @arr");
14831        assert_eq!(p.statements.len(), 1);
14832    }
14833
14834    #[test]
14835    fn parse_last_next_redo() {
14836        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
14837        assert_eq!(p.statements.len(), 1);
14838    }
14839
14840    #[test]
14841    fn parse_return_statement() {
14842        let p = parse_ok("fn foo { return 42 }");
14843        assert_eq!(p.statements.len(), 1);
14844    }
14845
14846    #[test]
14847    fn parse_wantarray() {
14848        let p = parse_ok("fn foo { wantarray ? @a : $a }");
14849        assert_eq!(p.statements.len(), 1);
14850    }
14851
14852    #[test]
14853    fn parse_caller_builtin() {
14854        let p = parse_ok("my @c = caller");
14855        assert_eq!(p.statements.len(), 1);
14856    }
14857
14858    #[test]
14859    fn parse_ref_to_array() {
14860        let p = parse_ok("my $r = \\@arr");
14861        assert_eq!(p.statements.len(), 1);
14862    }
14863
14864    #[test]
14865    fn parse_ref_to_hash() {
14866        let p = parse_ok("my $r = \\%hash");
14867        assert_eq!(p.statements.len(), 1);
14868    }
14869
14870    #[test]
14871    fn parse_ref_to_scalar() {
14872        let p = parse_ok("my $r = \\$x");
14873        assert_eq!(p.statements.len(), 1);
14874    }
14875
14876    #[test]
14877    fn parse_deref_scalar() {
14878        let p = parse_ok("my $v = $$r");
14879        assert_eq!(p.statements.len(), 1);
14880    }
14881
14882    #[test]
14883    fn parse_deref_array() {
14884        let p = parse_ok("my @a = @$r");
14885        assert_eq!(p.statements.len(), 1);
14886    }
14887
14888    #[test]
14889    fn parse_deref_hash() {
14890        let p = parse_ok("my %h = %$r");
14891        assert_eq!(p.statements.len(), 1);
14892    }
14893
14894    #[test]
14895    fn parse_blessed_ref() {
14896        let p = parse_ok("bless $r, 'Foo'");
14897        assert_eq!(p.statements.len(), 1);
14898    }
14899
14900    #[test]
14901    fn parse_heredoc_basic() {
14902        let p = parse_ok("my $s = <<END;\nfoo\nEND");
14903        assert_eq!(p.statements.len(), 1);
14904    }
14905
14906    #[test]
14907    fn parse_heredoc_quoted() {
14908        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
14909        assert_eq!(p.statements.len(), 1);
14910    }
14911
14912    #[test]
14913    fn parse_do_block() {
14914        let p = parse_ok("my $x = do { 1 + 2 }");
14915        assert_eq!(p.statements.len(), 1);
14916    }
14917
14918    #[test]
14919    fn parse_do_file() {
14920        let p = parse_ok(r#"do "foo.pl""#);
14921        assert_eq!(p.statements.len(), 1);
14922    }
14923
14924    #[test]
14925    fn parse_map_expression() {
14926        let p = parse_ok("my @b = map { $_ * 2 } @a");
14927        assert_eq!(p.statements.len(), 1);
14928    }
14929
14930    #[test]
14931    fn parse_grep_expression() {
14932        let p = parse_ok("my @b = grep { $_ > 0 } @a");
14933        assert_eq!(p.statements.len(), 1);
14934    }
14935
14936    #[test]
14937    fn parse_sort_expression() {
14938        let p = parse_ok("my @b = sort { $a <=> $b } @a");
14939        assert_eq!(p.statements.len(), 1);
14940    }
14941
14942    #[test]
14943    fn parse_pipe_forward() {
14944        let p = parse_ok("@a |> map { $_ * 2 }");
14945        assert_eq!(p.statements.len(), 1);
14946    }
14947
14948    #[test]
14949    fn parse_expression_from_str_simple() {
14950        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
14951        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
14952    }
14953
14954    #[test]
14955    fn parse_expression_from_str_extra_tokens_error() {
14956        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
14957        assert!(err.message.contains("Extra tokens"));
14958    }
14959
14960    #[test]
14961    fn parse_slice_indices_from_str_basic() {
14962        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
14963        assert_eq!(indices.len(), 3);
14964    }
14965
14966    #[test]
14967    fn parse_format_value_line_empty() {
14968        let exprs = parse_format_value_line("").unwrap();
14969        assert!(exprs.is_empty());
14970    }
14971
14972    #[test]
14973    fn parse_format_value_line_single() {
14974        let exprs = parse_format_value_line("$x").unwrap();
14975        assert_eq!(exprs.len(), 1);
14976    }
14977
14978    #[test]
14979    fn parse_format_value_line_multiple() {
14980        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
14981        assert_eq!(exprs.len(), 3);
14982    }
14983
14984    #[test]
14985    fn parse_unclosed_brace_error() {
14986        let err = parse_err("fn foo {");
14987        assert!(!err.is_empty());
14988    }
14989
14990    #[test]
14991    fn parse_unclosed_paren_error() {
14992        let err = parse_err("print (1, 2");
14993        assert!(!err.is_empty());
14994    }
14995
14996    #[test]
14997    fn parse_invalid_statement_error() {
14998        let err = parse_err("???");
14999        assert!(!err.is_empty());
15000    }
15001
15002    #[test]
15003    fn merge_expr_list_single() {
15004        let e = Expr {
15005            kind: ExprKind::Integer(1),
15006            line: 1,
15007        };
15008        let merged = merge_expr_list(vec![e.clone()]);
15009        matches!(merged.kind, ExprKind::Integer(1));
15010    }
15011
15012    #[test]
15013    fn merge_expr_list_multiple() {
15014        let e1 = Expr {
15015            kind: ExprKind::Integer(1),
15016            line: 1,
15017        };
15018        let e2 = Expr {
15019            kind: ExprKind::Integer(2),
15020            line: 1,
15021        };
15022        let merged = merge_expr_list(vec![e1, e2]);
15023        matches!(merged.kind, ExprKind::List(_));
15024    }
15025}