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(
2399                        self.syntax_err("stryke uses `rev` instead of `reverse` (this is not Perl 5)", line)
2400                    );
2401                }
2402                ExprKind::ReverseExpr(Box::new(arg))
2403            }
2404            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2405            "sort" | "so" => ExprKind::SortExpr {
2406                cmp: None,
2407                list: Box::new(arg),
2408            },
2409            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2410                name: "uniq".to_string(),
2411                args: vec![arg],
2412            },
2413            "trim" | "tm" => ExprKind::FuncCall {
2414                name: "trim".to_string(),
2415                args: vec![arg],
2416            },
2417            "flatten" | "fl" => ExprKind::FuncCall {
2418                name: "flatten".to_string(),
2419                args: vec![arg],
2420            },
2421            "compact" | "cpt" => ExprKind::FuncCall {
2422                name: "compact".to_string(),
2423                args: vec![arg],
2424            },
2425            "shuffle" | "shuf" => ExprKind::FuncCall {
2426                name: "shuffle".to_string(),
2427                args: vec![arg],
2428            },
2429            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2430                name: "frequencies".to_string(),
2431                args: vec![arg],
2432            },
2433            "dedup" | "dup" => ExprKind::FuncCall {
2434                name: "dedup".to_string(),
2435                args: vec![arg],
2436            },
2437            "enumerate" | "en" => ExprKind::FuncCall {
2438                name: "enumerate".to_string(),
2439                args: vec![arg],
2440            },
2441            "lines" | "ln" => ExprKind::FuncCall {
2442                name: "lines".to_string(),
2443                args: vec![arg],
2444            },
2445            "words" | "wd" => ExprKind::FuncCall {
2446                name: "words".to_string(),
2447                args: vec![arg],
2448            },
2449            "chars" | "ch" => ExprKind::FuncCall {
2450                name: "chars".to_string(),
2451                args: vec![arg],
2452            },
2453            "digits" | "dg" => ExprKind::FuncCall {
2454                name: "digits".to_string(),
2455                args: vec![arg],
2456            },
2457            "letters" | "lts" => ExprKind::FuncCall {
2458                name: "letters".to_string(),
2459                args: vec![arg],
2460            },
2461            "letters_uc" => ExprKind::FuncCall {
2462                name: "letters_uc".to_string(),
2463                args: vec![arg],
2464            },
2465            "letters_lc" => ExprKind::FuncCall {
2466                name: "letters_lc".to_string(),
2467                args: vec![arg],
2468            },
2469            "punctuation" | "punct" => ExprKind::FuncCall {
2470                name: "punctuation".to_string(),
2471                args: vec![arg],
2472            },
2473            "sentences" | "sents" => ExprKind::FuncCall {
2474                name: "sentences".to_string(),
2475                args: vec![arg],
2476            },
2477            "paragraphs" | "paras" => ExprKind::FuncCall {
2478                name: "paragraphs".to_string(),
2479                args: vec![arg],
2480            },
2481            "sections" | "sects" => ExprKind::FuncCall {
2482                name: "sections".to_string(),
2483                args: vec![arg],
2484            },
2485            "numbers" | "nums" => ExprKind::FuncCall {
2486                name: "numbers".to_string(),
2487                args: vec![arg],
2488            },
2489            "graphemes" | "grs" => ExprKind::FuncCall {
2490                name: "graphemes".to_string(),
2491                args: vec![arg],
2492            },
2493            "columns" | "cols" => ExprKind::FuncCall {
2494                name: "columns".to_string(),
2495                args: vec![arg],
2496            },
2497            // File functions
2498            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2499            "chdir" => ExprKind::Chdir(Box::new(arg)),
2500            "stat" => ExprKind::Stat(Box::new(arg)),
2501            "lstat" => ExprKind::Lstat(Box::new(arg)),
2502            "readlink" => ExprKind::Readlink(Box::new(arg)),
2503            "readdir" => ExprKind::Readdir(Box::new(arg)),
2504            "close" => ExprKind::Close(Box::new(arg)),
2505            "basename" | "bn" => ExprKind::FuncCall {
2506                name: "basename".to_string(),
2507                args: vec![arg],
2508            },
2509            "dirname" | "dn" => ExprKind::FuncCall {
2510                name: "dirname".to_string(),
2511                args: vec![arg],
2512            },
2513            "realpath" | "rp" => ExprKind::FuncCall {
2514                name: "realpath".to_string(),
2515                args: vec![arg],
2516            },
2517            "which" | "wh" => ExprKind::FuncCall {
2518                name: "which".to_string(),
2519                args: vec![arg],
2520            },
2521            // Other
2522            "eval" => ExprKind::Eval(Box::new(arg)),
2523            "require" => ExprKind::Require(Box::new(arg)),
2524            "study" => ExprKind::Study(Box::new(arg)),
2525            // Case conversion
2526            "snake_case" | "sc" => ExprKind::FuncCall {
2527                name: "snake_case".to_string(),
2528                args: vec![arg],
2529            },
2530            "camel_case" | "cc" => ExprKind::FuncCall {
2531                name: "camel_case".to_string(),
2532                args: vec![arg],
2533            },
2534            "kebab_case" | "kc" => ExprKind::FuncCall {
2535                name: "kebab_case".to_string(),
2536                args: vec![arg],
2537            },
2538            // Serialization
2539            "to_json" | "tj" => ExprKind::FuncCall {
2540                name: "to_json".to_string(),
2541                args: vec![arg],
2542            },
2543            "to_yaml" | "ty" => ExprKind::FuncCall {
2544                name: "to_yaml".to_string(),
2545                args: vec![arg],
2546            },
2547            "to_toml" | "tt" => ExprKind::FuncCall {
2548                name: "to_toml".to_string(),
2549                args: vec![arg],
2550            },
2551            "to_csv" | "tc" => ExprKind::FuncCall {
2552                name: "to_csv".to_string(),
2553                args: vec![arg],
2554            },
2555            "to_xml" | "tx" => ExprKind::FuncCall {
2556                name: "to_xml".to_string(),
2557                args: vec![arg],
2558            },
2559            "to_html" | "th" => ExprKind::FuncCall {
2560                name: "to_html".to_string(),
2561                args: vec![arg],
2562            },
2563            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2564                name: "to_markdown".to_string(),
2565                args: vec![arg],
2566            },
2567            "xopen" | "xo" => ExprKind::FuncCall {
2568                name: "xopen".to_string(),
2569                args: vec![arg],
2570            },
2571            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2572                name: "clip".to_string(),
2573                args: vec![arg],
2574            },
2575            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2576                name: "to_table".to_string(),
2577                args: vec![arg],
2578            },
2579            "sparkline" | "spark" => ExprKind::FuncCall {
2580                name: "sparkline".to_string(),
2581                args: vec![arg],
2582            },
2583            "bar_chart" | "bars" => ExprKind::FuncCall {
2584                name: "bar_chart".to_string(),
2585                args: vec![arg],
2586            },
2587            "flame" | "flamechart" => ExprKind::FuncCall {
2588                name: "flame".to_string(),
2589                args: vec![arg],
2590            },
2591            "ddump" | "dd" => ExprKind::FuncCall {
2592                name: "ddump".to_string(),
2593                args: vec![arg],
2594            },
2595            "say" => {
2596                if !crate::compat_mode() {
2597                    return Err(self.syntax_err("stryke uses `p` instead of `say` (this is not Perl 5)", line));
2598                }
2599                ExprKind::Say {
2600                    handle: None,
2601                    args: vec![arg],
2602                }
2603            }
2604            "p" => ExprKind::Say {
2605                handle: None,
2606                args: vec![arg],
2607            },
2608            "print" => ExprKind::Print {
2609                handle: None,
2610                args: vec![arg],
2611            },
2612            "warn" => ExprKind::Warn(vec![arg]),
2613            "die" => ExprKind::Die(vec![arg]),
2614            "stringify" | "str" => ExprKind::FuncCall {
2615                name: "stringify".to_string(),
2616                args: vec![arg],
2617            },
2618            "json_decode" | "jd" => ExprKind::FuncCall {
2619                name: "json_decode".to_string(),
2620                args: vec![arg],
2621            },
2622            "yaml_decode" | "yd" => ExprKind::FuncCall {
2623                name: "yaml_decode".to_string(),
2624                args: vec![arg],
2625            },
2626            "toml_decode" | "td" => ExprKind::FuncCall {
2627                name: "toml_decode".to_string(),
2628                args: vec![arg],
2629            },
2630            "xml_decode" | "xd" => ExprKind::FuncCall {
2631                name: "xml_decode".to_string(),
2632                args: vec![arg],
2633            },
2634            "json_encode" | "je" => ExprKind::FuncCall {
2635                name: "json_encode".to_string(),
2636                args: vec![arg],
2637            },
2638            "yaml_encode" | "ye" => ExprKind::FuncCall {
2639                name: "yaml_encode".to_string(),
2640                args: vec![arg],
2641            },
2642            "toml_encode" | "te" => ExprKind::FuncCall {
2643                name: "toml_encode".to_string(),
2644                args: vec![arg],
2645            },
2646            "xml_encode" | "xe" => ExprKind::FuncCall {
2647                name: "xml_encode".to_string(),
2648                args: vec![arg],
2649            },
2650            // Encoding
2651            "base64_encode" | "b64e" => ExprKind::FuncCall {
2652                name: "base64_encode".to_string(),
2653                args: vec![arg],
2654            },
2655            "base64_decode" | "b64d" => ExprKind::FuncCall {
2656                name: "base64_decode".to_string(),
2657                args: vec![arg],
2658            },
2659            "hex_encode" | "hxe" => ExprKind::FuncCall {
2660                name: "hex_encode".to_string(),
2661                args: vec![arg],
2662            },
2663            "hex_decode" | "hxd" => ExprKind::FuncCall {
2664                name: "hex_decode".to_string(),
2665                args: vec![arg],
2666            },
2667            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
2668                name: "url_encode".to_string(),
2669                args: vec![arg],
2670            },
2671            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
2672                name: "url_decode".to_string(),
2673                args: vec![arg],
2674            },
2675            "gzip" | "gz" => ExprKind::FuncCall {
2676                name: "gzip".to_string(),
2677                args: vec![arg],
2678            },
2679            "gunzip" | "ugz" => ExprKind::FuncCall {
2680                name: "gunzip".to_string(),
2681                args: vec![arg],
2682            },
2683            "zstd" | "zst" => ExprKind::FuncCall {
2684                name: "zstd".to_string(),
2685                args: vec![arg],
2686            },
2687            "zstd_decode" | "uzst" => ExprKind::FuncCall {
2688                name: "zstd_decode".to_string(),
2689                args: vec![arg],
2690            },
2691            // Crypto
2692            "sha256" | "s256" => ExprKind::FuncCall {
2693                name: "sha256".to_string(),
2694                args: vec![arg],
2695            },
2696            "sha1" | "s1" => ExprKind::FuncCall {
2697                name: "sha1".to_string(),
2698                args: vec![arg],
2699            },
2700            "md5" | "m5" => ExprKind::FuncCall {
2701                name: "md5".to_string(),
2702                args: vec![arg],
2703            },
2704            "uuid" | "uid" => ExprKind::FuncCall {
2705                name: "uuid".to_string(),
2706                args: vec![arg],
2707            },
2708            // Datetime
2709            "datetime_utc" | "utc" => ExprKind::FuncCall {
2710                name: "datetime_utc".to_string(),
2711                args: vec![arg],
2712            },
2713            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
2714            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
2715            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
2716                block: vec![Statement {
2717                    label: None,
2718                    kind: StmtKind::Expression(Expr {
2719                        kind: ExprKind::Say {
2720                            handle: None,
2721                            args: vec![Expr {
2722                                kind: ExprKind::ScalarVar("_".into()),
2723                                line,
2724                            }],
2725                        },
2726                        line,
2727                    }),
2728                    line,
2729                }],
2730                list: Box::new(arg),
2731            },
2732            // Default: generic function call
2733            _ => ExprKind::FuncCall {
2734                name: name.to_string(),
2735                args: vec![arg],
2736            },
2737        };
2738        Ok(Expr { kind, line })
2739    }
2740
2741    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
2742    /// In thread context, we only parse the block - the list comes from the piped result.
2743    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
2744        let block = self.parse_block()?;
2745        // Use a placeholder for the list - pipe_forward_apply will replace it
2746        let placeholder = self.pipe_placeholder_list(line);
2747
2748        match name {
2749            "map" | "flat_map" | "maps" | "flat_maps" => {
2750                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
2751                let stream = matches!(name, "maps" | "flat_maps");
2752                Ok(Expr {
2753                    kind: ExprKind::MapExpr {
2754                        block,
2755                        list: Box::new(placeholder),
2756                        flatten_array_refs,
2757                        stream,
2758                    },
2759                    line,
2760                })
2761            }
2762            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
2763                let keyword = match name {
2764                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
2765                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
2766                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
2767                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
2768                    _ => unreachable!(),
2769                };
2770                Ok(Expr {
2771                    kind: ExprKind::GrepExpr {
2772                        block,
2773                        list: Box::new(placeholder),
2774                        keyword,
2775                    },
2776                    line,
2777                })
2778            }
2779            "sort" | "so" => Ok(Expr {
2780                kind: ExprKind::SortExpr {
2781                    cmp: Some(SortComparator::Block(block)),
2782                    list: Box::new(placeholder),
2783                },
2784                line,
2785            }),
2786            "reduce" | "rd" => Ok(Expr {
2787                kind: ExprKind::ReduceExpr {
2788                    block,
2789                    list: Box::new(placeholder),
2790                },
2791                line,
2792            }),
2793            "fore" | "e" | "ep" => Ok(Expr {
2794                kind: ExprKind::ForEachExpr {
2795                    block,
2796                    list: Box::new(placeholder),
2797                },
2798                line,
2799            }),
2800            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
2801                kind: ExprKind::PMapExpr {
2802                    block,
2803                    list: Box::new(placeholder),
2804                    progress: None,
2805                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
2806                    on_cluster: None,
2807                    stream: name == "pmaps" || name == "pflat_maps",
2808                },
2809                line,
2810            }),
2811            "pgrep" | "pgreps" => Ok(Expr {
2812                kind: ExprKind::PGrepExpr {
2813                    block,
2814                    list: Box::new(placeholder),
2815                    progress: None,
2816                    stream: name == "pgreps",
2817                },
2818                line,
2819            }),
2820            "pfor" => Ok(Expr {
2821                kind: ExprKind::PForExpr {
2822                    block,
2823                    list: Box::new(placeholder),
2824                    progress: None,
2825                },
2826                line,
2827            }),
2828            "preduce" => Ok(Expr {
2829                kind: ExprKind::PReduceExpr {
2830                    block,
2831                    list: Box::new(placeholder),
2832                    progress: None,
2833                },
2834                line,
2835            }),
2836            "pcache" => Ok(Expr {
2837                kind: ExprKind::PcacheExpr {
2838                    block,
2839                    list: Box::new(placeholder),
2840                    progress: None,
2841                },
2842                line,
2843            }),
2844            "psort" => Ok(Expr {
2845                kind: ExprKind::PSortExpr {
2846                    cmp: Some(block),
2847                    list: Box::new(placeholder),
2848                    progress: None,
2849                },
2850                line,
2851            }),
2852            _ => {
2853                // Generic: parse block and treat as FuncCall with code ref arg
2854                let code_ref = Expr {
2855                    kind: ExprKind::CodeRef {
2856                        params: vec![],
2857                        body: block,
2858                    },
2859                    line,
2860                };
2861                Ok(Expr {
2862                    kind: ExprKind::FuncCall {
2863                        name: name.to_string(),
2864                        args: vec![code_ref],
2865                    },
2866                    line,
2867                })
2868            }
2869        }
2870    }
2871
2872    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
2873    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
2874        let line = self.peek_line();
2875        self.advance(); // tie
2876        let target = match self.peek().clone() {
2877            Token::HashVar(h) => {
2878                self.advance();
2879                TieTarget::Hash(h)
2880            }
2881            Token::ArrayVar(a) => {
2882                self.advance();
2883                TieTarget::Array(a)
2884            }
2885            Token::ScalarVar(s) => {
2886                self.advance();
2887                TieTarget::Scalar(s)
2888            }
2889            tok => {
2890                return Err(self.syntax_err(
2891                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
2892                    self.peek_line(),
2893                ));
2894            }
2895        };
2896        self.expect(&Token::Comma)?;
2897        let class = self.parse_assign_expr()?;
2898        let mut args = Vec::new();
2899        while self.eat(&Token::Comma) {
2900            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
2901                break;
2902            }
2903            args.push(self.parse_assign_expr()?);
2904        }
2905        self.eat(&Token::Semicolon);
2906        Ok(Statement {
2907            label: None,
2908            kind: StmtKind::Tie {
2909                target,
2910                class,
2911                args,
2912            },
2913            line,
2914        })
2915    }
2916
2917    /// `given (EXPR) { ... }`
2918    fn parse_given(&mut self) -> PerlResult<Statement> {
2919        let line = self.peek_line();
2920        self.advance();
2921        self.expect(&Token::LParen)?;
2922        let topic = self.parse_expression()?;
2923        self.expect(&Token::RParen)?;
2924        let body = self.parse_block()?;
2925        self.eat(&Token::Semicolon);
2926        Ok(Statement {
2927            label: None,
2928            kind: StmtKind::Given { topic, body },
2929            line,
2930        })
2931    }
2932
2933    /// `when (COND) { ... }` — only meaningful inside `given`
2934    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
2935        let line = self.peek_line();
2936        self.advance();
2937        self.expect(&Token::LParen)?;
2938        let cond = self.parse_expression()?;
2939        self.expect(&Token::RParen)?;
2940        let body = self.parse_block()?;
2941        self.eat(&Token::Semicolon);
2942        Ok(Statement {
2943            label: None,
2944            kind: StmtKind::When { cond, body },
2945            line,
2946        })
2947    }
2948
2949    /// `default { ... }` — only meaningful inside `given`
2950    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
2951        let line = self.peek_line();
2952        self.advance();
2953        let body = self.parse_block()?;
2954        self.eat(&Token::Semicolon);
2955        Ok(Statement {
2956            label: None,
2957            kind: StmtKind::DefaultCase { body },
2958            line,
2959        })
2960    }
2961
2962    /// `cond { EXPR => RESULT, ..., default => RESULT }`
2963    ///
2964    /// Desugars to an if/elsif/else chain at parse time.
2965    /// Each arm is `condition => { body }` or `condition => expr`.
2966    /// `default => ...` becomes the else branch.
2967    fn parse_cond_expr(&mut self, line: usize) -> PerlResult<Expr> {
2968        self.expect(&Token::LBrace)?;
2969
2970        let mut arms: Vec<(Expr, Block)> = Vec::new();
2971        let mut else_block: Option<Block> = None;
2972
2973        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2974            let arm_line = self.peek_line();
2975
2976            // Check for `default =>`
2977            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
2978                && matches!(self.peek_at(1), Token::FatArrow);
2979
2980            if is_default {
2981                self.advance(); // consume `default`
2982                self.advance(); // consume `=>`
2983                let body = if matches!(self.peek(), Token::LBrace) {
2984                    self.parse_block()?
2985                } else {
2986                    let expr = self.parse_assign_expr()?;
2987                    vec![Statement {
2988                        label: None,
2989                        kind: StmtKind::Expression(expr),
2990                        line: arm_line,
2991                    }]
2992                };
2993                else_block = Some(body);
2994                self.eat(&Token::Comma);
2995                break; // default must be last
2996            }
2997
2998            // Parse condition expression (stop before `=>`)
2999            let condition = self.parse_assign_expr()?;
3000            self.expect(&Token::FatArrow)?;
3001
3002            let body = if matches!(self.peek(), Token::LBrace) {
3003                self.parse_block()?
3004            } else {
3005                let expr = self.parse_assign_expr()?;
3006                vec![Statement {
3007                    label: None,
3008                    kind: StmtKind::Expression(expr),
3009                    line: arm_line,
3010                }]
3011            };
3012
3013            arms.push((condition, body));
3014            self.eat(&Token::Comma);
3015        }
3016
3017        self.expect(&Token::RBrace)?;
3018
3019        if arms.is_empty() {
3020            return Err(self.syntax_err("cond requires at least one condition arm", line));
3021        }
3022
3023        // Build if/elsif/else chain from the arms.
3024        let (first_cond, first_body) = arms.remove(0);
3025        let elsifs: Vec<(Expr, Block)> = arms;
3026
3027        // Wrap in a do-block so `cond { ... }` is an expression.
3028        let if_stmt = Statement {
3029            label: None,
3030            kind: StmtKind::If {
3031                condition: first_cond,
3032                body: first_body,
3033                elsifs,
3034                else_block,
3035            },
3036            line,
3037        };
3038        let inner = Expr {
3039            kind: ExprKind::CodeRef {
3040                params: vec![],
3041                body: vec![if_stmt],
3042            },
3043            line,
3044        };
3045        Ok(Expr {
3046            kind: ExprKind::Do(Box::new(inner)),
3047            line,
3048        })
3049    }
3050
3051    /// `match (EXPR) { PATTERN => EXPR, ... }`
3052    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
3053        self.expect(&Token::LParen)?;
3054        let subject = self.parse_expression()?;
3055        self.expect(&Token::RParen)?;
3056        self.expect(&Token::LBrace)?;
3057        let mut arms = Vec::new();
3058        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3059            if self.eat(&Token::Semicolon) {
3060                continue;
3061            }
3062            let pattern = self.parse_match_pattern()?;
3063            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3064                self.advance();
3065                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3066                // separator (see [`Self::parse_comma_expr`]).
3067                Some(Box::new(self.parse_assign_expr()?))
3068            } else {
3069                None
3070            };
3071            self.expect(&Token::FatArrow)?;
3072            // Use assign-level parsing so commas separate arms, not `List` elements.
3073            let body = self.parse_assign_expr()?;
3074            arms.push(MatchArm {
3075                pattern,
3076                guard,
3077                body,
3078            });
3079            self.eat(&Token::Comma);
3080        }
3081        self.expect(&Token::RBrace)?;
3082        Ok(Expr {
3083            kind: ExprKind::AlgebraicMatch {
3084                subject: Box::new(subject),
3085                arms,
3086            },
3087            line,
3088        })
3089    }
3090
3091    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
3092        match self.peek().clone() {
3093            Token::Regex(pattern, flags, _delim) => {
3094                self.advance();
3095                Ok(MatchPattern::Regex { pattern, flags })
3096            }
3097            Token::Ident(ref s) if s == "_" => {
3098                self.advance();
3099                Ok(MatchPattern::Any)
3100            }
3101            Token::Ident(ref s) if s == "Some" => {
3102                self.advance();
3103                self.expect(&Token::LParen)?;
3104                let name = self.parse_scalar_var_name()?;
3105                self.expect(&Token::RParen)?;
3106                Ok(MatchPattern::OptionSome(name))
3107            }
3108            Token::LBracket => self.parse_match_array_pattern(),
3109            Token::LBrace => self.parse_match_hash_pattern(),
3110            Token::LParen => {
3111                self.advance();
3112                let e = self.parse_expression()?;
3113                self.expect(&Token::RParen)?;
3114                Ok(MatchPattern::Value(Box::new(e)))
3115            }
3116            _ => {
3117                let e = self.parse_assign_expr()?;
3118                Ok(MatchPattern::Value(Box::new(e)))
3119            }
3120        }
3121    }
3122
3123    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3124    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
3125        let mut elems = Vec::new();
3126        if self.eat(&Token::RBracket) {
3127            return Ok(vec![]);
3128        }
3129        loop {
3130            if matches!(self.peek(), Token::Star) {
3131                self.advance();
3132                elems.push(MatchArrayElem::Rest);
3133                self.eat(&Token::Comma);
3134                if !matches!(self.peek(), Token::RBracket) {
3135                    return Err(self.syntax_err(
3136                        "`*` must be the last element in an array match pattern",
3137                        self.peek_line(),
3138                    ));
3139                }
3140                self.expect(&Token::RBracket)?;
3141                return Ok(elems);
3142            }
3143            if let Token::ArrayVar(name) = self.peek().clone() {
3144                self.advance();
3145                elems.push(MatchArrayElem::RestBind(name));
3146                self.eat(&Token::Comma);
3147                if !matches!(self.peek(), Token::RBracket) {
3148                    return Err(self.syntax_err(
3149                        "`@name` rest bind must be the last element in an array match pattern",
3150                        self.peek_line(),
3151                    ));
3152                }
3153                self.expect(&Token::RBracket)?;
3154                return Ok(elems);
3155            }
3156            if let Token::ScalarVar(name) = self.peek().clone() {
3157                self.advance();
3158                elems.push(MatchArrayElem::CaptureScalar(name));
3159                if self.eat(&Token::Comma) {
3160                    if matches!(self.peek(), Token::RBracket) {
3161                        break;
3162                    }
3163                    continue;
3164                }
3165                break;
3166            }
3167            let e = self.parse_assign_expr()?;
3168            elems.push(MatchArrayElem::Expr(e));
3169            if self.eat(&Token::Comma) {
3170                if matches!(self.peek(), Token::RBracket) {
3171                    break;
3172                }
3173                continue;
3174            }
3175            break;
3176        }
3177        self.expect(&Token::RBracket)?;
3178        Ok(elems)
3179    }
3180
3181    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
3182        self.expect(&Token::LBracket)?;
3183        let elems = self.parse_match_array_elems_until_rbracket()?;
3184        Ok(MatchPattern::Array(elems))
3185    }
3186
3187    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
3188        self.expect(&Token::LBrace)?;
3189        let mut pairs = Vec::new();
3190        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3191            if self.eat(&Token::Semicolon) {
3192                continue;
3193            }
3194            let key = self.parse_assign_expr()?;
3195            self.expect(&Token::FatArrow)?;
3196            match self.advance().0 {
3197                Token::Ident(ref s) if s == "_" => {
3198                    pairs.push(MatchHashPair::KeyOnly { key });
3199                }
3200                Token::ScalarVar(name) => {
3201                    pairs.push(MatchHashPair::Capture { key, name });
3202                }
3203                tok => {
3204                    return Err(self.syntax_err(
3205                        format!(
3206                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3207                            tok
3208                        ),
3209                        self.peek_line(),
3210                    ));
3211                }
3212            }
3213            self.eat(&Token::Comma);
3214        }
3215        self.expect(&Token::RBrace)?;
3216        Ok(MatchPattern::Hash(pairs))
3217    }
3218
3219    /// `eval_timeout SECS { ... }`
3220    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
3221        let line = self.peek_line();
3222        self.advance();
3223        let timeout = self.parse_postfix()?;
3224        let body = self.parse_block_or_bareword_block_no_args()?;
3225        self.eat(&Token::Semicolon);
3226        Ok(Statement {
3227            label: None,
3228            kind: StmtKind::EvalTimeout { timeout, body },
3229            line,
3230        })
3231    }
3232
3233    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3234        match &mut cond.kind {
3235            ExprKind::Match {
3236                flags, scalar_g, ..
3237            } if flags.contains('g') => {
3238                *scalar_g = true;
3239            }
3240            ExprKind::UnaryOp {
3241                op: UnaryOp::LogNot,
3242                expr,
3243            } => {
3244                if let ExprKind::Match {
3245                    flags, scalar_g, ..
3246                } = &mut expr.kind
3247                {
3248                    if flags.contains('g') {
3249                        *scalar_g = true;
3250                    }
3251                }
3252            }
3253            _ => {}
3254        }
3255    }
3256
3257    fn parse_if(&mut self) -> PerlResult<Statement> {
3258        let line = self.peek_line();
3259        self.advance(); // 'if'
3260        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3261            if crate::compat_mode() {
3262                return Err(self.syntax_err(
3263                    "`if let` is a stryke extension (disabled by --compat)",
3264                    line,
3265                ));
3266            }
3267            return self.parse_if_let(line);
3268        }
3269        self.expect(&Token::LParen)?;
3270        let mut cond = self.parse_expression()?;
3271        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3272        self.expect(&Token::RParen)?;
3273        let body = self.parse_block()?;
3274
3275        let mut elsifs = Vec::new();
3276        let mut else_block = None;
3277
3278        loop {
3279            if let Token::Ident(ref kw) = self.peek().clone() {
3280                if kw == "elsif" {
3281                    self.advance();
3282                    self.expect(&Token::LParen)?;
3283                    let mut c = self.parse_expression()?;
3284                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3285                    self.expect(&Token::RParen)?;
3286                    let b = self.parse_block()?;
3287                    elsifs.push((c, b));
3288                    continue;
3289                }
3290                if kw == "else" {
3291                    self.advance();
3292                    else_block = Some(self.parse_block()?);
3293                }
3294            }
3295            break;
3296        }
3297
3298        Ok(Statement {
3299            label: None,
3300            kind: StmtKind::If {
3301                condition: cond,
3302                body,
3303                elsifs,
3304                else_block,
3305            },
3306            line,
3307        })
3308    }
3309
3310    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3311    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3312        self.advance(); // `let`
3313        let pattern = self.parse_match_pattern()?;
3314        self.expect(&Token::Assign)?;
3315        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3316        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3317        let rhs = self.parse_assign_expr();
3318        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3319        let rhs = rhs?;
3320        let then_block = self.parse_block()?;
3321        let else_block_opt = match self.peek().clone() {
3322            Token::Ident(ref kw) if kw == "else" => {
3323                self.advance();
3324                Some(self.parse_block()?)
3325            }
3326            Token::Ident(ref kw) if kw == "elsif" => {
3327                return Err(self.syntax_err(
3328                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3329                    self.peek_line(),
3330                ));
3331            }
3332            _ => None,
3333        };
3334        let then_expr = Self::expr_do_anon_block(then_block, line);
3335        let else_expr = if let Some(eb) = else_block_opt {
3336            Self::expr_do_anon_block(eb, line)
3337        } else {
3338            Expr {
3339                kind: ExprKind::Undef,
3340                line,
3341            }
3342        };
3343        let arms = vec![
3344            MatchArm {
3345                pattern,
3346                guard: None,
3347                body: then_expr,
3348            },
3349            MatchArm {
3350                pattern: MatchPattern::Any,
3351                guard: None,
3352                body: else_expr,
3353            },
3354        ];
3355        Ok(Statement {
3356            label: None,
3357            kind: StmtKind::Expression(Expr {
3358                kind: ExprKind::AlgebraicMatch {
3359                    subject: Box::new(rhs),
3360                    arms,
3361                },
3362                line,
3363            }),
3364            line,
3365        })
3366    }
3367
3368    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3369        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3370        Expr {
3371            kind: ExprKind::Do(Box::new(Expr {
3372                kind: ExprKind::CodeRef {
3373                    params: vec![],
3374                    body: block,
3375                },
3376                line: inner_line,
3377            })),
3378            line: outer_line,
3379        }
3380    }
3381
3382    fn parse_unless(&mut self) -> PerlResult<Statement> {
3383        let line = self.peek_line();
3384        self.advance(); // 'unless'
3385        self.expect(&Token::LParen)?;
3386        let mut cond = self.parse_expression()?;
3387        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3388        self.expect(&Token::RParen)?;
3389        let body = self.parse_block()?;
3390        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3391            if kw == "else" {
3392                self.advance();
3393                Some(self.parse_block()?)
3394            } else {
3395                None
3396            }
3397        } else {
3398            None
3399        };
3400        Ok(Statement {
3401            label: None,
3402            kind: StmtKind::Unless {
3403                condition: cond,
3404                body,
3405                else_block,
3406            },
3407            line,
3408        })
3409    }
3410
3411    fn parse_while(&mut self) -> PerlResult<Statement> {
3412        let line = self.peek_line();
3413        self.advance(); // 'while'
3414        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3415            if crate::compat_mode() {
3416                return Err(self.syntax_err(
3417                    "`while let` is a stryke extension (disabled by --compat)",
3418                    line,
3419                ));
3420            }
3421            return self.parse_while_let(line);
3422        }
3423        self.expect(&Token::LParen)?;
3424        let mut cond = self.parse_expression()?;
3425        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3426        self.expect(&Token::RParen)?;
3427        let body = self.parse_block()?;
3428        let continue_block = self.parse_optional_continue_block()?;
3429        Ok(Statement {
3430            label: None,
3431            kind: StmtKind::While {
3432                condition: cond,
3433                body,
3434                label: None,
3435                continue_block,
3436            },
3437            line,
3438        })
3439    }
3440
3441    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3442    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3443    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3444        self.advance(); // `let`
3445        let pattern = self.parse_match_pattern()?;
3446        self.expect(&Token::Assign)?;
3447        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3448        let rhs = self.parse_assign_expr();
3449        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3450        let rhs = rhs?;
3451        let mut user_body = self.parse_block()?;
3452        let continue_block = self.parse_optional_continue_block()?;
3453        user_body.push(Statement::new(
3454            StmtKind::Expression(Expr {
3455                kind: ExprKind::Integer(1),
3456                line,
3457            }),
3458            line,
3459        ));
3460        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3461        let match_expr = Expr {
3462            kind: ExprKind::AlgebraicMatch {
3463                subject: Box::new(rhs),
3464                arms: vec![
3465                    MatchArm {
3466                        pattern,
3467                        guard: None,
3468                        body: Self::expr_do_anon_block(user_body, line),
3469                    },
3470                    MatchArm {
3471                        pattern: MatchPattern::Any,
3472                        guard: None,
3473                        body: Expr {
3474                            kind: ExprKind::Integer(0),
3475                            line,
3476                        },
3477                    },
3478                ],
3479            },
3480            line,
3481        };
3482        let my_stmt = Statement::new(
3483            StmtKind::My(vec![VarDecl {
3484                sigil: Sigil::Scalar,
3485                name: tmp.clone(),
3486                initializer: Some(match_expr),
3487                frozen: false,
3488                type_annotation: None,
3489            }]),
3490            line,
3491        );
3492        let unless_last = Statement::new(
3493            StmtKind::Unless {
3494                condition: Expr {
3495                    kind: ExprKind::ScalarVar(tmp),
3496                    line,
3497                },
3498                body: vec![Statement::new(StmtKind::Last(None), line)],
3499                else_block: None,
3500            },
3501            line,
3502        );
3503        Ok(Statement::new(
3504            StmtKind::While {
3505                condition: Expr {
3506                    kind: ExprKind::Integer(1),
3507                    line,
3508                },
3509                body: vec![my_stmt, unless_last],
3510                label: None,
3511                continue_block,
3512            },
3513            line,
3514        ))
3515    }
3516
3517    fn parse_until(&mut self) -> PerlResult<Statement> {
3518        let line = self.peek_line();
3519        self.advance(); // 'until'
3520        self.expect(&Token::LParen)?;
3521        let mut cond = self.parse_expression()?;
3522        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3523        self.expect(&Token::RParen)?;
3524        let body = self.parse_block()?;
3525        let continue_block = self.parse_optional_continue_block()?;
3526        Ok(Statement {
3527            label: None,
3528            kind: StmtKind::Until {
3529                condition: cond,
3530                body,
3531                label: None,
3532                continue_block,
3533            },
3534            line,
3535        })
3536    }
3537
3538    /// `continue { ... }` after a loop body (optional).
3539    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3540        if let Token::Ident(ref kw) = self.peek().clone() {
3541            if kw == "continue" {
3542                self.advance();
3543                return Ok(Some(self.parse_block()?));
3544            }
3545        }
3546        Ok(None)
3547    }
3548
3549    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3550        let line = self.peek_line();
3551        self.advance(); // 'for'
3552
3553        // Peek to determine if C-style for or foreach
3554        // C-style: for (init; cond; step)
3555        // foreach-style: for $var (list) or for (list)
3556        match self.peek() {
3557            Token::LParen => {
3558                // Check if next after ( is a semicolon or an assignment — C-style
3559                // Or if it's a list — foreach-style
3560                // Heuristic: if the token after ( is 'my' or '$' followed by
3561                // content that contains ';', it's C-style.
3562                let saved = self.pos;
3563                self.advance(); // consume (
3564                                // Look for semicolon at paren depth 0
3565                let mut depth = 1;
3566                let mut has_semi = false;
3567                let mut scan = self.pos;
3568                while scan < self.tokens.len() {
3569                    match &self.tokens[scan].0 {
3570                        Token::LParen => depth += 1,
3571                        Token::RParen => {
3572                            depth -= 1;
3573                            if depth == 0 {
3574                                break;
3575                            }
3576                        }
3577                        Token::Semicolon if depth == 1 => {
3578                            has_semi = true;
3579                            break;
3580                        }
3581                        _ => {}
3582                    }
3583                    scan += 1;
3584                }
3585                self.pos = saved;
3586
3587                if has_semi {
3588                    self.parse_c_style_for(line)
3589                } else {
3590                    // foreach without explicit var — uses $_
3591                    self.expect(&Token::LParen)?;
3592                    let list = self.parse_expression()?;
3593                    self.expect(&Token::RParen)?;
3594                    let body = self.parse_block()?;
3595                    let continue_block = self.parse_optional_continue_block()?;
3596                    Ok(Statement {
3597                        label: None,
3598                        kind: StmtKind::Foreach {
3599                            var: "_".to_string(),
3600                            list,
3601                            body,
3602                            label: None,
3603                            continue_block,
3604                        },
3605                        line,
3606                    })
3607                }
3608            }
3609            Token::Ident(ref kw) if kw == "my" => {
3610                self.advance(); // 'my'
3611                let var = self.parse_scalar_var_name()?;
3612                self.expect(&Token::LParen)?;
3613                let list = self.parse_expression()?;
3614                self.expect(&Token::RParen)?;
3615                let body = self.parse_block()?;
3616                let continue_block = self.parse_optional_continue_block()?;
3617                Ok(Statement {
3618                    label: None,
3619                    kind: StmtKind::Foreach {
3620                        var,
3621                        list,
3622                        body,
3623                        label: None,
3624                        continue_block,
3625                    },
3626                    line,
3627                })
3628            }
3629            Token::ScalarVar(_) => {
3630                let var = self.parse_scalar_var_name()?;
3631                self.expect(&Token::LParen)?;
3632                let list = self.parse_expression()?;
3633                self.expect(&Token::RParen)?;
3634                let body = self.parse_block()?;
3635                let continue_block = self.parse_optional_continue_block()?;
3636                Ok(Statement {
3637                    label: None,
3638                    kind: StmtKind::Foreach {
3639                        var,
3640                        list,
3641                        body,
3642                        label: None,
3643                        continue_block,
3644                    },
3645                    line,
3646                })
3647            }
3648            _ => self.parse_c_style_for(line),
3649        }
3650    }
3651
3652    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
3653        self.expect(&Token::LParen)?;
3654        let init = if self.eat(&Token::Semicolon) {
3655            None
3656        } else {
3657            let s = self.parse_statement()?;
3658            self.eat(&Token::Semicolon);
3659            Some(Box::new(s))
3660        };
3661        let mut condition = if matches!(self.peek(), Token::Semicolon) {
3662            None
3663        } else {
3664            Some(self.parse_expression()?)
3665        };
3666        if let Some(ref mut c) = condition {
3667            Self::mark_match_scalar_g_for_boolean_condition(c);
3668        }
3669        self.expect(&Token::Semicolon)?;
3670        let step = if matches!(self.peek(), Token::RParen) {
3671            None
3672        } else {
3673            Some(self.parse_expression()?)
3674        };
3675        self.expect(&Token::RParen)?;
3676        let body = self.parse_block()?;
3677        let continue_block = self.parse_optional_continue_block()?;
3678        Ok(Statement {
3679            label: None,
3680            kind: StmtKind::For {
3681                init,
3682                condition,
3683                step,
3684                body,
3685                label: None,
3686                continue_block,
3687            },
3688            line,
3689        })
3690    }
3691
3692    fn parse_foreach(&mut self) -> PerlResult<Statement> {
3693        let line = self.peek_line();
3694        self.advance(); // 'foreach'
3695        let var = match self.peek() {
3696            Token::Ident(ref kw) if kw == "my" => {
3697                self.advance();
3698                self.parse_scalar_var_name()?
3699            }
3700            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
3701            _ => "_".to_string(),
3702        };
3703        self.expect(&Token::LParen)?;
3704        let list = self.parse_expression()?;
3705        self.expect(&Token::RParen)?;
3706        let body = self.parse_block()?;
3707        let continue_block = self.parse_optional_continue_block()?;
3708        Ok(Statement {
3709            label: None,
3710            kind: StmtKind::Foreach {
3711                var,
3712                list,
3713                body,
3714                label: None,
3715                continue_block,
3716            },
3717            line,
3718        })
3719    }
3720
3721    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
3722        match self.advance() {
3723            (Token::ScalarVar(name), _) => Ok(name),
3724            (tok, line) => {
3725                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
3726            }
3727        }
3728    }
3729
3730    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
3731    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
3732        let mut s = String::new();
3733        loop {
3734            match self.peek().clone() {
3735                Token::RParen => {
3736                    self.advance();
3737                    break;
3738                }
3739                Token::Eof => {
3740                    return Err(self.syntax_err(
3741                        "Unterminated sub prototype (expected ')' before end of input)",
3742                        self.peek_line(),
3743                    ));
3744                }
3745                Token::ScalarVar(v) if v == ")" => {
3746                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
3747                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
3748                    self.advance();
3749                    s.push('$');
3750                    if matches!(self.peek(), Token::LBrace) {
3751                        break;
3752                    }
3753                }
3754                Token::Ident(i) => {
3755                    let i = i.clone();
3756                    self.advance();
3757                    s.push_str(&i);
3758                }
3759                Token::Semicolon => {
3760                    self.advance();
3761                    s.push(';');
3762                }
3763                Token::LParen => {
3764                    self.advance();
3765                    s.push('(');
3766                }
3767                Token::LBracket => {
3768                    self.advance();
3769                    s.push('[');
3770                }
3771                Token::RBracket => {
3772                    self.advance();
3773                    s.push(']');
3774                }
3775                Token::Backslash => {
3776                    self.advance();
3777                    s.push('\\');
3778                }
3779                Token::Comma => {
3780                    self.advance();
3781                    s.push(',');
3782                }
3783                Token::ScalarVar(v) => {
3784                    let v = v.clone();
3785                    self.advance();
3786                    s.push('$');
3787                    s.push_str(&v);
3788                }
3789                Token::ArrayVar(v) => {
3790                    let v = v.clone();
3791                    self.advance();
3792                    s.push('@');
3793                    s.push_str(&v);
3794                }
3795                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
3796                Token::ArrayAt => {
3797                    self.advance();
3798                    s.push('@');
3799                }
3800                Token::HashVar(v) => {
3801                    let v = v.clone();
3802                    self.advance();
3803                    s.push('%');
3804                    s.push_str(&v);
3805                }
3806                Token::HashPercent => {
3807                    self.advance();
3808                    s.push('%');
3809                }
3810                Token::Plus => {
3811                    self.advance();
3812                    s.push('+');
3813                }
3814                Token::Minus => {
3815                    self.advance();
3816                    s.push('-');
3817                }
3818                Token::BitAnd => {
3819                    self.advance();
3820                    s.push('&');
3821                }
3822                tok => {
3823                    return Err(self.syntax_err(
3824                        format!("Unexpected token in sub prototype: {:?}", tok),
3825                        self.peek_line(),
3826                    ));
3827                }
3828            }
3829        }
3830        Ok(s)
3831    }
3832
3833    fn sub_signature_list_starts_here(&self) -> bool {
3834        match self.peek() {
3835            Token::LBrace | Token::LBracket => true,
3836            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
3837            Token::ArrayVar(_) | Token::HashVar(_) => true,
3838            _ => false,
3839        }
3840    }
3841
3842    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
3843        let (tok, line) = self.advance();
3844        match tok {
3845            Token::Ident(i) => Ok(i),
3846            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
3847            tok => Err(self.syntax_err(
3848                format!(
3849                    "sub signature: expected hash key (identifier or string), got {:?}",
3850                    tok
3851                ),
3852                line,
3853            )),
3854        }
3855    }
3856
3857    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
3858        let mut params = Vec::new();
3859        loop {
3860            if matches!(self.peek(), Token::RParen) {
3861                break;
3862            }
3863            match self.peek().clone() {
3864                Token::ScalarVar(name) => {
3865                    if name == "$$" || name == ")" {
3866                        return Err(self.syntax_err(
3867                            format!(
3868                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
3869                            ),
3870                            self.peek_line(),
3871                        ));
3872                    }
3873                    self.advance();
3874                    let ty = if self.eat(&Token::Colon) {
3875                        match self.peek() {
3876                            Token::Ident(ref tname) => {
3877                                let tname = tname.clone();
3878                                self.advance();
3879                                Some(match tname.as_str() {
3880                                    "Int" => PerlTypeName::Int,
3881                                    "Str" => PerlTypeName::Str,
3882                                    "Float" => PerlTypeName::Float,
3883                                    "Bool" => PerlTypeName::Bool,
3884                                    "Array" => PerlTypeName::Array,
3885                                    "Hash" => PerlTypeName::Hash,
3886                                    "Ref" => PerlTypeName::Ref,
3887                                    "Any" => PerlTypeName::Any,
3888                                    _ => PerlTypeName::Struct(tname),
3889                                })
3890                            }
3891                            _ => {
3892                                return Err(self.syntax_err(
3893                                    "expected type name after `:` in sub signature",
3894                                    self.peek_line(),
3895                                ));
3896                            }
3897                        }
3898                    } else {
3899                        None
3900                    };
3901                    // Check for default value: `$x = expr`
3902                    let default = if self.eat(&Token::Assign) {
3903                        Some(Box::new(self.parse_ternary()?))
3904                    } else {
3905                        None
3906                    };
3907                    params.push(SubSigParam::Scalar(name, ty, default));
3908                }
3909                Token::ArrayVar(name) => {
3910                    self.advance();
3911                    let default = if self.eat(&Token::Assign) {
3912                        Some(Box::new(self.parse_ternary()?))
3913                    } else {
3914                        None
3915                    };
3916                    params.push(SubSigParam::Array(name, default));
3917                }
3918                Token::HashVar(name) => {
3919                    self.advance();
3920                    let default = if self.eat(&Token::Assign) {
3921                        Some(Box::new(self.parse_ternary()?))
3922                    } else {
3923                        None
3924                    };
3925                    params.push(SubSigParam::Hash(name, default));
3926                }
3927                Token::LBracket => {
3928                    self.advance();
3929                    let elems = self.parse_match_array_elems_until_rbracket()?;
3930                    params.push(SubSigParam::ArrayDestruct(elems));
3931                }
3932                Token::LBrace => {
3933                    self.advance();
3934                    let mut pairs = Vec::new();
3935                    loop {
3936                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
3937                            break;
3938                        }
3939                        if self.eat(&Token::Comma) {
3940                            continue;
3941                        }
3942                        let key = self.parse_sub_signature_hash_key()?;
3943                        self.expect(&Token::FatArrow)?;
3944                        let bind = self.parse_scalar_var_name()?;
3945                        pairs.push((key, bind));
3946                        self.eat(&Token::Comma);
3947                    }
3948                    self.expect(&Token::RBrace)?;
3949                    params.push(SubSigParam::HashDestruct(pairs));
3950                }
3951                tok => {
3952                    return Err(self.syntax_err(
3953                        format!(
3954                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
3955                            tok
3956                        ),
3957                        self.peek_line(),
3958                    ));
3959                }
3960            }
3961            match self.peek() {
3962                Token::Comma => {
3963                    self.advance();
3964                    if matches!(self.peek(), Token::RParen) {
3965                        return Err(self.syntax_err(
3966                            "trailing `,` before `)` in sub signature",
3967                            self.peek_line(),
3968                        ));
3969                    }
3970                }
3971                Token::RParen => break,
3972                _ => {
3973                    return Err(self.syntax_err(
3974                        format!(
3975                            "expected `,` or `)` after sub signature parameter, got {:?}",
3976                            self.peek()
3977                        ),
3978                        self.peek_line(),
3979                    ));
3980                }
3981            }
3982        }
3983        Ok(params)
3984    }
3985
3986    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
3987    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
3988        if !matches!(self.peek(), Token::LParen) {
3989            return Ok((vec![], None));
3990        }
3991        self.advance();
3992        if matches!(self.peek(), Token::RParen) {
3993            self.advance();
3994            return Ok((vec![], Some(String::new())));
3995        }
3996        if self.sub_signature_list_starts_here() {
3997            let params = self.parse_sub_signature_param_list()?;
3998            self.expect(&Token::RParen)?;
3999            return Ok((params, None));
4000        }
4001        let proto = self.parse_legacy_sub_prototype_tail()?;
4002        Ok((vec![], Some(proto)))
4003    }
4004
4005    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4006    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
4007        while self.eat(&Token::Colon) {
4008            match self.advance() {
4009                (Token::Ident(_), _) => {}
4010                (tok, line) => {
4011                    return Err(self.syntax_err(
4012                        format!("Expected attribute name after `:`, got {:?}", tok),
4013                        line,
4014                    ));
4015                }
4016            }
4017            if self.eat(&Token::LParen) {
4018                let mut depth = 1usize;
4019                while depth > 0 {
4020                    match self.advance().0 {
4021                        Token::LParen => depth += 1,
4022                        Token::RParen => {
4023                            depth -= 1;
4024                        }
4025                        Token::Eof => {
4026                            return Err(self.syntax_err(
4027                                "Unterminated sub attribute argument list",
4028                                self.peek_line(),
4029                            ));
4030                        }
4031                        _ => {}
4032                    }
4033                }
4034            }
4035        }
4036        Ok(())
4037    }
4038
4039    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> PerlResult<Statement> {
4040        let line = self.peek_line();
4041        self.advance(); // 'sub' or 'fn'
4042        match self.peek().clone() {
4043            Token::Ident(_) => {
4044                let name = self.parse_package_qualified_identifier()?;
4045                if !crate::compat_mode() {
4046                    self.check_udf_shadows_builtin(&name, line)?;
4047                }
4048                self.declared_subs.insert(name.clone());
4049                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4050                self.parse_sub_attributes()?;
4051                let body = self.parse_block()?;
4052                Ok(Statement {
4053                    label: None,
4054                    kind: StmtKind::SubDecl {
4055                        name,
4056                        params,
4057                        body,
4058                        prototype,
4059                    },
4060                    line,
4061                })
4062            }
4063            Token::LParen | Token::LBrace | Token::Colon => {
4064                // In non-compat mode, `fn {}` anonymous is not allowed — must use `fn {}`
4065                if is_sub_keyword && !crate::compat_mode() {
4066                    return Err(self.syntax_err(
4067                        "stryke uses `fn {}` instead of `fn {}` (this is not Perl 5)",
4068                        line,
4069                    ));
4070                }
4071                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4072                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4073                self.parse_sub_attributes()?;
4074                let body = self.parse_block()?;
4075                Ok(Statement {
4076                    label: None,
4077                    kind: StmtKind::Expression(Expr {
4078                        kind: ExprKind::CodeRef { params, body },
4079                        line,
4080                    }),
4081                    line,
4082                })
4083            }
4084            tok => Err(self.syntax_err(
4085                format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4086                self.peek_line(),
4087            )),
4088        }
4089    }
4090
4091    /// `struct Name { field => Type, ... ; fn method { } }`
4092    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
4093        let line = self.peek_line();
4094        self.advance(); // struct
4095        let name = match self.advance() {
4096            (Token::Ident(n), _) => n,
4097            (tok, err_line) => {
4098                return Err(
4099                    self.syntax_err(format!("Expected struct name, got {:?}", tok), err_line)
4100                )
4101            }
4102        };
4103        self.expect(&Token::LBrace)?;
4104        let mut fields = Vec::new();
4105        let mut methods = Vec::new();
4106        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4107            // Check for method definition: `fn name { }` or `fn name { }`
4108            let is_method = match self.peek() {
4109                Token::Ident(s) => s == "fn" || s == "sub",
4110                _ => false,
4111            };
4112            if is_method {
4113                self.advance(); // fn/sub
4114                let method_name = match self.advance() {
4115                    (Token::Ident(n), _) => n,
4116                    (tok, err_line) => {
4117                        return Err(self
4118                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4119                    }
4120                };
4121                // Parse optional signature: `($self, $arg: Type, ...)`
4122                let params = if self.eat(&Token::LParen) {
4123                    let p = self.parse_sub_signature_param_list()?;
4124                    self.expect(&Token::RParen)?;
4125                    p
4126                } else {
4127                    Vec::new()
4128                };
4129                // parse_block handles its own { } delimiters
4130                let body = self.parse_block()?;
4131                methods.push(crate::ast::StructMethod {
4132                    name: method_name,
4133                    params,
4134                    body,
4135                });
4136                // Optional trailing comma/semicolon after method
4137                self.eat(&Token::Comma);
4138                self.eat(&Token::Semicolon);
4139                continue;
4140            }
4141
4142            let field_name = match self.advance() {
4143                (Token::Ident(n), _) => n,
4144                (tok, err_line) => {
4145                    return Err(
4146                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4147                    )
4148                }
4149            };
4150            // Support both `field => Type` and bare `field` (implies Any type)
4151            let ty = if self.eat(&Token::FatArrow) {
4152                self.parse_type_name()?
4153            } else {
4154                crate::ast::PerlTypeName::Any
4155            };
4156            let default = if self.eat(&Token::Assign) {
4157                // Use parse_ternary to avoid consuming commas (next field separator)
4158                Some(self.parse_ternary()?)
4159            } else {
4160                None
4161            };
4162            fields.push(StructField {
4163                name: field_name,
4164                ty,
4165                default,
4166            });
4167            if !self.eat(&Token::Comma) {
4168                // Also allow semicolons as field separators
4169                self.eat(&Token::Semicolon);
4170            }
4171        }
4172        self.expect(&Token::RBrace)?;
4173        self.eat(&Token::Semicolon);
4174        Ok(Statement {
4175            label: None,
4176            kind: StmtKind::StructDecl {
4177                def: StructDef {
4178                    name,
4179                    fields,
4180                    methods,
4181                },
4182            },
4183            line,
4184        })
4185    }
4186
4187    /// `enum Name { Variant1, Variant2 => Type, ... }`
4188    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
4189        let line = self.peek_line();
4190        self.advance(); // enum
4191        let name = match self.advance() {
4192            (Token::Ident(n), _) => n,
4193            (tok, err_line) => {
4194                return Err(self.syntax_err(format!("Expected enum name, got {:?}", tok), err_line))
4195            }
4196        };
4197        self.expect(&Token::LBrace)?;
4198        let mut variants = Vec::new();
4199        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4200            let variant_name = match self.advance() {
4201                (Token::Ident(n), _) => n,
4202                (tok, err_line) => {
4203                    return Err(
4204                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
4205                    )
4206                }
4207            };
4208            let ty = if self.eat(&Token::FatArrow) {
4209                Some(self.parse_type_name()?)
4210            } else {
4211                None
4212            };
4213            variants.push(EnumVariant {
4214                name: variant_name,
4215                ty,
4216            });
4217            if !self.eat(&Token::Comma) {
4218                self.eat(&Token::Semicolon);
4219            }
4220        }
4221        self.expect(&Token::RBrace)?;
4222        self.eat(&Token::Semicolon);
4223        Ok(Statement {
4224            label: None,
4225            kind: StmtKind::EnumDecl {
4226                def: EnumDef { name, variants },
4227            },
4228            line,
4229        })
4230    }
4231
4232    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
4233    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
4234        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
4235        let line = self.peek_line();
4236        self.advance(); // class
4237        let name = match self.advance() {
4238            (Token::Ident(n), _) => n,
4239            (tok, err_line) => {
4240                return Err(self.syntax_err(format!("Expected class name, got {:?}", tok), err_line))
4241            }
4242        };
4243
4244        // Parse `extends Parent1, Parent2`
4245        let mut extends = Vec::new();
4246        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
4247            self.advance(); // extends
4248            loop {
4249                match self.advance() {
4250                    (Token::Ident(parent), _) => extends.push(parent),
4251                    (tok, err_line) => {
4252                        return Err(self.syntax_err(
4253                            format!("Expected parent class name after `extends`, got {:?}", tok),
4254                            err_line,
4255                        ))
4256                    }
4257                }
4258                if !self.eat(&Token::Comma) {
4259                    break;
4260                }
4261            }
4262        }
4263
4264        // Parse `impl Trait1, Trait2`
4265        let mut implements = Vec::new();
4266        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
4267            self.advance(); // impl
4268            loop {
4269                match self.advance() {
4270                    (Token::Ident(trait_name), _) => implements.push(trait_name),
4271                    (tok, err_line) => {
4272                        return Err(self.syntax_err(
4273                            format!("Expected trait name after `impl`, got {:?}", tok),
4274                            err_line,
4275                        ))
4276                    }
4277                }
4278                if !self.eat(&Token::Comma) {
4279                    break;
4280                }
4281            }
4282        }
4283
4284        self.expect(&Token::LBrace)?;
4285        let mut fields = Vec::new();
4286        let mut methods = Vec::new();
4287        let mut static_fields = Vec::new();
4288
4289        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4290            // Check for visibility modifier
4291            let visibility = match self.peek() {
4292                Token::Ident(ref s) if s == "pub" => {
4293                    self.advance();
4294                    Visibility::Public
4295                }
4296                Token::Ident(ref s) if s == "priv" => {
4297                    self.advance();
4298                    Visibility::Private
4299                }
4300                Token::Ident(ref s) if s == "prot" => {
4301                    self.advance();
4302                    Visibility::Protected
4303                }
4304                _ => Visibility::Public, // default public
4305            };
4306
4307            // Check for static field: `static name: Type = default`
4308            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
4309                self.advance(); // static
4310
4311                // Could be a static method (`static fn`) or static field
4312                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4313                    // static fn is same as fn Self.name — handled below but not here
4314                    return Err(self.syntax_err(
4315                        "use `fn Self.name` for static methods, not `static fn`",
4316                        self.peek_line(),
4317                    ));
4318                }
4319
4320                let field_name = match self.advance() {
4321                    (Token::Ident(n), _) => n,
4322                    (tok, err_line) => {
4323                        return Err(self.syntax_err(
4324                            format!("Expected static field name, got {:?}", tok),
4325                            err_line,
4326                        ))
4327                    }
4328                };
4329
4330                let ty = if self.eat(&Token::Colon) {
4331                    self.parse_type_name()?
4332                } else {
4333                    crate::ast::PerlTypeName::Any
4334                };
4335
4336                let default = if self.eat(&Token::Assign) {
4337                    Some(self.parse_ternary()?)
4338                } else {
4339                    None
4340                };
4341
4342                static_fields.push(ClassStaticField {
4343                    name: field_name,
4344                    ty,
4345                    visibility,
4346                    default,
4347                });
4348
4349                if !self.eat(&Token::Comma) {
4350                    self.eat(&Token::Semicolon);
4351                }
4352                continue;
4353            }
4354
4355            // Check for `final` modifier before fn
4356            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
4357            if method_is_final {
4358                self.advance(); // final
4359            }
4360
4361            // Check for method: `fn name` or `fn Self.name` (static)
4362            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
4363            if is_method {
4364                self.advance(); // fn/sub
4365
4366                // Check for static method: `fn Self.name`
4367                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
4368                if is_static {
4369                    self.advance(); // Self
4370                    self.expect(&Token::Dot)?;
4371                }
4372
4373                let method_name = match self.advance() {
4374                    (Token::Ident(n), _) => n,
4375                    (tok, err_line) => {
4376                        return Err(self
4377                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4378                    }
4379                };
4380
4381                // Parse optional signature
4382                let params = if self.eat(&Token::LParen) {
4383                    let p = self.parse_sub_signature_param_list()?;
4384                    self.expect(&Token::RParen)?;
4385                    p
4386                } else {
4387                    Vec::new()
4388                };
4389
4390                // Body is optional (abstract method in trait has no body)
4391                let body = if matches!(self.peek(), Token::LBrace) {
4392                    Some(self.parse_block()?)
4393                } else {
4394                    None
4395                };
4396
4397                methods.push(ClassMethod {
4398                    name: method_name,
4399                    params,
4400                    body,
4401                    visibility,
4402                    is_static,
4403                    is_final: method_is_final,
4404                });
4405                self.eat(&Token::Comma);
4406                self.eat(&Token::Semicolon);
4407                continue;
4408            } else if method_is_final {
4409                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
4410            }
4411
4412            // Parse field: `name: Type = default`
4413            let field_name = match self.advance() {
4414                (Token::Ident(n), _) => n,
4415                (tok, err_line) => {
4416                    return Err(
4417                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4418                    )
4419                }
4420            };
4421
4422            // Type after colon: `name: Type`
4423            let ty = if self.eat(&Token::Colon) {
4424                self.parse_type_name()?
4425            } else {
4426                crate::ast::PerlTypeName::Any
4427            };
4428
4429            // Default value after `=`
4430            let default = if self.eat(&Token::Assign) {
4431                Some(self.parse_ternary()?)
4432            } else {
4433                None
4434            };
4435
4436            fields.push(ClassField {
4437                name: field_name,
4438                ty,
4439                visibility,
4440                default,
4441            });
4442
4443            if !self.eat(&Token::Comma) {
4444                self.eat(&Token::Semicolon);
4445            }
4446        }
4447
4448        self.expect(&Token::RBrace)?;
4449        self.eat(&Token::Semicolon);
4450
4451        Ok(Statement {
4452            label: None,
4453            kind: StmtKind::ClassDecl {
4454                def: ClassDef {
4455                    name,
4456                    is_abstract,
4457                    is_final,
4458                    extends,
4459                    implements,
4460                    fields,
4461                    methods,
4462                    static_fields,
4463                },
4464            },
4465            line,
4466        })
4467    }
4468
4469    /// `trait Name { fn required; fn with_default { } }`
4470    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
4471        use crate::ast::{ClassMethod, TraitDef, Visibility};
4472        let line = self.peek_line();
4473        self.advance(); // trait
4474        let name = match self.advance() {
4475            (Token::Ident(n), _) => n,
4476            (tok, err_line) => {
4477                return Err(self.syntax_err(format!("Expected trait name, got {:?}", tok), err_line))
4478            }
4479        };
4480
4481        self.expect(&Token::LBrace)?;
4482        let mut methods = Vec::new();
4483
4484        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4485            // Optional visibility
4486            let visibility = match self.peek() {
4487                Token::Ident(ref s) if s == "pub" => {
4488                    self.advance();
4489                    Visibility::Public
4490                }
4491                Token::Ident(ref s) if s == "priv" => {
4492                    self.advance();
4493                    Visibility::Private
4494                }
4495                Token::Ident(ref s) if s == "prot" => {
4496                    self.advance();
4497                    Visibility::Protected
4498                }
4499                _ => Visibility::Public,
4500            };
4501
4502            // Expect `fn` or `sub`
4503            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4504                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
4505            }
4506            self.advance(); // fn/sub
4507
4508            let method_name = match self.advance() {
4509                (Token::Ident(n), _) => n,
4510                (tok, err_line) => {
4511                    return Err(
4512                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
4513                    )
4514                }
4515            };
4516
4517            // Optional signature
4518            let params = if self.eat(&Token::LParen) {
4519                let p = self.parse_sub_signature_param_list()?;
4520                self.expect(&Token::RParen)?;
4521                p
4522            } else {
4523                Vec::new()
4524            };
4525
4526            // Body is optional (no body = abstract/required method)
4527            let body = if matches!(self.peek(), Token::LBrace) {
4528                Some(self.parse_block()?)
4529            } else {
4530                None
4531            };
4532
4533            methods.push(ClassMethod {
4534                name: method_name,
4535                params,
4536                body,
4537                visibility,
4538                is_static: false,
4539                is_final: false,
4540            });
4541
4542            self.eat(&Token::Comma);
4543            self.eat(&Token::Semicolon);
4544        }
4545
4546        self.expect(&Token::RBrace)?;
4547        self.eat(&Token::Semicolon);
4548
4549        Ok(Statement {
4550            label: None,
4551            kind: StmtKind::TraitDecl {
4552                def: TraitDef { name, methods },
4553            },
4554            line,
4555        })
4556    }
4557
4558    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
4559        match &target.kind {
4560            ExprKind::ScalarVar(name) => Some(VarDecl {
4561                sigil: Sigil::Scalar,
4562                name: name.clone(),
4563                initializer: None,
4564                frozen: false,
4565                type_annotation: None,
4566            }),
4567            ExprKind::ArrayVar(name) => Some(VarDecl {
4568                sigil: Sigil::Array,
4569                name: name.clone(),
4570                initializer: None,
4571                frozen: false,
4572                type_annotation: None,
4573            }),
4574            ExprKind::HashVar(name) => Some(VarDecl {
4575                sigil: Sigil::Hash,
4576                name: name.clone(),
4577                initializer: None,
4578                frozen: false,
4579                type_annotation: None,
4580            }),
4581            ExprKind::Typeglob(name) => Some(VarDecl {
4582                sigil: Sigil::Typeglob,
4583                name: name.clone(),
4584                initializer: None,
4585                frozen: false,
4586                type_annotation: None,
4587            }),
4588            _ => None,
4589        }
4590    }
4591
4592    fn parse_decl_array_destructure(
4593        &mut self,
4594        keyword: &str,
4595        line: usize,
4596    ) -> PerlResult<Statement> {
4597        self.expect(&Token::LBracket)?;
4598        let elems = self.parse_match_array_elems_until_rbracket()?;
4599        self.expect(&Token::Assign)?;
4600        self.suppress_scalar_hash_brace += 1;
4601        let rhs = self.parse_expression()?;
4602        self.suppress_scalar_hash_brace -= 1;
4603        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
4604        self.parse_stmt_postfix_modifier(stmt)
4605    }
4606
4607    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
4608        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
4609            unreachable!("parse_match_hash_pattern returns Hash");
4610        };
4611        self.expect(&Token::Assign)?;
4612        self.suppress_scalar_hash_brace += 1;
4613        let rhs = self.parse_expression()?;
4614        self.suppress_scalar_hash_brace -= 1;
4615        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
4616        self.parse_stmt_postfix_modifier(stmt)
4617    }
4618
4619    fn desugar_array_destructure(
4620        &mut self,
4621        keyword: &str,
4622        line: usize,
4623        elems: Vec<MatchArrayElem>,
4624        rhs: Expr,
4625    ) -> PerlResult<Statement> {
4626        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4627        let mut stmts: Vec<Statement> = Vec::new();
4628        stmts.push(destructure_stmt_from_var_decls(
4629            keyword,
4630            vec![VarDecl {
4631                sigil: Sigil::Scalar,
4632                name: tmp.clone(),
4633                initializer: Some(rhs),
4634                frozen: false,
4635                type_annotation: None,
4636            }],
4637            line,
4638        ));
4639
4640        let has_rest = elems
4641            .iter()
4642            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
4643        let fixed_slots = elems
4644            .iter()
4645            .filter(|e| {
4646                matches!(
4647                    e,
4648                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
4649                )
4650            })
4651            .count();
4652        if !has_rest {
4653            let cond = Expr {
4654                kind: ExprKind::BinOp {
4655                    left: Box::new(destructure_expr_array_len(&tmp, line)),
4656                    op: BinOp::NumEq,
4657                    right: Box::new(Expr {
4658                        kind: ExprKind::Integer(fixed_slots as i64),
4659                        line,
4660                    }),
4661                },
4662                line,
4663            };
4664            stmts.push(destructure_stmt_unless_die(
4665                line,
4666                cond,
4667                "array destructure: length mismatch",
4668            ));
4669        }
4670
4671        let mut idx: i64 = 0;
4672        for elem in elems {
4673            match elem {
4674                MatchArrayElem::Rest => break,
4675                MatchArrayElem::RestBind(name) => {
4676                    let list_source = Expr {
4677                        kind: ExprKind::Deref {
4678                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4679                            kind: Sigil::Array,
4680                        },
4681                        line,
4682                    };
4683                    let last_ix = Expr {
4684                        kind: ExprKind::BinOp {
4685                            left: Box::new(destructure_expr_array_len(&tmp, line)),
4686                            op: BinOp::Sub,
4687                            right: Box::new(Expr {
4688                                kind: ExprKind::Integer(1),
4689                                line,
4690                            }),
4691                        },
4692                        line,
4693                    };
4694                    let range = Expr {
4695                        kind: ExprKind::Range {
4696                            from: Box::new(Expr {
4697                                kind: ExprKind::Integer(idx),
4698                                line,
4699                            }),
4700                            to: Box::new(last_ix),
4701                            exclusive: false,
4702                            step: None,
4703                        },
4704                        line,
4705                    };
4706                    let slice = Expr {
4707                        kind: ExprKind::AnonymousListSlice {
4708                            source: Box::new(list_source),
4709                            indices: vec![range],
4710                        },
4711                        line,
4712                    };
4713                    stmts.push(destructure_stmt_from_var_decls(
4714                        keyword,
4715                        vec![VarDecl {
4716                            sigil: Sigil::Array,
4717                            name,
4718                            initializer: Some(slice),
4719                            frozen: false,
4720                            type_annotation: None,
4721                        }],
4722                        line,
4723                    ));
4724                    break;
4725                }
4726                MatchArrayElem::CaptureScalar(name) => {
4727                    let arrow = Expr {
4728                        kind: ExprKind::ArrowDeref {
4729                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4730                            index: Box::new(Expr {
4731                                kind: ExprKind::Integer(idx),
4732                                line,
4733                            }),
4734                            kind: DerefKind::Array,
4735                        },
4736                        line,
4737                    };
4738                    stmts.push(destructure_stmt_from_var_decls(
4739                        keyword,
4740                        vec![VarDecl {
4741                            sigil: Sigil::Scalar,
4742                            name,
4743                            initializer: Some(arrow),
4744                            frozen: false,
4745                            type_annotation: None,
4746                        }],
4747                        line,
4748                    ));
4749                    idx += 1;
4750                }
4751                MatchArrayElem::Expr(e) => {
4752                    let elem_subj = Expr {
4753                        kind: ExprKind::ArrowDeref {
4754                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4755                            index: Box::new(Expr {
4756                                kind: ExprKind::Integer(idx),
4757                                line,
4758                            }),
4759                            kind: DerefKind::Array,
4760                        },
4761                        line,
4762                    };
4763                    let match_expr = Expr {
4764                        kind: ExprKind::AlgebraicMatch {
4765                            subject: Box::new(elem_subj),
4766                            arms: vec![
4767                                MatchArm {
4768                                    pattern: MatchPattern::Value(Box::new(e.clone())),
4769                                    guard: None,
4770                                    body: Expr {
4771                                        kind: ExprKind::Integer(0),
4772                                        line,
4773                                    },
4774                                },
4775                                MatchArm {
4776                                    pattern: MatchPattern::Any,
4777                                    guard: None,
4778                                    body: Expr {
4779                                        kind: ExprKind::Die(vec![Expr {
4780                                            kind: ExprKind::String(
4781                                                "array destructure: element pattern mismatch"
4782                                                    .to_string(),
4783                                            ),
4784                                            line,
4785                                        }]),
4786                                        line,
4787                                    },
4788                                },
4789                            ],
4790                        },
4791                        line,
4792                    };
4793                    stmts.push(Statement {
4794                        label: None,
4795                        kind: StmtKind::Expression(match_expr),
4796                        line,
4797                    });
4798                    idx += 1;
4799                }
4800            }
4801        }
4802
4803        Ok(Statement {
4804            label: None,
4805            kind: StmtKind::StmtGroup(stmts),
4806            line,
4807        })
4808    }
4809
4810    fn desugar_hash_destructure(
4811        &mut self,
4812        keyword: &str,
4813        line: usize,
4814        pairs: Vec<MatchHashPair>,
4815        rhs: Expr,
4816    ) -> PerlResult<Statement> {
4817        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4818        let mut stmts: Vec<Statement> = Vec::new();
4819        stmts.push(destructure_stmt_from_var_decls(
4820            keyword,
4821            vec![VarDecl {
4822                sigil: Sigil::Scalar,
4823                name: tmp.clone(),
4824                initializer: Some(rhs),
4825                frozen: false,
4826                type_annotation: None,
4827            }],
4828            line,
4829        ));
4830
4831        for pair in pairs {
4832            match pair {
4833                MatchHashPair::KeyOnly { key } => {
4834                    let exists_op = Expr {
4835                        kind: ExprKind::Exists(Box::new(Expr {
4836                            kind: ExprKind::ArrowDeref {
4837                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4838                                index: Box::new(key),
4839                                kind: DerefKind::Hash,
4840                            },
4841                            line,
4842                        })),
4843                        line,
4844                    };
4845                    stmts.push(destructure_stmt_unless_die(
4846                        line,
4847                        exists_op,
4848                        "hash destructure: missing required key",
4849                    ));
4850                }
4851                MatchHashPair::Capture { key, name } => {
4852                    let init = Expr {
4853                        kind: ExprKind::ArrowDeref {
4854                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4855                            index: Box::new(key),
4856                            kind: DerefKind::Hash,
4857                        },
4858                        line,
4859                    };
4860                    stmts.push(destructure_stmt_from_var_decls(
4861                        keyword,
4862                        vec![VarDecl {
4863                            sigil: Sigil::Scalar,
4864                            name,
4865                            initializer: Some(init),
4866                            frozen: false,
4867                            type_annotation: None,
4868                        }],
4869                        line,
4870                    ));
4871                }
4872            }
4873        }
4874
4875        Ok(Statement {
4876            label: None,
4877            kind: StmtKind::StmtGroup(stmts),
4878            line,
4879        })
4880    }
4881
4882    fn parse_my_our_local(
4883        &mut self,
4884        keyword: &str,
4885        allow_type_annotation: bool,
4886    ) -> PerlResult<Statement> {
4887        let line = self.peek_line();
4888        self.advance(); // 'my'/'our'/'local'
4889
4890        if keyword == "local"
4891            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
4892        {
4893            let target = self.parse_postfix()?;
4894            let mut initializer: Option<Expr> = None;
4895            if self.eat(&Token::Assign) {
4896                initializer = Some(self.parse_expression()?);
4897            } else if matches!(
4898                self.peek(),
4899                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
4900            ) {
4901                if matches!(&target.kind, ExprKind::Typeglob(_)) {
4902                    return Err(self.syntax_err(
4903                        "compound assignment on typeglob declaration is not supported",
4904                        self.peek_line(),
4905                    ));
4906                }
4907                let op = match self.peek().clone() {
4908                    Token::OrAssign => BinOp::LogOr,
4909                    Token::DefinedOrAssign => BinOp::DefinedOr,
4910                    Token::AndAssign => BinOp::LogAnd,
4911                    _ => unreachable!(),
4912                };
4913                self.advance();
4914                let rhs = self.parse_assign_expr()?;
4915                let tgt_line = target.line;
4916                initializer = Some(Expr {
4917                    kind: ExprKind::CompoundAssign {
4918                        target: Box::new(target.clone()),
4919                        op,
4920                        value: Box::new(rhs),
4921                    },
4922                    line: tgt_line,
4923                });
4924            }
4925
4926            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
4927                decl.initializer = initializer;
4928                StmtKind::Local(vec![decl])
4929            } else {
4930                StmtKind::LocalExpr {
4931                    target,
4932                    initializer,
4933                }
4934            };
4935            let stmt = Statement {
4936                label: None,
4937                kind,
4938                line,
4939            };
4940            return self.parse_stmt_postfix_modifier(stmt);
4941        }
4942
4943        if matches!(self.peek(), Token::LBracket) {
4944            return self.parse_decl_array_destructure(keyword, line);
4945        }
4946        if matches!(self.peek(), Token::LBrace) {
4947            return self.parse_decl_hash_destructure(keyword, line);
4948        }
4949
4950        let mut decls = Vec::new();
4951
4952        if self.eat(&Token::LParen) {
4953            // my ($a, @b, %c)
4954            while !matches!(self.peek(), Token::RParen | Token::Eof) {
4955                let decl = self.parse_var_decl(allow_type_annotation)?;
4956                decls.push(decl);
4957                if !self.eat(&Token::Comma) {
4958                    break;
4959                }
4960            }
4961            self.expect(&Token::RParen)?;
4962        } else {
4963            decls.push(self.parse_var_decl(allow_type_annotation)?);
4964        }
4965
4966        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
4967        if self.eat(&Token::Assign) {
4968            if keyword == "our" && decls.len() == 1 {
4969                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
4970                    self.advance();
4971                    decls.push(self.parse_var_decl(allow_type_annotation)?);
4972                    if !self.eat(&Token::Assign) {
4973                        return Err(self.syntax_err(
4974                            "expected `=` after `our` in chained our-declaration",
4975                            self.peek_line(),
4976                        ));
4977                    }
4978                }
4979            }
4980            let val = self.parse_expression()?;
4981            // Validate assignment for single variable declarations (not destructuring)
4982            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
4983            if !crate::compat_mode() && decls.len() == 1 {
4984                let decl = &decls[0];
4985                let target_kind = match decl.sigil {
4986                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
4987                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
4988                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
4989                    Sigil::Typeglob => {
4990                        // Skip validation for typeglob
4991                        if decls.len() == 1 {
4992                            decls[0].initializer = Some(val);
4993                        } else {
4994                            for d in &mut decls {
4995                                d.initializer = Some(val.clone());
4996                            }
4997                        }
4998                        return Ok(Statement {
4999                            label: None,
5000                            kind: match keyword {
5001                                "my" => StmtKind::My(decls),
5002                                "mysync" => StmtKind::MySync(decls),
5003                                "our" => StmtKind::Our(decls),
5004                                "local" => StmtKind::Local(decls),
5005                                "state" => StmtKind::State(decls),
5006                                _ => unreachable!(),
5007                            },
5008                            line,
5009                        });
5010                    }
5011                };
5012                let target = Expr {
5013                    kind: target_kind,
5014                    line,
5015                };
5016                self.validate_assignment(&target, &val, line)?;
5017            }
5018            if decls.len() == 1 {
5019                decls[0].initializer = Some(val);
5020            } else {
5021                for decl in &mut decls {
5022                    decl.initializer = Some(val.clone());
5023                }
5024            }
5025        } else if decls.len() == 1 {
5026            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5027            let op = match self.peek().clone() {
5028                Token::OrAssign => Some(BinOp::LogOr),
5029                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5030                Token::AndAssign => Some(BinOp::LogAnd),
5031                _ => None,
5032            };
5033            if let Some(op) = op {
5034                let d = &decls[0];
5035                if matches!(d.sigil, Sigil::Typeglob) {
5036                    return Err(self.syntax_err(
5037                        "compound assignment on typeglob declaration is not supported",
5038                        self.peek_line(),
5039                    ));
5040                }
5041                self.advance();
5042                let rhs = self.parse_assign_expr()?;
5043                let target = Expr {
5044                    kind: match d.sigil {
5045                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5046                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
5047                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
5048                        Sigil::Typeglob => unreachable!(),
5049                    },
5050                    line,
5051                };
5052                decls[0].initializer = Some(Expr {
5053                    kind: ExprKind::CompoundAssign {
5054                        target: Box::new(target),
5055                        op,
5056                        value: Box::new(rhs),
5057                    },
5058                    line,
5059                });
5060            }
5061        }
5062
5063        let kind = match keyword {
5064            "my" => StmtKind::My(decls),
5065            "mysync" => StmtKind::MySync(decls),
5066            "our" => StmtKind::Our(decls),
5067            "local" => StmtKind::Local(decls),
5068            "state" => StmtKind::State(decls),
5069            _ => unreachable!(),
5070        };
5071        let stmt = Statement {
5072            label: None,
5073            kind,
5074            line,
5075        };
5076        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
5077        self.parse_stmt_postfix_modifier(stmt)
5078    }
5079
5080    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
5081        let mut decl = match self.advance() {
5082            (Token::ScalarVar(name), _) => VarDecl {
5083                sigil: Sigil::Scalar,
5084                name,
5085                initializer: None,
5086                frozen: false,
5087                type_annotation: None,
5088            },
5089            (Token::ArrayVar(name), _) => VarDecl {
5090                sigil: Sigil::Array,
5091                name,
5092                initializer: None,
5093                frozen: false,
5094                type_annotation: None,
5095            },
5096            (Token::HashVar(name), line) => {
5097                if !crate::compat_mode() {
5098                    self.check_hash_shadows_reserved(&name, line)?;
5099                }
5100                VarDecl {
5101                    sigil: Sigil::Hash,
5102                    name,
5103                    initializer: None,
5104                    frozen: false,
5105                    type_annotation: None,
5106                }
5107            }
5108            (Token::Star, _line) => {
5109                let name = match self.advance() {
5110                    (Token::Ident(n), _) => n,
5111                    (tok, l) => {
5112                        return Err(self
5113                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
5114                    }
5115                };
5116                VarDecl {
5117                    sigil: Sigil::Typeglob,
5118                    name,
5119                    initializer: None,
5120                    frozen: false,
5121                    type_annotation: None,
5122                }
5123            }
5124            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
5125            // slot in a list assignment. The interpreter treats `undef`-named
5126            // scalar decls as throwaway: declared into a unique sink so the
5127            // distribute-to-decls loop advances past the slot.
5128            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
5129                sigil: Sigil::Scalar,
5130                // Synthesize a name that user code cannot reference. Each
5131                // sink slot in a list-assign gets its own unique name so the
5132                // declarations don't collide.
5133                name: format!("__undef_sink_{}", self.pos),
5134                initializer: None,
5135                frozen: false,
5136                type_annotation: None,
5137            },
5138            (tok, line) => {
5139                return Err(self.syntax_err(
5140                    format!("Expected variable in declaration, got {:?}", tok),
5141                    line,
5142                ));
5143            }
5144        };
5145        if allow_type_annotation && self.eat(&Token::Colon) {
5146            let ty = self.parse_type_name()?;
5147            if decl.sigil != Sigil::Scalar {
5148                return Err(self.syntax_err(
5149                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
5150                    self.peek_line(),
5151                ));
5152            }
5153            decl.type_annotation = Some(ty);
5154        }
5155        Ok(decl)
5156    }
5157
5158    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
5159        match self.advance() {
5160            (Token::Ident(name), _) => match name.as_str() {
5161                "Int" => Ok(PerlTypeName::Int),
5162                "Str" => Ok(PerlTypeName::Str),
5163                "Float" => Ok(PerlTypeName::Float),
5164                "Bool" => Ok(PerlTypeName::Bool),
5165                "Array" => Ok(PerlTypeName::Array),
5166                "Hash" => Ok(PerlTypeName::Hash),
5167                "Ref" => Ok(PerlTypeName::Ref),
5168                "Any" => Ok(PerlTypeName::Any),
5169                _ => Ok(PerlTypeName::Struct(name)),
5170            },
5171            (tok, err_line) => Err(self.syntax_err(
5172                format!("Expected type name after `:`, got {:?}", tok),
5173                err_line,
5174            )),
5175        }
5176    }
5177
5178    fn parse_package(&mut self) -> PerlResult<Statement> {
5179        let line = self.peek_line();
5180        self.advance(); // 'package'
5181        let name = match self.advance() {
5182            (Token::Ident(n), _) => n,
5183            (tok, line) => {
5184                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
5185            }
5186        };
5187        // Handle Foo::Bar
5188        let mut full_name = name;
5189        while self.eat(&Token::PackageSep) {
5190            if let (Token::Ident(part), _) = self.advance() {
5191                full_name = format!("{}::{}", full_name, part);
5192            }
5193        }
5194        self.eat(&Token::Semicolon);
5195        Ok(Statement {
5196            label: None,
5197            kind: StmtKind::Package { name: full_name },
5198            line,
5199        })
5200    }
5201
5202    fn parse_use(&mut self) -> PerlResult<Statement> {
5203        let line = self.peek_line();
5204        self.advance(); // 'use'
5205        let (tok, tok_line) = self.advance();
5206        match tok {
5207            Token::Float(v) => {
5208                self.eat(&Token::Semicolon);
5209                Ok(Statement {
5210                    label: None,
5211                    kind: StmtKind::UsePerlVersion { version: v },
5212                    line,
5213                })
5214            }
5215            Token::Integer(n) => {
5216                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5217                    self.eat(&Token::Semicolon);
5218                    Ok(Statement {
5219                        label: None,
5220                        kind: StmtKind::UsePerlVersion { version: n as f64 },
5221                        line,
5222                    })
5223                } else {
5224                    Err(self.syntax_err(
5225                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
5226                        line,
5227                    ))
5228                }
5229            }
5230            Token::Ident(n) => {
5231                let mut full_name = n;
5232                while self.eat(&Token::PackageSep) {
5233                    if let (Token::Ident(part), _) = self.advance() {
5234                        full_name = format!("{}::{}", full_name, part);
5235                    }
5236                }
5237                if full_name == "overload" {
5238                    let mut pairs = Vec::new();
5239                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
5240                        loop {
5241                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
5242                            {
5243                                break;
5244                            }
5245                            let key_e = this.parse_assign_expr()?;
5246                            this.expect(&Token::FatArrow)?;
5247                            let val_e = this.parse_assign_expr()?;
5248                            let key = this.expr_to_overload_key(&key_e)?;
5249                            let val = this.expr_to_overload_sub(&val_e)?;
5250                            pairs.push((key, val));
5251                            if !this.eat(&Token::Comma) {
5252                                break;
5253                            }
5254                        }
5255                        Ok(())
5256                    };
5257                    if self.eat(&Token::LParen) {
5258                        // `use overload ();` — common in JSON::PP and other modules.
5259                        parse_overload_pairs(self)?;
5260                        self.expect(&Token::RParen)?;
5261                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
5262                        parse_overload_pairs(self)?;
5263                    }
5264                    self.eat(&Token::Semicolon);
5265                    return Ok(Statement {
5266                        label: None,
5267                        kind: StmtKind::UseOverload { pairs },
5268                        line,
5269                    });
5270                }
5271                let mut imports = Vec::new();
5272                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5273                    && !self.next_is_new_statement_start(tok_line)
5274                {
5275                    loop {
5276                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5277                            break;
5278                        }
5279                        imports.push(self.parse_expression()?);
5280                        if !self.eat(&Token::Comma) {
5281                            break;
5282                        }
5283                    }
5284                }
5285                self.eat(&Token::Semicolon);
5286                Ok(Statement {
5287                    label: None,
5288                    kind: StmtKind::Use {
5289                        module: full_name,
5290                        imports,
5291                    },
5292                    line,
5293                })
5294            }
5295            other => Err(self.syntax_err(
5296                format!("Expected module name or version after use, got {:?}", other),
5297                tok_line,
5298            )),
5299        }
5300    }
5301
5302    fn parse_no(&mut self) -> PerlResult<Statement> {
5303        let line = self.peek_line();
5304        self.advance(); // 'no'
5305        let module = match self.advance() {
5306            (Token::Ident(n), tok_line) => (n, tok_line),
5307            (tok, line) => {
5308                return Err(self.syntax_err(
5309                    format!("Expected module name after no, got {:?}", tok),
5310                    line,
5311                ))
5312            }
5313        };
5314        let (module_name, tok_line) = module;
5315        let mut full_name = module_name;
5316        while self.eat(&Token::PackageSep) {
5317            if let (Token::Ident(part), _) = self.advance() {
5318                full_name = format!("{}::{}", full_name, part);
5319            }
5320        }
5321        let mut imports = Vec::new();
5322        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5323            && !self.next_is_new_statement_start(tok_line)
5324        {
5325            loop {
5326                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5327                    break;
5328                }
5329                imports.push(self.parse_expression()?);
5330                if !self.eat(&Token::Comma) {
5331                    break;
5332                }
5333            }
5334        }
5335        self.eat(&Token::Semicolon);
5336        Ok(Statement {
5337            label: None,
5338            kind: StmtKind::No {
5339                module: full_name,
5340                imports,
5341            },
5342            line,
5343        })
5344    }
5345
5346    fn parse_return(&mut self) -> PerlResult<Statement> {
5347        let line = self.peek_line();
5348        self.advance(); // 'return'
5349        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
5350            None
5351        } else {
5352            // Only parse up to the assign level to avoid consuming postfix if/unless
5353            Some(self.parse_assign_expr()?)
5354        };
5355        // Check for postfix modifiers on return
5356        let stmt = Statement {
5357            label: None,
5358            kind: StmtKind::Return(val),
5359            line,
5360        };
5361        if let Token::Ident(ref kw) = self.peek().clone() {
5362            match kw.as_str() {
5363                "if" => {
5364                    self.advance();
5365                    let cond = self.parse_expression()?;
5366                    self.eat(&Token::Semicolon);
5367                    return Ok(Statement {
5368                        label: None,
5369                        kind: StmtKind::If {
5370                            condition: cond,
5371                            body: vec![stmt],
5372                            elsifs: vec![],
5373                            else_block: None,
5374                        },
5375                        line,
5376                    });
5377                }
5378                "unless" => {
5379                    self.advance();
5380                    let cond = self.parse_expression()?;
5381                    self.eat(&Token::Semicolon);
5382                    return Ok(Statement {
5383                        label: None,
5384                        kind: StmtKind::Unless {
5385                            condition: cond,
5386                            body: vec![stmt],
5387                            else_block: None,
5388                        },
5389                        line,
5390                    });
5391                }
5392                _ => {}
5393            }
5394        }
5395        self.eat(&Token::Semicolon);
5396        Ok(stmt)
5397    }
5398
5399    // ── Expressions (Pratt / precedence climbing) ──
5400
5401    fn parse_expression(&mut self) -> PerlResult<Expr> {
5402        self.parse_comma_expr()
5403    }
5404
5405    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
5406        let expr = self.parse_assign_expr()?;
5407        let mut exprs = vec![expr];
5408        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5409            if matches!(
5410                self.peek(),
5411                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
5412            ) {
5413                break; // trailing comma
5414            }
5415            exprs.push(self.parse_assign_expr()?);
5416        }
5417        if exprs.len() == 1 {
5418            return Ok(exprs.pop().unwrap());
5419        }
5420        let line = exprs[0].line;
5421        Ok(Expr {
5422            kind: ExprKind::List(exprs),
5423            line,
5424        })
5425    }
5426
5427    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
5428        let expr = self.parse_ternary()?;
5429        let line = expr.line;
5430
5431        match self.peek().clone() {
5432            Token::Assign => {
5433                self.advance();
5434                let right = self.parse_assign_expr()?;
5435                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
5436                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
5437                    if args.is_empty() {
5438                        // Destructure again to take ownership
5439                        let ExprKind::MethodCall {
5440                            object,
5441                            method,
5442                            super_call,
5443                            ..
5444                        } = expr.kind
5445                        else {
5446                            unreachable!()
5447                        };
5448                        return Ok(Expr {
5449                            kind: ExprKind::MethodCall {
5450                                object,
5451                                method,
5452                                args: vec![right],
5453                                super_call,
5454                            },
5455                            line,
5456                        });
5457                    }
5458                }
5459                self.validate_assignment(&expr, &right, line)?;
5460                Ok(Expr {
5461                    kind: ExprKind::Assign {
5462                        target: Box::new(expr),
5463                        value: Box::new(right),
5464                    },
5465                    line,
5466                })
5467            }
5468            Token::PlusAssign => {
5469                self.advance();
5470                let r = self.parse_assign_expr()?;
5471                Ok(Expr {
5472                    kind: ExprKind::CompoundAssign {
5473                        target: Box::new(expr),
5474                        op: BinOp::Add,
5475                        value: Box::new(r),
5476                    },
5477                    line,
5478                })
5479            }
5480            Token::MinusAssign => {
5481                self.advance();
5482                let r = self.parse_assign_expr()?;
5483                Ok(Expr {
5484                    kind: ExprKind::CompoundAssign {
5485                        target: Box::new(expr),
5486                        op: BinOp::Sub,
5487                        value: Box::new(r),
5488                    },
5489                    line,
5490                })
5491            }
5492            Token::MulAssign => {
5493                self.advance();
5494                let r = self.parse_assign_expr()?;
5495                Ok(Expr {
5496                    kind: ExprKind::CompoundAssign {
5497                        target: Box::new(expr),
5498                        op: BinOp::Mul,
5499                        value: Box::new(r),
5500                    },
5501                    line,
5502                })
5503            }
5504            Token::DivAssign => {
5505                self.advance();
5506                let r = self.parse_assign_expr()?;
5507                Ok(Expr {
5508                    kind: ExprKind::CompoundAssign {
5509                        target: Box::new(expr),
5510                        op: BinOp::Div,
5511                        value: Box::new(r),
5512                    },
5513                    line,
5514                })
5515            }
5516            Token::ModAssign => {
5517                self.advance();
5518                let r = self.parse_assign_expr()?;
5519                Ok(Expr {
5520                    kind: ExprKind::CompoundAssign {
5521                        target: Box::new(expr),
5522                        op: BinOp::Mod,
5523                        value: Box::new(r),
5524                    },
5525                    line,
5526                })
5527            }
5528            Token::PowAssign => {
5529                self.advance();
5530                let r = self.parse_assign_expr()?;
5531                Ok(Expr {
5532                    kind: ExprKind::CompoundAssign {
5533                        target: Box::new(expr),
5534                        op: BinOp::Pow,
5535                        value: Box::new(r),
5536                    },
5537                    line,
5538                })
5539            }
5540            Token::DotAssign => {
5541                self.advance();
5542                let r = self.parse_assign_expr()?;
5543                Ok(Expr {
5544                    kind: ExprKind::CompoundAssign {
5545                        target: Box::new(expr),
5546                        op: BinOp::Concat,
5547                        value: Box::new(r),
5548                    },
5549                    line,
5550                })
5551            }
5552            Token::BitAndAssign => {
5553                self.advance();
5554                let r = self.parse_assign_expr()?;
5555                Ok(Expr {
5556                    kind: ExprKind::CompoundAssign {
5557                        target: Box::new(expr),
5558                        op: BinOp::BitAnd,
5559                        value: Box::new(r),
5560                    },
5561                    line,
5562                })
5563            }
5564            Token::BitOrAssign => {
5565                self.advance();
5566                let r = self.parse_assign_expr()?;
5567                Ok(Expr {
5568                    kind: ExprKind::CompoundAssign {
5569                        target: Box::new(expr),
5570                        op: BinOp::BitOr,
5571                        value: Box::new(r),
5572                    },
5573                    line,
5574                })
5575            }
5576            Token::XorAssign => {
5577                self.advance();
5578                let r = self.parse_assign_expr()?;
5579                Ok(Expr {
5580                    kind: ExprKind::CompoundAssign {
5581                        target: Box::new(expr),
5582                        op: BinOp::BitXor,
5583                        value: Box::new(r),
5584                    },
5585                    line,
5586                })
5587            }
5588            Token::ShiftLeftAssign => {
5589                self.advance();
5590                let r = self.parse_assign_expr()?;
5591                Ok(Expr {
5592                    kind: ExprKind::CompoundAssign {
5593                        target: Box::new(expr),
5594                        op: BinOp::ShiftLeft,
5595                        value: Box::new(r),
5596                    },
5597                    line,
5598                })
5599            }
5600            Token::ShiftRightAssign => {
5601                self.advance();
5602                let r = self.parse_assign_expr()?;
5603                Ok(Expr {
5604                    kind: ExprKind::CompoundAssign {
5605                        target: Box::new(expr),
5606                        op: BinOp::ShiftRight,
5607                        value: Box::new(r),
5608                    },
5609                    line,
5610                })
5611            }
5612            Token::OrAssign => {
5613                self.advance();
5614                let r = self.parse_assign_expr()?;
5615                Ok(Expr {
5616                    kind: ExprKind::CompoundAssign {
5617                        target: Box::new(expr),
5618                        op: BinOp::LogOr,
5619                        value: Box::new(r),
5620                    },
5621                    line,
5622                })
5623            }
5624            Token::DefinedOrAssign => {
5625                self.advance();
5626                let r = self.parse_assign_expr()?;
5627                Ok(Expr {
5628                    kind: ExprKind::CompoundAssign {
5629                        target: Box::new(expr),
5630                        op: BinOp::DefinedOr,
5631                        value: Box::new(r),
5632                    },
5633                    line,
5634                })
5635            }
5636            Token::AndAssign => {
5637                self.advance();
5638                let r = self.parse_assign_expr()?;
5639                Ok(Expr {
5640                    kind: ExprKind::CompoundAssign {
5641                        target: Box::new(expr),
5642                        op: BinOp::LogAnd,
5643                        value: Box::new(r),
5644                    },
5645                    line,
5646                })
5647            }
5648            _ => Ok(expr),
5649        }
5650    }
5651
5652    fn parse_ternary(&mut self) -> PerlResult<Expr> {
5653        let expr = self.parse_pipe_forward()?;
5654        if self.eat(&Token::Question) {
5655            let line = expr.line;
5656            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
5657            let then_expr = self.parse_assign_expr();
5658            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
5659            let then_expr = then_expr?;
5660            self.expect(&Token::Colon)?;
5661            let else_expr = self.parse_assign_expr()?;
5662            return Ok(Expr {
5663                kind: ExprKind::Ternary {
5664                    condition: Box::new(expr),
5665                    then_expr: Box::new(then_expr),
5666                    else_expr: Box::new(else_expr),
5667                },
5668                line,
5669            });
5670        }
5671        Ok(expr)
5672    }
5673
5674    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
5675    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
5676    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
5677    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
5678    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
5679    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
5680        let mut left = self.parse_or_word()?;
5681        // Inside a paren-less arg list, `|>` is a hard terminator for the
5682        // enclosing call — leave it for the outer `parse_pipe_forward` loop
5683        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
5684        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
5685        // outer `|>` via its first-arg `parse_assign_expr`.
5686        if self.no_pipe_forward_depth > 0 {
5687            return Ok(left);
5688        }
5689        while matches!(self.peek(), Token::PipeForward) {
5690            if crate::compat_mode() {
5691                return Err(self.syntax_err(
5692                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
5693                    left.line,
5694                ));
5695            }
5696            let line = left.line;
5697            self.advance();
5698            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
5699            // `join`, …) accept a placeholder in place of their list operand.
5700            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
5701            let right_result = self.parse_or_word();
5702            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
5703            let right = right_result?;
5704            left = self.pipe_forward_apply(left, right, line)?;
5705        }
5706        Ok(left)
5707    }
5708
5709    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
5710    /// its **first** argument (Elixir / R / proposed-JS convention).
5711    ///
5712    /// The strategy depends on the shape of `rhs`:
5713    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
5714    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
5715    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
5716    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
5717    ///   matching the `(data, filter)` signature the builtin expects.
5718    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
5719    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
5720    ///   `lhs` (these parse a single default `$_` when called without an arg, so
5721    ///   piping overrides that default; first-arg and last-arg are identical).
5722    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
5723    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
5724    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
5725    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
5726    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
5727    ///   as the sole argument.
5728    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
5729    ///   since silently calling a non-callable at runtime would be worse.
5730    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
5731        let Expr { kind, line: rline } = rhs;
5732        let new_kind = match kind {
5733            // ── Generic / user-defined calls ───────────────────────────────────
5734            ExprKind::FuncCall { name, mut args } => {
5735                match name.as_str() {
5736                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
5737                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
5738                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
5739                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "letters"
5740                    | "letters_uc" | "letters_lc" | "punctuation" | "numbers" | "graphemes"
5741                    | "columns" | "sentences" | "paragraphs" | "sections" | "trim" | "avg"
5742                    | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml" | "to_html"
5743                    | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
5744                    | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline" | "bar_chart"
5745                    | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed" | "cb"
5746                    | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
5747                        if args.is_empty() {
5748                            args.push(lhs);
5749                        } else {
5750                            args[0] = lhs;
5751                        }
5752                    }
5753                    "chunked" | "windowed" => {
5754                        if args.is_empty() {
5755                            return Err(self.syntax_err(
5756                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
5757                                line,
5758                            ));
5759                        }
5760                        args.insert(0, lhs);
5761                    }
5762                    "List::Util::reduce" | "List::Util::fold" => {
5763                        args.push(lhs);
5764                    }
5765                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
5766                        // data |> grep_v "pattern" → grep_v("pattern", data...)
5767                        // data |> pluck "key" → pluck("key", data...)
5768                        // data |> tee "file" → tee("file", data...)
5769                        // data |> nth N → nth(N, data...)
5770                        // data |> chunk N → chunk(N, data...)
5771                        args.push(lhs);
5772                    }
5773                    "enumerate" | "dedup" => {
5774                        // data |> enumerate → enumerate(data)
5775                        // data |> dedup → dedup(data)
5776                        args.insert(0, lhs);
5777                    }
5778                    "clamp" => {
5779                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
5780                        args.push(lhs);
5781                    }
5782                    "pfirst" | "pany" | "any" | "all" | "none" | "first" | "take_while"
5783                    | "drop_while" | "skip_while" | "reject" | "tap" | "peek" | "group_by"
5784                    | "chunk_by" | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
5785                        if args.len() < 2 {
5786                            return Err(self.syntax_err(
5787                                format!(
5788                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
5789                                ),
5790                                line,
5791                            ));
5792                        }
5793                        args[1] = lhs;
5794                    }
5795                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail" => {
5796                        if args.is_empty() {
5797                            return Err(self.syntax_err(
5798                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
5799                                line,
5800                            ));
5801                        }
5802                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
5803                        args.insert(0, lhs);
5804                    }
5805                    _ => {
5806                        if self.thread_last_mode {
5807                            args.push(lhs);
5808                        } else {
5809                            args.insert(0, lhs);
5810                        }
5811                    }
5812                }
5813                ExprKind::FuncCall { name, args }
5814            }
5815            ExprKind::MethodCall {
5816                object,
5817                method,
5818                mut args,
5819                super_call,
5820            } => {
5821                if self.thread_last_mode {
5822                    args.push(lhs);
5823                } else {
5824                    args.insert(0, lhs);
5825                }
5826                ExprKind::MethodCall {
5827                    object,
5828                    method,
5829                    args,
5830                    super_call,
5831                }
5832            }
5833            ExprKind::IndirectCall {
5834                target,
5835                mut args,
5836                ampersand,
5837                pass_caller_arglist: _,
5838            } => {
5839                if self.thread_last_mode {
5840                    args.push(lhs);
5841                } else {
5842                    args.insert(0, lhs);
5843                }
5844                ExprKind::IndirectCall {
5845                    target,
5846                    args,
5847                    ampersand,
5848                    // Prepending an explicit first arg means this is no longer
5849                    // "pass the caller's @_" — that form is only bare `&$cr`.
5850                    pass_caller_arglist: false,
5851                }
5852            }
5853
5854            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
5855            ExprKind::Print { handle, mut args } => {
5856                if self.thread_last_mode {
5857                    args.push(lhs);
5858                } else {
5859                    args.insert(0, lhs);
5860                }
5861                ExprKind::Print { handle, args }
5862            }
5863            ExprKind::Say { handle, mut args } => {
5864                if self.thread_last_mode {
5865                    args.push(lhs);
5866                } else {
5867                    args.insert(0, lhs);
5868                }
5869                ExprKind::Say { handle, args }
5870            }
5871            ExprKind::Printf { handle, mut args } => {
5872                if self.thread_last_mode {
5873                    args.push(lhs);
5874                } else {
5875                    args.insert(0, lhs);
5876                }
5877                ExprKind::Printf { handle, args }
5878            }
5879            ExprKind::Die(mut args) => {
5880                if self.thread_last_mode {
5881                    args.push(lhs);
5882                } else {
5883                    args.insert(0, lhs);
5884                }
5885                ExprKind::Die(args)
5886            }
5887            ExprKind::Warn(mut args) => {
5888                if self.thread_last_mode {
5889                    args.push(lhs);
5890                } else {
5891                    args.insert(0, lhs);
5892                }
5893                ExprKind::Warn(args)
5894            }
5895
5896            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
5897            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
5898            //   but piping the format string is the rarer case. Prepending
5899            //   to the values list gives `sprintf(format, lhs, ...args)` for
5900            //   the common `$n |> sprintf "count=%d"` case.
5901            ExprKind::Sprintf { format, mut args } => {
5902                if self.thread_last_mode {
5903                    args.push(lhs);
5904                } else {
5905                    args.insert(0, lhs);
5906                }
5907                ExprKind::Sprintf { format, args }
5908            }
5909
5910            // ── System / exec / globbing / filesystem variadics ────────────────
5911            ExprKind::System(mut args) => {
5912                if self.thread_last_mode {
5913                    args.push(lhs);
5914                } else {
5915                    args.insert(0, lhs);
5916                }
5917                ExprKind::System(args)
5918            }
5919            ExprKind::Exec(mut args) => {
5920                if self.thread_last_mode {
5921                    args.push(lhs);
5922                } else {
5923                    args.insert(0, lhs);
5924                }
5925                ExprKind::Exec(args)
5926            }
5927            ExprKind::Unlink(mut args) => {
5928                if self.thread_last_mode {
5929                    args.push(lhs);
5930                } else {
5931                    args.insert(0, lhs);
5932                }
5933                ExprKind::Unlink(args)
5934            }
5935            ExprKind::Chmod(mut args) => {
5936                if self.thread_last_mode {
5937                    args.push(lhs);
5938                } else {
5939                    args.insert(0, lhs);
5940                }
5941                ExprKind::Chmod(args)
5942            }
5943            ExprKind::Chown(mut args) => {
5944                if self.thread_last_mode {
5945                    args.push(lhs);
5946                } else {
5947                    args.insert(0, lhs);
5948                }
5949                ExprKind::Chown(args)
5950            }
5951            ExprKind::Glob(mut args) => {
5952                if self.thread_last_mode {
5953                    args.push(lhs);
5954                } else {
5955                    args.insert(0, lhs);
5956                }
5957                ExprKind::Glob(args)
5958            }
5959            ExprKind::Files(mut args) => {
5960                if self.thread_last_mode {
5961                    args.push(lhs);
5962                } else {
5963                    args.insert(0, lhs);
5964                }
5965                ExprKind::Files(args)
5966            }
5967            ExprKind::Filesf(mut args) => {
5968                if self.thread_last_mode {
5969                    args.push(lhs);
5970                } else {
5971                    args.insert(0, lhs);
5972                }
5973                ExprKind::Filesf(args)
5974            }
5975            ExprKind::FilesfRecursive(mut args) => {
5976                if self.thread_last_mode {
5977                    args.push(lhs);
5978                } else {
5979                    args.insert(0, lhs);
5980                }
5981                ExprKind::FilesfRecursive(args)
5982            }
5983            ExprKind::Dirs(mut args) => {
5984                if self.thread_last_mode {
5985                    args.push(lhs);
5986                } else {
5987                    args.insert(0, lhs);
5988                }
5989                ExprKind::Dirs(args)
5990            }
5991            ExprKind::DirsRecursive(mut args) => {
5992                if self.thread_last_mode {
5993                    args.push(lhs);
5994                } else {
5995                    args.insert(0, lhs);
5996                }
5997                ExprKind::DirsRecursive(args)
5998            }
5999            ExprKind::SymLinks(mut args) => {
6000                if self.thread_last_mode {
6001                    args.push(lhs);
6002                } else {
6003                    args.insert(0, lhs);
6004                }
6005                ExprKind::SymLinks(args)
6006            }
6007            ExprKind::Sockets(mut args) => {
6008                if self.thread_last_mode {
6009                    args.push(lhs);
6010                } else {
6011                    args.insert(0, lhs);
6012                }
6013                ExprKind::Sockets(args)
6014            }
6015            ExprKind::Pipes(mut args) => {
6016                if self.thread_last_mode {
6017                    args.push(lhs);
6018                } else {
6019                    args.insert(0, lhs);
6020                }
6021                ExprKind::Pipes(args)
6022            }
6023            ExprKind::BlockDevices(mut args) => {
6024                if self.thread_last_mode {
6025                    args.push(lhs);
6026                } else {
6027                    args.insert(0, lhs);
6028                }
6029                ExprKind::BlockDevices(args)
6030            }
6031            ExprKind::CharDevices(mut args) => {
6032                if self.thread_last_mode {
6033                    args.push(lhs);
6034                } else {
6035                    args.insert(0, lhs);
6036                }
6037                ExprKind::CharDevices(args)
6038            }
6039            ExprKind::GlobPar { mut args, progress } => {
6040                if self.thread_last_mode {
6041                    args.push(lhs);
6042                } else {
6043                    args.insert(0, lhs);
6044                }
6045                ExprKind::GlobPar { args, progress }
6046            }
6047            ExprKind::ParSed { mut args, progress } => {
6048                if self.thread_last_mode {
6049                    args.push(lhs);
6050                } else {
6051                    args.insert(0, lhs);
6052                }
6053                ExprKind::ParSed { args, progress }
6054            }
6055
6056            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
6057            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
6058            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
6059            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
6060            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
6061            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
6062            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
6063            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
6064            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
6065            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
6066            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
6067            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
6068            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
6069            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
6070            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
6071            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
6072            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
6073            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
6074            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
6075            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
6076            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
6077            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
6078            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
6079            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
6080            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
6081            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
6082            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
6083            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
6084            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
6085            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
6086            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
6087            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
6088            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
6089            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
6090            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
6091            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
6092            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
6093            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
6094            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
6095            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
6096            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
6097            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
6098            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
6099            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
6100            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
6101            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
6102            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
6103            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
6104            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
6105            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
6106            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
6107            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
6108
6109            // ── Higher-order / list-taking forms: replace the `list` slot ──────
6110            ExprKind::MapExpr {
6111                block,
6112                list: _,
6113                flatten_array_refs,
6114                stream,
6115            } => ExprKind::MapExpr {
6116                block,
6117                list: Box::new(lhs),
6118                flatten_array_refs,
6119                stream,
6120            },
6121            ExprKind::MapExprComma {
6122                expr,
6123                list: _,
6124                flatten_array_refs,
6125                stream,
6126            } => ExprKind::MapExprComma {
6127                expr,
6128                list: Box::new(lhs),
6129                flatten_array_refs,
6130                stream,
6131            },
6132            ExprKind::GrepExpr {
6133                block,
6134                list: _,
6135                keyword,
6136            } => ExprKind::GrepExpr {
6137                block,
6138                list: Box::new(lhs),
6139                keyword,
6140            },
6141            ExprKind::GrepExprComma {
6142                expr,
6143                list: _,
6144                keyword,
6145            } => ExprKind::GrepExprComma {
6146                expr,
6147                list: Box::new(lhs),
6148                keyword,
6149            },
6150            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
6151                block,
6152                list: Box::new(lhs),
6153            },
6154            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
6155                cmp,
6156                list: Box::new(lhs),
6157            },
6158            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
6159                separator,
6160                list: Box::new(lhs),
6161            },
6162            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
6163                block,
6164                list: Box::new(lhs),
6165            },
6166            ExprKind::PMapExpr {
6167                block,
6168                list: _,
6169                progress,
6170                flat_outputs,
6171                on_cluster,
6172                stream,
6173            } => ExprKind::PMapExpr {
6174                block,
6175                list: Box::new(lhs),
6176                progress,
6177                flat_outputs,
6178                on_cluster,
6179                stream,
6180            },
6181            ExprKind::PMapChunkedExpr {
6182                chunk_size,
6183                block,
6184                list: _,
6185                progress,
6186            } => ExprKind::PMapChunkedExpr {
6187                chunk_size,
6188                block,
6189                list: Box::new(lhs),
6190                progress,
6191            },
6192            ExprKind::PGrepExpr {
6193                block,
6194                list: _,
6195                progress,
6196                stream,
6197            } => ExprKind::PGrepExpr {
6198                block,
6199                list: Box::new(lhs),
6200                progress,
6201                stream,
6202            },
6203            ExprKind::PForExpr {
6204                block,
6205                list: _,
6206                progress,
6207            } => ExprKind::PForExpr {
6208                block,
6209                list: Box::new(lhs),
6210                progress,
6211            },
6212            ExprKind::PSortExpr {
6213                cmp,
6214                list: _,
6215                progress,
6216            } => ExprKind::PSortExpr {
6217                cmp,
6218                list: Box::new(lhs),
6219                progress,
6220            },
6221            ExprKind::PReduceExpr {
6222                block,
6223                list: _,
6224                progress,
6225            } => ExprKind::PReduceExpr {
6226                block,
6227                list: Box::new(lhs),
6228                progress,
6229            },
6230            ExprKind::PcacheExpr {
6231                block,
6232                list: _,
6233                progress,
6234            } => ExprKind::PcacheExpr {
6235                block,
6236                list: Box::new(lhs),
6237                progress,
6238            },
6239            ExprKind::PReduceInitExpr {
6240                init,
6241                block,
6242                list: _,
6243                progress,
6244            } => ExprKind::PReduceInitExpr {
6245                init,
6246                block,
6247                list: Box::new(lhs),
6248                progress,
6249            },
6250            ExprKind::PMapReduceExpr {
6251                map_block,
6252                reduce_block,
6253                list: _,
6254                progress,
6255            } => ExprKind::PMapReduceExpr {
6256                map_block,
6257                reduce_block,
6258                list: Box::new(lhs),
6259                progress,
6260            },
6261
6262            // ── Push / unshift: first arg is the array, so pipe the LHS
6263            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
6264            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
6265            //     directly for that.
6266            ExprKind::Push { array, mut values } => {
6267                values.insert(0, lhs);
6268                ExprKind::Push { array, values }
6269            }
6270            ExprKind::Unshift { array, mut values } => {
6271                values.insert(0, lhs);
6272                ExprKind::Unshift { array, values }
6273            }
6274
6275            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
6276            ExprKind::SplitExpr {
6277                pattern,
6278                string: _,
6279                limit,
6280            } => ExprKind::SplitExpr {
6281                pattern,
6282                string: Box::new(lhs),
6283                limit,
6284            },
6285
6286            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
6287            //    Auto-inject `r` flag so the substitution returns the modified
6288            //    string instead of the match count (non-destructive / Perl /r).
6289            ExprKind::Substitution {
6290                pattern,
6291                replacement,
6292                mut flags,
6293                expr: _,
6294                delim,
6295            } => {
6296                if !flags.contains('r') {
6297                    flags.push('r');
6298                }
6299                ExprKind::Substitution {
6300                    expr: Box::new(lhs),
6301                    pattern,
6302                    replacement,
6303                    flags,
6304                    delim,
6305                }
6306            }
6307            ExprKind::Transliterate {
6308                from,
6309                to,
6310                mut flags,
6311                expr: _,
6312                delim,
6313            } => {
6314                if !flags.contains('r') {
6315                    flags.push('r');
6316                }
6317                ExprKind::Transliterate {
6318                    expr: Box::new(lhs),
6319                    from,
6320                    to,
6321                    flags,
6322                    delim,
6323                }
6324            }
6325            ExprKind::Match {
6326                pattern,
6327                flags,
6328                scalar_g,
6329                expr: _,
6330                delim,
6331            } => ExprKind::Match {
6332                expr: Box::new(lhs),
6333                pattern,
6334                flags,
6335                scalar_g,
6336                delim,
6337            },
6338            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
6339            ExprKind::Regex(pattern, flags) => ExprKind::Match {
6340                expr: Box::new(lhs),
6341                pattern,
6342                flags,
6343                scalar_g: false,
6344                delim: '/',
6345            },
6346
6347            // ── Bareword function name → plain unary call ──────────────────────
6348            ExprKind::Bareword(name) => match name.as_str() {
6349                "reverse" => {
6350                    if !crate::compat_mode() {
6351                        return Err(self
6352                            .syntax_err("stryke uses `rev` instead of `reverse` (this is not Perl 5)", line));
6353                    }
6354                    ExprKind::ReverseExpr(Box::new(lhs))
6355                }
6356                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
6357                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
6358                    name: "uniq".to_string(),
6359                    args: vec![lhs],
6360                },
6361                "fl" | "flatten" => ExprKind::FuncCall {
6362                    name: "flatten".to_string(),
6363                    args: vec![lhs],
6364                },
6365                _ => ExprKind::FuncCall {
6366                    name,
6367                    args: vec![lhs],
6368                },
6369            },
6370
6371            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
6372            kind @ (ExprKind::ScalarVar(_)
6373            | ExprKind::ArrayElement { .. }
6374            | ExprKind::HashElement { .. }
6375            | ExprKind::Deref { .. }
6376            | ExprKind::ArrowDeref { .. }
6377            | ExprKind::CodeRef { .. }
6378            | ExprKind::SubroutineRef(_)
6379            | ExprKind::SubroutineCodeRef(_)
6380            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
6381                target: Box::new(Expr { kind, line: rline }),
6382                args: vec![lhs],
6383                ampersand: false,
6384                pass_caller_arglist: false,
6385            },
6386
6387            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
6388            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
6389            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
6390            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
6391                ExprKind::IndirectCall {
6392                    target: inner,
6393                    args: vec![lhs],
6394                    ampersand: false,
6395                    pass_caller_arglist: false,
6396                }
6397            }
6398
6399            other => {
6400                return Err(self.syntax_err(
6401                    format!(
6402                        "right-hand side of `|>` must be a call, builtin, or coderef \
6403                         expression (got {})",
6404                        Self::expr_kind_name(&other)
6405                    ),
6406                    line,
6407                ));
6408            }
6409        };
6410        Ok(Expr {
6411            kind: new_kind,
6412            line,
6413        })
6414    }
6415
6416    /// Short label for an `ExprKind` (used in `|>` error messages).
6417    fn expr_kind_name(kind: &ExprKind) -> &'static str {
6418        match kind {
6419            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
6420            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
6421            ExprKind::BinOp { .. } => "binary expression",
6422            ExprKind::UnaryOp { .. } => "unary expression",
6423            ExprKind::Ternary { .. } => "ternary expression",
6424            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
6425            ExprKind::List(_) => "list expression",
6426            ExprKind::Range { .. } => "range expression",
6427            _ => "expression",
6428        }
6429    }
6430
6431    // or / not (lowest precedence word operators)
6432    fn parse_or_word(&mut self) -> PerlResult<Expr> {
6433        let mut left = self.parse_and_word()?;
6434        while matches!(self.peek(), Token::LogOrWord) {
6435            let line = left.line;
6436            self.advance();
6437            let right = self.parse_and_word()?;
6438            left = Expr {
6439                kind: ExprKind::BinOp {
6440                    left: Box::new(left),
6441                    op: BinOp::LogOrWord,
6442                    right: Box::new(right),
6443                },
6444                line,
6445            };
6446        }
6447        Ok(left)
6448    }
6449
6450    fn parse_and_word(&mut self) -> PerlResult<Expr> {
6451        let mut left = self.parse_not_word()?;
6452        while matches!(self.peek(), Token::LogAndWord) {
6453            let line = left.line;
6454            self.advance();
6455            let right = self.parse_not_word()?;
6456            left = Expr {
6457                kind: ExprKind::BinOp {
6458                    left: Box::new(left),
6459                    op: BinOp::LogAndWord,
6460                    right: Box::new(right),
6461                },
6462                line,
6463            };
6464        }
6465        Ok(left)
6466    }
6467
6468    fn parse_not_word(&mut self) -> PerlResult<Expr> {
6469        if matches!(self.peek(), Token::LogNotWord) {
6470            let line = self.peek_line();
6471            self.advance();
6472            let expr = self.parse_not_word()?;
6473            return Ok(Expr {
6474                kind: ExprKind::UnaryOp {
6475                    op: UnaryOp::LogNotWord,
6476                    expr: Box::new(expr),
6477                },
6478                line,
6479            });
6480        }
6481        self.parse_range()
6482    }
6483
6484    fn parse_log_or(&mut self) -> PerlResult<Expr> {
6485        let mut left = self.parse_log_and()?;
6486        loop {
6487            let op = match self.peek() {
6488                Token::LogOr => BinOp::LogOr,
6489                Token::DefinedOr => BinOp::DefinedOr,
6490                _ => break,
6491            };
6492            let line = left.line;
6493            self.advance();
6494            let right = self.parse_log_and()?;
6495            left = Expr {
6496                kind: ExprKind::BinOp {
6497                    left: Box::new(left),
6498                    op,
6499                    right: Box::new(right),
6500                },
6501                line,
6502            };
6503        }
6504        Ok(left)
6505    }
6506
6507    fn parse_log_and(&mut self) -> PerlResult<Expr> {
6508        let mut left = self.parse_bit_or()?;
6509        while matches!(self.peek(), Token::LogAnd) {
6510            let line = left.line;
6511            self.advance();
6512            let right = self.parse_bit_or()?;
6513            left = Expr {
6514                kind: ExprKind::BinOp {
6515                    left: Box::new(left),
6516                    op: BinOp::LogAnd,
6517                    right: Box::new(right),
6518                },
6519                line,
6520            };
6521        }
6522        Ok(left)
6523    }
6524
6525    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
6526        let mut left = self.parse_bit_xor()?;
6527        while matches!(self.peek(), Token::BitOr) {
6528            let line = left.line;
6529            self.advance();
6530            let right = self.parse_bit_xor()?;
6531            left = Expr {
6532                kind: ExprKind::BinOp {
6533                    left: Box::new(left),
6534                    op: BinOp::BitOr,
6535                    right: Box::new(right),
6536                },
6537                line,
6538            };
6539        }
6540        Ok(left)
6541    }
6542
6543    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
6544        let mut left = self.parse_bit_and()?;
6545        while matches!(self.peek(), Token::BitXor) {
6546            let line = left.line;
6547            self.advance();
6548            let right = self.parse_bit_and()?;
6549            left = Expr {
6550                kind: ExprKind::BinOp {
6551                    left: Box::new(left),
6552                    op: BinOp::BitXor,
6553                    right: Box::new(right),
6554                },
6555                line,
6556            };
6557        }
6558        Ok(left)
6559    }
6560
6561    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
6562        let mut left = self.parse_equality()?;
6563        while matches!(self.peek(), Token::BitAnd) {
6564            let line = left.line;
6565            self.advance();
6566            let right = self.parse_equality()?;
6567            left = Expr {
6568                kind: ExprKind::BinOp {
6569                    left: Box::new(left),
6570                    op: BinOp::BitAnd,
6571                    right: Box::new(right),
6572                },
6573                line,
6574            };
6575        }
6576        Ok(left)
6577    }
6578
6579    fn parse_equality(&mut self) -> PerlResult<Expr> {
6580        let mut left = self.parse_comparison()?;
6581        loop {
6582            let op = match self.peek() {
6583                Token::NumEq => BinOp::NumEq,
6584                Token::NumNe => BinOp::NumNe,
6585                Token::StrEq => BinOp::StrEq,
6586                Token::StrNe => BinOp::StrNe,
6587                Token::Spaceship => BinOp::Spaceship,
6588                Token::StrCmp => BinOp::StrCmp,
6589                _ => break,
6590            };
6591            let line = left.line;
6592            self.advance();
6593            let right = self.parse_comparison()?;
6594            left = Expr {
6595                kind: ExprKind::BinOp {
6596                    left: Box::new(left),
6597                    op,
6598                    right: Box::new(right),
6599                },
6600                line,
6601            };
6602        }
6603        Ok(left)
6604    }
6605
6606    fn parse_comparison(&mut self) -> PerlResult<Expr> {
6607        let left = self.parse_shift()?;
6608        let first_op = match self.peek() {
6609            Token::NumLt => BinOp::NumLt,
6610            Token::NumGt => BinOp::NumGt,
6611            Token::NumLe => BinOp::NumLe,
6612            Token::NumGe => BinOp::NumGe,
6613            Token::StrLt => BinOp::StrLt,
6614            Token::StrGt => BinOp::StrGt,
6615            Token::StrLe => BinOp::StrLe,
6616            Token::StrGe => BinOp::StrGe,
6617            _ => return Ok(left),
6618        };
6619        let line = left.line;
6620        self.advance();
6621        let middle = self.parse_shift()?;
6622
6623        let second_op = match self.peek() {
6624            Token::NumLt => Some(BinOp::NumLt),
6625            Token::NumGt => Some(BinOp::NumGt),
6626            Token::NumLe => Some(BinOp::NumLe),
6627            Token::NumGe => Some(BinOp::NumGe),
6628            Token::StrLt => Some(BinOp::StrLt),
6629            Token::StrGt => Some(BinOp::StrGt),
6630            Token::StrLe => Some(BinOp::StrLe),
6631            Token::StrGe => Some(BinOp::StrGe),
6632            _ => None,
6633        };
6634
6635        if second_op.is_none() {
6636            return Ok(Expr {
6637                kind: ExprKind::BinOp {
6638                    left: Box::new(left),
6639                    op: first_op,
6640                    right: Box::new(middle),
6641                },
6642                line,
6643            });
6644        }
6645
6646        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
6647        // Collect all operands and operators for chains like `1 < x < 10 < y`
6648        let mut operands = vec![left, middle];
6649        let mut ops = vec![first_op];
6650
6651        loop {
6652            let op = match self.peek() {
6653                Token::NumLt => BinOp::NumLt,
6654                Token::NumGt => BinOp::NumGt,
6655                Token::NumLe => BinOp::NumLe,
6656                Token::NumGe => BinOp::NumGe,
6657                Token::StrLt => BinOp::StrLt,
6658                Token::StrGt => BinOp::StrGt,
6659                Token::StrLe => BinOp::StrLe,
6660                Token::StrGe => BinOp::StrGe,
6661                _ => break,
6662            };
6663            self.advance();
6664            ops.push(op);
6665            operands.push(self.parse_shift()?);
6666        }
6667
6668        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
6669        let mut result = Expr {
6670            kind: ExprKind::BinOp {
6671                left: Box::new(operands[0].clone()),
6672                op: ops[0],
6673                right: Box::new(operands[1].clone()),
6674            },
6675            line,
6676        };
6677
6678        for i in 1..ops.len() {
6679            let cmp = Expr {
6680                kind: ExprKind::BinOp {
6681                    left: Box::new(operands[i].clone()),
6682                    op: ops[i],
6683                    right: Box::new(operands[i + 1].clone()),
6684                },
6685                line,
6686            };
6687            result = Expr {
6688                kind: ExprKind::BinOp {
6689                    left: Box::new(result),
6690                    op: BinOp::LogAnd,
6691                    right: Box::new(cmp),
6692                },
6693                line,
6694            };
6695        }
6696
6697        Ok(result)
6698    }
6699
6700    fn parse_shift(&mut self) -> PerlResult<Expr> {
6701        let mut left = self.parse_addition()?;
6702        loop {
6703            let op = match self.peek() {
6704                Token::ShiftLeft => BinOp::ShiftLeft,
6705                Token::ShiftRight => BinOp::ShiftRight,
6706                _ => break,
6707            };
6708            let line = left.line;
6709            self.advance();
6710            let right = self.parse_addition()?;
6711            left = Expr {
6712                kind: ExprKind::BinOp {
6713                    left: Box::new(left),
6714                    op,
6715                    right: Box::new(right),
6716                },
6717                line,
6718            };
6719        }
6720        Ok(left)
6721    }
6722
6723    fn parse_addition(&mut self) -> PerlResult<Expr> {
6724        let mut left = self.parse_multiplication()?;
6725        loop {
6726            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
6727            // the next statement, not a binary operator continuing this expression.
6728            let op = match self.peek() {
6729                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
6730                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
6731                Token::Dot => BinOp::Concat,
6732                _ => break,
6733            };
6734            let line = left.line;
6735            self.advance();
6736            let right = self.parse_multiplication()?;
6737            left = Expr {
6738                kind: ExprKind::BinOp {
6739                    left: Box::new(left),
6740                    op,
6741                    right: Box::new(right),
6742                },
6743                line,
6744            };
6745        }
6746        Ok(left)
6747    }
6748
6749    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
6750        let mut left = self.parse_regex_bind()?;
6751        loop {
6752            let op = match self.peek() {
6753                Token::Star => BinOp::Mul,
6754                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
6755                // Implicit semicolon: `%` on a new line is a hash dereference or hash
6756                // sigil for the next statement, not modulo operator on this expression.
6757                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
6758                Token::X => {
6759                    let line = left.line;
6760                    self.advance();
6761                    let right = self.parse_regex_bind()?;
6762                    left = Expr {
6763                        kind: ExprKind::Repeat {
6764                            expr: Box::new(left),
6765                            count: Box::new(right),
6766                        },
6767                        line,
6768                    };
6769                    continue;
6770                }
6771                _ => break,
6772            };
6773            let line = left.line;
6774            self.advance();
6775            let right = self.parse_regex_bind()?;
6776            left = Expr {
6777                kind: ExprKind::BinOp {
6778                    left: Box::new(left),
6779                    op,
6780                    right: Box::new(right),
6781                },
6782                line,
6783            };
6784        }
6785        Ok(left)
6786    }
6787
6788    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
6789        let left = self.parse_unary()?;
6790        match self.peek() {
6791            Token::BindMatch => {
6792                let line = left.line;
6793                self.advance();
6794                match self.peek().clone() {
6795                    Token::Regex(pattern, flags, delim) => {
6796                        self.advance();
6797                        Ok(Expr {
6798                            kind: ExprKind::Match {
6799                                expr: Box::new(left),
6800                                pattern,
6801                                flags,
6802                                scalar_g: false,
6803                                delim,
6804                            },
6805                            line,
6806                        })
6807                    }
6808                    Token::Ident(ref s) if s.starts_with('\x00') => {
6809                        let (Token::Ident(encoded), _) = self.advance() else {
6810                            unreachable!()
6811                        };
6812                        let parts: Vec<&str> = encoded.split('\x00').collect();
6813                        if parts.len() >= 4 && parts[1] == "s" {
6814                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6815                            Ok(Expr {
6816                                kind: ExprKind::Substitution {
6817                                    expr: Box::new(left),
6818                                    pattern: parts[2].to_string(),
6819                                    replacement: parts[3].to_string(),
6820                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6821                                    delim,
6822                                },
6823                                line,
6824                            })
6825                        } else if parts.len() >= 4 && parts[1] == "tr" {
6826                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6827                            Ok(Expr {
6828                                kind: ExprKind::Transliterate {
6829                                    expr: Box::new(left),
6830                                    from: parts[2].to_string(),
6831                                    to: parts[3].to_string(),
6832                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6833                                    delim,
6834                                },
6835                                line,
6836                            })
6837                        } else {
6838                            Err(self.syntax_err("Invalid regex binding", line))
6839                        }
6840                    }
6841                    _ => {
6842                        let rhs = self.parse_unary()?;
6843                        Ok(Expr {
6844                            kind: ExprKind::BinOp {
6845                                left: Box::new(left),
6846                                op: BinOp::BindMatch,
6847                                right: Box::new(rhs),
6848                            },
6849                            line,
6850                        })
6851                    }
6852                }
6853            }
6854            Token::BindNotMatch => {
6855                let line = left.line;
6856                self.advance();
6857                match self.peek().clone() {
6858                    Token::Regex(pattern, flags, delim) => {
6859                        self.advance();
6860                        Ok(Expr {
6861                            kind: ExprKind::UnaryOp {
6862                                op: UnaryOp::LogNot,
6863                                expr: Box::new(Expr {
6864                                    kind: ExprKind::Match {
6865                                        expr: Box::new(left),
6866                                        pattern,
6867                                        flags,
6868                                        scalar_g: false,
6869                                        delim,
6870                                    },
6871                                    line,
6872                                }),
6873                            },
6874                            line,
6875                        })
6876                    }
6877                    Token::Ident(ref s) if s.starts_with('\x00') => {
6878                        let (Token::Ident(encoded), _) = self.advance() else {
6879                            unreachable!()
6880                        };
6881                        let parts: Vec<&str> = encoded.split('\x00').collect();
6882                        if parts.len() >= 4 && parts[1] == "s" {
6883                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6884                            Ok(Expr {
6885                                kind: ExprKind::UnaryOp {
6886                                    op: UnaryOp::LogNot,
6887                                    expr: Box::new(Expr {
6888                                        kind: ExprKind::Substitution {
6889                                            expr: Box::new(left),
6890                                            pattern: parts[2].to_string(),
6891                                            replacement: parts[3].to_string(),
6892                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6893                                            delim,
6894                                        },
6895                                        line,
6896                                    }),
6897                                },
6898                                line,
6899                            })
6900                        } else if parts.len() >= 4 && parts[1] == "tr" {
6901                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6902                            Ok(Expr {
6903                                kind: ExprKind::UnaryOp {
6904                                    op: UnaryOp::LogNot,
6905                                    expr: Box::new(Expr {
6906                                        kind: ExprKind::Transliterate {
6907                                            expr: Box::new(left),
6908                                            from: parts[2].to_string(),
6909                                            to: parts[3].to_string(),
6910                                            flags: parts.get(4).unwrap_or(&"").to_string(),
6911                                            delim,
6912                                        },
6913                                        line,
6914                                    }),
6915                                },
6916                                line,
6917                            })
6918                        } else {
6919                            Err(self.syntax_err("Invalid regex binding after !~", line))
6920                        }
6921                    }
6922                    _ => {
6923                        let rhs = self.parse_unary()?;
6924                        Ok(Expr {
6925                            kind: ExprKind::BinOp {
6926                                left: Box::new(left),
6927                                op: BinOp::BindNotMatch,
6928                                right: Box::new(rhs),
6929                            },
6930                            line,
6931                        })
6932                    }
6933                }
6934            }
6935            _ => Ok(left),
6936        }
6937    }
6938
6939    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
6940    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
6941    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
6942        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
6943        let result = self.parse_range();
6944        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
6945        result
6946    }
6947
6948    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
6949    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
6950    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
6951    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
6952    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
6953    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
6954    fn parse_range(&mut self) -> PerlResult<Expr> {
6955        let left = self.parse_log_or()?;
6956        let line = left.line;
6957        // `1..10` or `1...10` (traditional) or `1:10` (short form)
6958        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
6959            (true, false)
6960        } else if self.eat(&Token::Range) {
6961            (false, false)
6962        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
6963            // `1:10` short form — only valid for numeric ranges, not ternary
6964            // Lookahead: must be followed by something that looks like a range endpoint
6965            (false, true)
6966        } else {
6967            return Ok(left);
6968        };
6969        let right = self.parse_log_or()?;
6970        // Optional step: `1..100:2` or `1:100:2`
6971        let step = if self.eat(&Token::Colon) {
6972            Some(Box::new(self.parse_unary()?))
6973        } else {
6974            None
6975        };
6976        Ok(Expr {
6977            kind: ExprKind::Range {
6978                from: Box::new(left),
6979                to: Box::new(right),
6980                exclusive,
6981                step,
6982            },
6983            line,
6984        })
6985    }
6986
6987    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
6988    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
6989        let mut name = match self.advance() {
6990            (Token::Ident(n), _) => n,
6991            (tok, l) => {
6992                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
6993            }
6994        };
6995        while self.eat(&Token::PackageSep) {
6996            match self.advance() {
6997                (Token::Ident(part), _) => {
6998                    name.push_str("::");
6999                    name.push_str(&part);
7000                }
7001                (tok, l) => {
7002                    return Err(self
7003                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
7004                }
7005            }
7006        }
7007        Ok(name)
7008    }
7009
7010    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
7011    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
7012        self.parse_package_qualified_identifier()
7013    }
7014
7015    fn parse_unary(&mut self) -> PerlResult<Expr> {
7016        let line = self.peek_line();
7017        match self.peek().clone() {
7018            Token::Minus => {
7019                self.advance();
7020                let expr = self.parse_power()?;
7021                Ok(Expr {
7022                    kind: ExprKind::UnaryOp {
7023                        op: UnaryOp::Negate,
7024                        expr: Box::new(expr),
7025                    },
7026                    line,
7027                })
7028            }
7029            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
7030            // and for scalar context; treat as a no-op on the parsed operand.
7031            Token::Plus => {
7032                self.advance();
7033                self.parse_unary()
7034            }
7035            Token::LogNot => {
7036                self.advance();
7037                let expr = self.parse_unary()?;
7038                Ok(Expr {
7039                    kind: ExprKind::UnaryOp {
7040                        op: UnaryOp::LogNot,
7041                        expr: Box::new(expr),
7042                    },
7043                    line,
7044                })
7045            }
7046            Token::BitNot => {
7047                self.advance();
7048                let expr = self.parse_unary()?;
7049                Ok(Expr {
7050                    kind: ExprKind::UnaryOp {
7051                        op: UnaryOp::BitNot,
7052                        expr: Box::new(expr),
7053                    },
7054                    line,
7055                })
7056            }
7057            Token::Increment => {
7058                self.advance();
7059                let expr = self.parse_postfix()?;
7060                Ok(Expr {
7061                    kind: ExprKind::UnaryOp {
7062                        op: UnaryOp::PreIncrement,
7063                        expr: Box::new(expr),
7064                    },
7065                    line,
7066                })
7067            }
7068            Token::Decrement => {
7069                self.advance();
7070                let expr = self.parse_postfix()?;
7071                Ok(Expr {
7072                    kind: ExprKind::UnaryOp {
7073                        op: UnaryOp::PreDecrement,
7074                        expr: Box::new(expr),
7075                    },
7076                    line,
7077                })
7078            }
7079            Token::BitAnd => {
7080                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
7081                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
7082                self.advance();
7083                if matches!(self.peek(), Token::LBrace) {
7084                    self.advance();
7085                    let inner = self.parse_expression()?;
7086                    self.expect(&Token::RBrace)?;
7087                    return Ok(Expr {
7088                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
7089                        line,
7090                    });
7091                }
7092                if matches!(self.peek(), Token::Ident(_)) {
7093                    let name = self.parse_qualified_subroutine_name()?;
7094                    return Ok(Expr {
7095                        kind: ExprKind::SubroutineRef(name),
7096                        line,
7097                    });
7098                }
7099                let target = self.parse_primary()?;
7100                if matches!(self.peek(), Token::LParen) {
7101                    self.advance();
7102                    let args = self.parse_arg_list()?;
7103                    self.expect(&Token::RParen)?;
7104                    return Ok(Expr {
7105                        kind: ExprKind::IndirectCall {
7106                            target: Box::new(target),
7107                            args,
7108                            ampersand: true,
7109                            pass_caller_arglist: false,
7110                        },
7111                        line,
7112                    });
7113                }
7114                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
7115                Ok(Expr {
7116                    kind: ExprKind::IndirectCall {
7117                        target: Box::new(target),
7118                        args: vec![],
7119                        ampersand: true,
7120                        pass_caller_arglist: true,
7121                    },
7122                    line,
7123                })
7124            }
7125            Token::Backslash => {
7126                self.advance();
7127                let expr = self.parse_unary()?;
7128                if let ExprKind::SubroutineRef(name) = expr.kind {
7129                    return Ok(Expr {
7130                        kind: ExprKind::SubroutineCodeRef(name),
7131                        line,
7132                    });
7133                }
7134                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
7135                    return Ok(expr);
7136                }
7137                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
7138                Ok(Expr {
7139                    kind: ExprKind::ScalarRef(Box::new(expr)),
7140                    line,
7141                })
7142            }
7143            Token::FileTest(op) => {
7144                self.advance();
7145                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
7146                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
7147                    Expr {
7148                        kind: ExprKind::ScalarVar("_".into()),
7149                        line: self.peek_line(),
7150                    }
7151                } else {
7152                    self.parse_unary()?
7153                };
7154                Ok(Expr {
7155                    kind: ExprKind::FileTest {
7156                        op,
7157                        expr: Box::new(expr),
7158                    },
7159                    line,
7160                })
7161            }
7162            _ => self.parse_power(),
7163        }
7164    }
7165
7166    fn parse_power(&mut self) -> PerlResult<Expr> {
7167        let left = self.parse_postfix()?;
7168        if matches!(self.peek(), Token::Power) {
7169            let line = left.line;
7170            self.advance();
7171            let right = self.parse_unary()?; // right-associative
7172            return Ok(Expr {
7173                kind: ExprKind::BinOp {
7174                    left: Box::new(left),
7175                    op: BinOp::Pow,
7176                    right: Box::new(right),
7177                },
7178                line,
7179            });
7180        }
7181        Ok(left)
7182    }
7183
7184    fn parse_postfix(&mut self) -> PerlResult<Expr> {
7185        let mut expr = self.parse_primary()?;
7186        loop {
7187            match self.peek().clone() {
7188                Token::Increment => {
7189                    // Implicit semicolon: `++` on a new line is a prefix operator
7190                    // on the next statement, not postfix on the previous expression.
7191                    if self.peek_line() > self.prev_line() {
7192                        break;
7193                    }
7194                    let line = expr.line;
7195                    self.advance();
7196                    expr = Expr {
7197                        kind: ExprKind::PostfixOp {
7198                            expr: Box::new(expr),
7199                            op: PostfixOp::Increment,
7200                        },
7201                        line,
7202                    };
7203                }
7204                Token::Decrement => {
7205                    // Implicit semicolon: `--` on a new line is a prefix operator
7206                    // on the next statement, not postfix on the previous expression.
7207                    if self.peek_line() > self.prev_line() {
7208                        break;
7209                    }
7210                    let line = expr.line;
7211                    self.advance();
7212                    expr = Expr {
7213                        kind: ExprKind::PostfixOp {
7214                            expr: Box::new(expr),
7215                            op: PostfixOp::Decrement,
7216                        },
7217                        line,
7218                    };
7219                }
7220                Token::LParen => {
7221                    if self.suppress_indirect_paren_call > 0 {
7222                        break;
7223                    }
7224                    // Implicit semicolon: `(` on a new line after an expression
7225                    // is a new statement, not a postfix code-ref call.
7226                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
7227                    if self.peek_line() > self.prev_line() {
7228                        break;
7229                    }
7230                    let line = expr.line;
7231                    self.advance();
7232                    let args = self.parse_arg_list()?;
7233                    self.expect(&Token::RParen)?;
7234                    expr = Expr {
7235                        kind: ExprKind::IndirectCall {
7236                            target: Box::new(expr),
7237                            args,
7238                            ampersand: false,
7239                            pass_caller_arglist: false,
7240                        },
7241                        line,
7242                    };
7243                }
7244                Token::Arrow => {
7245                    let line = expr.line;
7246                    self.advance();
7247                    match self.peek().clone() {
7248                        Token::LBracket => {
7249                            self.advance();
7250                            let index = self.parse_expression()?;
7251                            self.expect(&Token::RBracket)?;
7252                            expr = Expr {
7253                                kind: ExprKind::ArrowDeref {
7254                                    expr: Box::new(expr),
7255                                    index: Box::new(index),
7256                                    kind: DerefKind::Array,
7257                                },
7258                                line,
7259                            };
7260                        }
7261                        Token::LBrace => {
7262                            self.advance();
7263                            let key = self.parse_hash_subscript_key()?;
7264                            self.expect(&Token::RBrace)?;
7265                            expr = Expr {
7266                                kind: ExprKind::ArrowDeref {
7267                                    expr: Box::new(expr),
7268                                    index: Box::new(key),
7269                                    kind: DerefKind::Hash,
7270                                },
7271                                line,
7272                            };
7273                        }
7274                        Token::LParen => {
7275                            self.advance();
7276                            let args = self.parse_arg_list()?;
7277                            self.expect(&Token::RParen)?;
7278                            expr = Expr {
7279                                kind: ExprKind::ArrowDeref {
7280                                    expr: Box::new(expr),
7281                                    index: Box::new(Expr {
7282                                        kind: ExprKind::List(args),
7283                                        line,
7284                                    }),
7285                                    kind: DerefKind::Call,
7286                                },
7287                                line,
7288                            };
7289                        }
7290                        Token::Ident(method) => {
7291                            self.advance();
7292                            if method == "SUPER" {
7293                                self.expect(&Token::PackageSep)?;
7294                                let real_method = match self.advance() {
7295                                    (Token::Ident(n), _) => n,
7296                                    (tok, l) => {
7297                                        return Err(self.syntax_err(
7298                                            format!(
7299                                                "Expected method name after SUPER::, got {:?}",
7300                                                tok
7301                                            ),
7302                                            l,
7303                                        ));
7304                                    }
7305                                };
7306                                let args = if self.eat(&Token::LParen) {
7307                                    let a = self.parse_arg_list()?;
7308                                    self.expect(&Token::RParen)?;
7309                                    a
7310                                } else {
7311                                    self.parse_method_arg_list_no_paren()?
7312                                };
7313                                expr = Expr {
7314                                    kind: ExprKind::MethodCall {
7315                                        object: Box::new(expr),
7316                                        method: real_method,
7317                                        args,
7318                                        super_call: true,
7319                                    },
7320                                    line,
7321                                };
7322                            } else {
7323                                let mut method_name = method;
7324                                while self.eat(&Token::PackageSep) {
7325                                    match self.advance() {
7326                                        (Token::Ident(part), _) => {
7327                                            method_name.push_str("::");
7328                                            method_name.push_str(&part);
7329                                        }
7330                                        (tok, l) => {
7331                                            return Err(self.syntax_err(
7332                                                format!(
7333                                                    "Expected identifier after :: in method name, got {:?}",
7334                                                    tok
7335                                                ),
7336                                                l,
7337                                            ));
7338                                        }
7339                                    }
7340                                }
7341                                let args = if self.eat(&Token::LParen) {
7342                                    let a = self.parse_arg_list()?;
7343                                    self.expect(&Token::RParen)?;
7344                                    a
7345                                } else {
7346                                    self.parse_method_arg_list_no_paren()?
7347                                };
7348                                expr = Expr {
7349                                    kind: ExprKind::MethodCall {
7350                                        object: Box::new(expr),
7351                                        method: method_name,
7352                                        args,
7353                                        super_call: false,
7354                                    },
7355                                    line,
7356                                };
7357                            }
7358                        }
7359                        // Postfix dereference (Perl 5.20+, default 5.24+):
7360                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
7361                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
7362                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
7363                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
7364                        Token::ArrayAt => {
7365                            self.advance(); // consume `@`
7366                            match self.peek().clone() {
7367                                Token::Star => {
7368                                    self.advance();
7369                                    expr = Expr {
7370                                        kind: ExprKind::Deref {
7371                                            expr: Box::new(expr),
7372                                            kind: Sigil::Array,
7373                                        },
7374                                        line,
7375                                    };
7376                                }
7377                                Token::LBracket => {
7378                                    self.advance();
7379                                    let mut indices = Vec::new();
7380                                    while !matches!(self.peek(), Token::RBracket | Token::Eof) {
7381                                        indices.push(self.parse_assign_expr()?);
7382                                        if !self.eat(&Token::Comma) {
7383                                            break;
7384                                        }
7385                                    }
7386                                    self.expect(&Token::RBracket)?;
7387                                    let source = Expr {
7388                                        kind: ExprKind::Deref {
7389                                            expr: Box::new(expr),
7390                                            kind: Sigil::Array,
7391                                        },
7392                                        line,
7393                                    };
7394                                    expr = Expr {
7395                                        kind: ExprKind::AnonymousListSlice {
7396                                            source: Box::new(source),
7397                                            indices,
7398                                        },
7399                                        line,
7400                                    };
7401                                }
7402                                Token::LBrace => {
7403                                    self.advance();
7404                                    let mut keys = Vec::new();
7405                                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7406                                        keys.push(self.parse_assign_expr()?);
7407                                        if !self.eat(&Token::Comma) {
7408                                            break;
7409                                        }
7410                                    }
7411                                    self.expect(&Token::RBrace)?;
7412                                    expr = Expr {
7413                                        kind: ExprKind::HashSliceDeref {
7414                                            container: Box::new(expr),
7415                                            keys,
7416                                        },
7417                                        line,
7418                                    };
7419                                }
7420                                tok => {
7421                                    return Err(self.syntax_err(
7422                                        format!(
7423                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
7424                                            tok
7425                                        ),
7426                                        line,
7427                                    ));
7428                                }
7429                            }
7430                        }
7431                        Token::HashPercent => {
7432                            self.advance(); // consume `%`
7433                            match self.peek().clone() {
7434                                Token::Star => {
7435                                    self.advance();
7436                                    expr = Expr {
7437                                        kind: ExprKind::Deref {
7438                                            expr: Box::new(expr),
7439                                            kind: Sigil::Hash,
7440                                        },
7441                                        line,
7442                                    };
7443                                }
7444                                tok => {
7445                                    return Err(self.syntax_err(
7446                                        format!("Expected `*` after `->%`, got {:?}", tok),
7447                                        line,
7448                                    ));
7449                                }
7450                            }
7451                        }
7452                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
7453                        Token::X => {
7454                            self.advance();
7455                            let args = if self.eat(&Token::LParen) {
7456                                let a = self.parse_arg_list()?;
7457                                self.expect(&Token::RParen)?;
7458                                a
7459                            } else {
7460                                self.parse_method_arg_list_no_paren()?
7461                            };
7462                            expr = Expr {
7463                                kind: ExprKind::MethodCall {
7464                                    object: Box::new(expr),
7465                                    method: "x".to_string(),
7466                                    args,
7467                                    super_call: false,
7468                                },
7469                                line,
7470                            };
7471                        }
7472                        _ => break,
7473                    }
7474                }
7475                Token::LBracket => {
7476                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
7477                    let line = expr.line;
7478                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
7479                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7480                            let name = name.clone();
7481                            self.advance();
7482                            let index = self.parse_expression()?;
7483                            self.expect(&Token::RBracket)?;
7484                            expr = Expr {
7485                                kind: ExprKind::ArrayElement {
7486                                    array: name,
7487                                    index: Box::new(index),
7488                                },
7489                                line,
7490                            };
7491                        }
7492                    } else if postfix_lbracket_is_arrow_container(&expr) {
7493                        self.advance();
7494                        let indices = self.parse_arg_list()?;
7495                        self.expect(&Token::RBracket)?;
7496                        expr = Expr {
7497                            kind: ExprKind::ArrowDeref {
7498                                expr: Box::new(expr),
7499                                index: Box::new(Expr {
7500                                    kind: ExprKind::List(indices),
7501                                    line,
7502                                }),
7503                                kind: DerefKind::Array,
7504                            },
7505                            line,
7506                        };
7507                    } else {
7508                        self.advance();
7509                        let indices = self.parse_arg_list()?;
7510                        self.expect(&Token::RBracket)?;
7511                        expr = Expr {
7512                            kind: ExprKind::AnonymousListSlice {
7513                                source: Box::new(expr),
7514                                indices,
7515                            },
7516                            line,
7517                        };
7518                    }
7519                }
7520                Token::LBrace => {
7521                    if self.suppress_scalar_hash_brace > 0 {
7522                        break;
7523                    }
7524                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
7525                    // not a hash subscript on the preceding expression.
7526                    if self.peek_line() > self.prev_line() {
7527                        break;
7528                    }
7529                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
7530                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
7531                    let line = expr.line;
7532                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
7533                    let is_chainable_hash_subscript = is_scalar_named_hash
7534                        || matches!(
7535                            expr.kind,
7536                            ExprKind::HashElement { .. }
7537                                | ExprKind::ArrayElement { .. }
7538                                | ExprKind::ArrowDeref { .. }
7539                                | ExprKind::Deref {
7540                                    kind: Sigil::Scalar,
7541                                    ..
7542                                }
7543                        );
7544                    if !is_chainable_hash_subscript {
7545                        break;
7546                    }
7547                    self.advance();
7548                    let key = self.parse_hash_subscript_key()?;
7549                    self.expect(&Token::RBrace)?;
7550                    expr = if is_scalar_named_hash {
7551                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7552                            let name = name.clone();
7553                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
7554                            if name == "_" {
7555                                Expr {
7556                                    kind: ExprKind::ArrowDeref {
7557                                        expr: Box::new(Expr {
7558                                            kind: ExprKind::ScalarVar("_".into()),
7559                                            line,
7560                                        }),
7561                                        index: Box::new(key),
7562                                        kind: DerefKind::Hash,
7563                                    },
7564                                    line,
7565                                }
7566                            } else {
7567                                Expr {
7568                                    kind: ExprKind::HashElement {
7569                                        hash: name,
7570                                        key: Box::new(key),
7571                                    },
7572                                    line,
7573                                }
7574                            }
7575                        } else {
7576                            unreachable!("is_scalar_named_hash implies ScalarVar");
7577                        }
7578                    } else {
7579                        Expr {
7580                            kind: ExprKind::ArrowDeref {
7581                                expr: Box::new(expr),
7582                                index: Box::new(key),
7583                                kind: DerefKind::Hash,
7584                            },
7585                            line,
7586                        }
7587                    };
7588                }
7589                _ => break,
7590            }
7591        }
7592        Ok(expr)
7593    }
7594
7595    fn parse_primary(&mut self) -> PerlResult<Expr> {
7596        let line = self.peek_line();
7597        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
7598        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
7599        // assigned value(s); has the side effect of declaring the variable in
7600        // the current scope.  See `ExprKind::MyExpr`.
7601        if let Token::Ident(ref kw) = self.peek().clone() {
7602            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
7603                let kw_owned = kw.clone();
7604                // Parse exactly like the statement form via `parse_my_our_local`,
7605                // then unwrap the resulting `StmtKind::*` back into a list of
7606                // `VarDecl`s for the expression node.  This re-uses the full
7607                // syntax (typed sigs, list destructuring, type annotations).
7608                let saved_pos = self.pos;
7609                let stmt = self.parse_my_our_local(&kw_owned, false)?;
7610                let decls = match stmt.kind {
7611                    StmtKind::My(d)
7612                    | StmtKind::Our(d)
7613                    | StmtKind::State(d)
7614                    | StmtKind::Local(d) => d,
7615                    _ => {
7616                        // `local *FOO = …` / non-decl forms — fall back to the
7617                        // statement parser (already advanced); restore position
7618                        // and let the surrounding code handle it as a statement
7619                        // by erroring loudly here.
7620                        self.pos = saved_pos;
7621                        return Err(self.syntax_err(
7622                            "`my`/`our`/`local` in expression must declare variables",
7623                            line,
7624                        ));
7625                    }
7626                };
7627                return Ok(Expr {
7628                    kind: ExprKind::MyExpr {
7629                        keyword: kw_owned,
7630                        decls,
7631                    },
7632                    line,
7633                });
7634            }
7635        }
7636        match self.peek().clone() {
7637            Token::Integer(n) => {
7638                self.advance();
7639                Ok(Expr {
7640                    kind: ExprKind::Integer(n),
7641                    line,
7642                })
7643            }
7644            Token::Float(f) => {
7645                self.advance();
7646                Ok(Expr {
7647                    kind: ExprKind::Float(f),
7648                    line,
7649                })
7650            }
7651            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
7652            // Valid in any expression position; evaluates the block and yields its last value.
7653            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
7654            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
7655            // instead pipe-applied as a coderef — that path is never reached from here.
7656            Token::ArrowBrace => {
7657                self.advance();
7658                let mut stmts = Vec::new();
7659                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7660                    if self.eat(&Token::Semicolon) {
7661                        continue;
7662                    }
7663                    stmts.push(self.parse_statement()?);
7664                }
7665                self.expect(&Token::RBrace)?;
7666                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
7667                let inner = Expr {
7668                    kind: ExprKind::CodeRef {
7669                        params: vec![],
7670                        body: stmts,
7671                    },
7672                    line: inner_line,
7673                };
7674                Ok(Expr {
7675                    kind: ExprKind::Do(Box::new(inner)),
7676                    line,
7677                })
7678            }
7679            Token::Star => {
7680                self.advance();
7681                if matches!(self.peek(), Token::LBrace) {
7682                    self.advance();
7683                    let inner = self.parse_expression()?;
7684                    self.expect(&Token::RBrace)?;
7685                    return Ok(Expr {
7686                        kind: ExprKind::Deref {
7687                            expr: Box::new(inner),
7688                            kind: Sigil::Typeglob,
7689                        },
7690                        line,
7691                    });
7692                }
7693                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
7694                if matches!(
7695                    self.peek(),
7696                    Token::ScalarVar(_)
7697                        | Token::ArrayVar(_)
7698                        | Token::HashVar(_)
7699                        | Token::DerefScalarVar(_)
7700                        | Token::HashPercent
7701                ) {
7702                    let inner = self.parse_postfix()?;
7703                    return Ok(Expr {
7704                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
7705                        line,
7706                    });
7707                }
7708                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
7709                let mut full_name = match self.advance() {
7710                    (Token::Ident(n), _) => n,
7711                    (Token::X, _) => "x".to_string(),
7712                    (tok, l) => {
7713                        return Err(self
7714                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
7715                    }
7716                };
7717                while self.eat(&Token::PackageSep) {
7718                    match self.advance() {
7719                        (Token::Ident(part), _) => {
7720                            full_name = format!("{}::{}", full_name, part);
7721                        }
7722                        (Token::X, _) => {
7723                            full_name = format!("{}::x", full_name);
7724                        }
7725                        (tok, l) => {
7726                            return Err(self.syntax_err(
7727                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
7728                                l,
7729                            ));
7730                        }
7731                    }
7732                }
7733                Ok(Expr {
7734                    kind: ExprKind::Typeglob(full_name),
7735                    line,
7736                })
7737            }
7738            Token::SingleString(s) => {
7739                self.advance();
7740                Ok(Expr {
7741                    kind: ExprKind::String(s),
7742                    line,
7743                })
7744            }
7745            Token::DoubleString(s) => {
7746                self.advance();
7747                self.parse_interpolated_string(&s, line)
7748            }
7749            Token::BacktickString(s) => {
7750                self.advance();
7751                let inner = self.parse_interpolated_string(&s, line)?;
7752                Ok(Expr {
7753                    kind: ExprKind::Qx(Box::new(inner)),
7754                    line,
7755                })
7756            }
7757            Token::HereDoc(_, body, interpolate) => {
7758                self.advance();
7759                if interpolate {
7760                    self.parse_interpolated_string(&body, line)
7761                } else {
7762                    Ok(Expr {
7763                        kind: ExprKind::String(body),
7764                        line,
7765                    })
7766                }
7767            }
7768            Token::Regex(pattern, flags, _delim) => {
7769                self.advance();
7770                Ok(Expr {
7771                    kind: ExprKind::Regex(pattern, flags),
7772                    line,
7773                })
7774            }
7775            Token::QW(words) => {
7776                self.advance();
7777                Ok(Expr {
7778                    kind: ExprKind::QW(words),
7779                    line,
7780                })
7781            }
7782            Token::DerefScalarVar(name) => {
7783                self.advance();
7784                Ok(Expr {
7785                    kind: ExprKind::Deref {
7786                        expr: Box::new(Expr {
7787                            kind: ExprKind::ScalarVar(name),
7788                            line,
7789                        }),
7790                        kind: Sigil::Scalar,
7791                    },
7792                    line,
7793                })
7794            }
7795            Token::ScalarVar(name) => {
7796                self.advance();
7797                Ok(Expr {
7798                    kind: ExprKind::ScalarVar(name),
7799                    line,
7800                })
7801            }
7802            Token::ArrayVar(name) => {
7803                self.advance();
7804                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
7805                match self.peek() {
7806                    Token::LBracket => {
7807                        self.advance();
7808                        let indices = self.parse_arg_list()?;
7809                        self.expect(&Token::RBracket)?;
7810                        Ok(Expr {
7811                            kind: ExprKind::ArraySlice {
7812                                array: name,
7813                                indices,
7814                            },
7815                            line,
7816                        })
7817                    }
7818                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
7819                        self.advance();
7820                        let keys = self.parse_arg_list()?;
7821                        self.expect(&Token::RBrace)?;
7822                        Ok(Expr {
7823                            kind: ExprKind::HashSlice { hash: name, keys },
7824                            line,
7825                        })
7826                    }
7827                    _ => Ok(Expr {
7828                        kind: ExprKind::ArrayVar(name),
7829                        line,
7830                    }),
7831                }
7832            }
7833            Token::HashVar(name) => {
7834                self.advance();
7835                Ok(Expr {
7836                    kind: ExprKind::HashVar(name),
7837                    line,
7838                })
7839            }
7840            Token::HashPercent => {
7841                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
7842                self.advance();
7843                if matches!(self.peek(), Token::ScalarVar(_)) {
7844                    let n = match self.advance() {
7845                        (Token::ScalarVar(n), _) => n,
7846                        (tok, l) => {
7847                            return Err(self.syntax_err(
7848                                format!("Expected scalar variable after %%, got {:?}", tok),
7849                                l,
7850                            ));
7851                        }
7852                    };
7853                    return Ok(Expr {
7854                        kind: ExprKind::Deref {
7855                            expr: Box::new(Expr {
7856                                kind: ExprKind::ScalarVar(n),
7857                                line,
7858                            }),
7859                            kind: Sigil::Hash,
7860                        },
7861                        line,
7862                    });
7863                }
7864                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
7865                // anonymous hashref inline, using `[...]` as the delimiter to avoid
7866                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
7867                // Real Perl errors on `%[...]` syntactically, so no compat risk.
7868                if matches!(self.peek(), Token::LBracket) {
7869                    self.advance();
7870                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7871                    self.expect(&Token::RBracket)?;
7872                    let href = Expr {
7873                        kind: ExprKind::HashRef(pairs),
7874                        line,
7875                    };
7876                    return Ok(Expr {
7877                        kind: ExprKind::Deref {
7878                            expr: Box::new(href),
7879                            kind: Sigil::Hash,
7880                        },
7881                        line,
7882                    });
7883                }
7884                self.expect(&Token::LBrace)?;
7885                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
7886                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
7887                // heuristic is famously unreliable — when the first non-whitespace
7888                // token is an ident/string followed by `=>`, treat the whole thing
7889                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
7890                let looks_like_pair = matches!(
7891                    self.peek(),
7892                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7893                ) && matches!(self.peek_at(1), Token::FatArrow);
7894                let inner = if looks_like_pair {
7895                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7896                    Expr {
7897                        kind: ExprKind::HashRef(pairs),
7898                        line,
7899                    }
7900                } else {
7901                    self.parse_expression()?
7902                };
7903                self.expect(&Token::RBrace)?;
7904                Ok(Expr {
7905                    kind: ExprKind::Deref {
7906                        expr: Box::new(inner),
7907                        kind: Sigil::Hash,
7908                    },
7909                    line,
7910                })
7911            }
7912            Token::ArrayAt => {
7913                self.advance();
7914                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
7915                if matches!(self.peek(), Token::LBrace) {
7916                    self.advance();
7917                    let inner = self.parse_expression()?;
7918                    self.expect(&Token::RBrace)?;
7919                    return Ok(Expr {
7920                        kind: ExprKind::Deref {
7921                            expr: Box::new(inner),
7922                            kind: Sigil::Array,
7923                        },
7924                        line,
7925                    });
7926                }
7927                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
7928                // anonymous arrayref inline. Real Perl rejects `@[...]` at
7929                // the parser level, so this extension has no compat risk.
7930                if matches!(self.peek(), Token::LBracket) {
7931                    self.advance();
7932                    let mut elems = Vec::new();
7933                    if !matches!(self.peek(), Token::RBracket) {
7934                        elems.push(self.parse_assign_expr()?);
7935                        while self.eat(&Token::Comma) {
7936                            if matches!(self.peek(), Token::RBracket) {
7937                                break;
7938                            }
7939                            elems.push(self.parse_assign_expr()?);
7940                        }
7941                    }
7942                    self.expect(&Token::RBracket)?;
7943                    let aref = Expr {
7944                        kind: ExprKind::ArrayRef(elems),
7945                        line,
7946                    };
7947                    return Ok(Expr {
7948                        kind: ExprKind::Deref {
7949                            expr: Box::new(aref),
7950                            kind: Sigil::Array,
7951                        },
7952                        line,
7953                    });
7954                }
7955                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
7956                let container = match self.peek().clone() {
7957                    Token::ScalarVar(n) => {
7958                        self.advance();
7959                        Expr {
7960                            kind: ExprKind::ScalarVar(n),
7961                            line,
7962                        }
7963                    }
7964                    _ => {
7965                        return Err(self.syntax_err(
7966                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
7967                            line,
7968                        ));
7969                    }
7970                };
7971                if matches!(self.peek(), Token::LBrace) {
7972                    self.advance();
7973                    let keys = self.parse_arg_list()?;
7974                    self.expect(&Token::RBrace)?;
7975                    return Ok(Expr {
7976                        kind: ExprKind::HashSliceDeref {
7977                            container: Box::new(container),
7978                            keys,
7979                        },
7980                        line,
7981                    });
7982                }
7983                Ok(Expr {
7984                    kind: ExprKind::Deref {
7985                        expr: Box::new(container),
7986                        kind: Sigil::Array,
7987                    },
7988                    line,
7989                })
7990            }
7991            Token::LParen => {
7992                self.advance();
7993                if matches!(self.peek(), Token::RParen) {
7994                    self.advance();
7995                    return Ok(Expr {
7996                        kind: ExprKind::List(vec![]),
7997                        line,
7998                    });
7999                }
8000                // Inside parens, pipe-forward is allowed even if we're in a
8001                // paren-less arg context. Save and restore no_pipe_forward_depth.
8002                let saved_no_pipe = self.no_pipe_forward_depth;
8003                self.no_pipe_forward_depth = 0;
8004                let expr = self.parse_expression();
8005                self.no_pipe_forward_depth = saved_no_pipe;
8006                let expr = expr?;
8007                self.expect(&Token::RParen)?;
8008                Ok(expr)
8009            }
8010            Token::LBracket => {
8011                self.advance();
8012                let elems = self.parse_arg_list()?;
8013                self.expect(&Token::RBracket)?;
8014                Ok(Expr {
8015                    kind: ExprKind::ArrayRef(elems),
8016                    line,
8017                })
8018            }
8019            Token::LBrace => {
8020                // Could be hash ref or block — disambiguate
8021                self.advance();
8022                // Try to parse as hash ref: { key => val, ... }
8023                let saved = self.pos;
8024                match self.try_parse_hash_ref() {
8025                    Ok(pairs) => Ok(Expr {
8026                        kind: ExprKind::HashRef(pairs),
8027                        line,
8028                    }),
8029                    Err(_) => {
8030                        self.pos = saved;
8031                        // Parse as block, wrap in code ref
8032                        let mut stmts = Vec::new();
8033                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8034                            if self.eat(&Token::Semicolon) {
8035                                continue;
8036                            }
8037                            stmts.push(self.parse_statement()?);
8038                        }
8039                        self.expect(&Token::RBrace)?;
8040                        Ok(Expr {
8041                            kind: ExprKind::CodeRef {
8042                                params: vec![],
8043                                body: stmts,
8044                            },
8045                            line,
8046                        })
8047                    }
8048                }
8049            }
8050            Token::Diamond => {
8051                self.advance();
8052                Ok(Expr {
8053                    kind: ExprKind::ReadLine(None),
8054                    line,
8055                })
8056            }
8057            Token::ReadLine(handle) => {
8058                self.advance();
8059                Ok(Expr {
8060                    kind: ExprKind::ReadLine(Some(handle)),
8061                    line,
8062                })
8063            }
8064
8065            // Named functions / builtins
8066            Token::ThreadArrow => {
8067                self.advance();
8068                self.parse_thread_macro(line, false)
8069            }
8070            Token::ThreadArrowLast => {
8071                self.advance();
8072                self.parse_thread_macro(line, true)
8073            }
8074            Token::Ident(ref name) => {
8075                let name = name.clone();
8076                // Handle s///
8077                if name.starts_with('\x00') {
8078                    self.advance();
8079                    let parts: Vec<&str> = name.split('\x00').collect();
8080                    if parts.len() >= 4 && parts[1] == "s" {
8081                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8082                        return Ok(Expr {
8083                            kind: ExprKind::Substitution {
8084                                expr: Box::new(Expr {
8085                                    kind: ExprKind::ScalarVar("_".into()),
8086                                    line,
8087                                }),
8088                                pattern: parts[2].to_string(),
8089                                replacement: parts[3].to_string(),
8090                                flags: parts.get(4).unwrap_or(&"").to_string(),
8091                                delim,
8092                            },
8093                            line,
8094                        });
8095                    }
8096                    if parts.len() >= 4 && parts[1] == "tr" {
8097                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8098                        return Ok(Expr {
8099                            kind: ExprKind::Transliterate {
8100                                expr: Box::new(Expr {
8101                                    kind: ExprKind::ScalarVar("_".into()),
8102                                    line,
8103                                }),
8104                                from: parts[2].to_string(),
8105                                to: parts[3].to_string(),
8106                                flags: parts.get(4).unwrap_or(&"").to_string(),
8107                                delim,
8108                            },
8109                            line,
8110                        });
8111                    }
8112                    return Err(self.syntax_err("Unexpected encoded token", line));
8113                }
8114                self.parse_named_expr(name)
8115            }
8116
8117            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
8118            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
8119            Token::Percent => {
8120                self.advance();
8121                match self.peek().clone() {
8122                    Token::Ident(name) => {
8123                        self.advance();
8124                        Ok(Expr {
8125                            kind: ExprKind::HashVar(name),
8126                            line,
8127                        })
8128                    }
8129                    Token::ScalarVar(n) => {
8130                        self.advance();
8131                        Ok(Expr {
8132                            kind: ExprKind::Deref {
8133                                expr: Box::new(Expr {
8134                                    kind: ExprKind::ScalarVar(n),
8135                                    line,
8136                                }),
8137                                kind: Sigil::Hash,
8138                            },
8139                            line,
8140                        })
8141                    }
8142                    Token::LBrace => {
8143                        self.advance();
8144                        let looks_like_pair = matches!(
8145                            self.peek(),
8146                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8147                        ) && matches!(self.peek_at(1), Token::FatArrow);
8148                        let inner = if looks_like_pair {
8149                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8150                            Expr {
8151                                kind: ExprKind::HashRef(pairs),
8152                                line,
8153                            }
8154                        } else {
8155                            self.parse_expression()?
8156                        };
8157                        self.expect(&Token::RBrace)?;
8158                        Ok(Expr {
8159                            kind: ExprKind::Deref {
8160                                expr: Box::new(inner),
8161                                kind: Sigil::Hash,
8162                            },
8163                            line,
8164                        })
8165                    }
8166                    Token::LBracket => {
8167                        self.advance();
8168                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8169                        self.expect(&Token::RBracket)?;
8170                        let href = Expr {
8171                            kind: ExprKind::HashRef(pairs),
8172                            line,
8173                        };
8174                        Ok(Expr {
8175                            kind: ExprKind::Deref {
8176                                expr: Box::new(href),
8177                                kind: Sigil::Hash,
8178                            },
8179                            line,
8180                        })
8181                    }
8182                    tok => Err(self.syntax_err(
8183                        format!(
8184                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
8185                            tok
8186                        ),
8187                        line,
8188                    )),
8189                }
8190            }
8191
8192            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
8193        }
8194    }
8195
8196    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
8197        let line = self.peek_line();
8198        self.advance(); // consume the ident
8199        while self.eat(&Token::PackageSep) {
8200            match self.advance() {
8201                (Token::Ident(part), _) => {
8202                    name = format!("{}::{}", name, part);
8203                }
8204                (tok, err_line) => {
8205                    return Err(self.syntax_err(
8206                        format!("Expected identifier after `::`, got {:?}", tok),
8207                        err_line,
8208                    ));
8209                }
8210            }
8211        }
8212
8213        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
8214        // before `=>` is treated as a string key, matching Perl 5 semantics.
8215        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
8216        if matches!(self.peek(), Token::FatArrow) {
8217            return Ok(Expr {
8218                kind: ExprKind::String(name),
8219                line,
8220            });
8221        }
8222
8223        if crate::compat_mode() {
8224            if let Some(ext) = Self::stryke_extension_name(&name) {
8225                if !self.declared_subs.contains(&name) {
8226                    return Err(self.syntax_err(
8227                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
8228                        line,
8229                    ));
8230                }
8231            }
8232        }
8233
8234        match name.as_str() {
8235            "__FILE__" => Ok(Expr {
8236                kind: ExprKind::MagicConst(MagicConstKind::File),
8237                line,
8238            }),
8239            "__LINE__" => Ok(Expr {
8240                kind: ExprKind::MagicConst(MagicConstKind::Line),
8241                line,
8242            }),
8243            "__SUB__" => Ok(Expr {
8244                kind: ExprKind::MagicConst(MagicConstKind::Sub),
8245                line,
8246            }),
8247            "stdin" => Ok(Expr {
8248                kind: ExprKind::FuncCall {
8249                    name: "stdin".into(),
8250                    args: vec![],
8251                },
8252                line,
8253            }),
8254            "range" => {
8255                let args = self.parse_builtin_args()?;
8256                Ok(Expr {
8257                    kind: ExprKind::FuncCall {
8258                        name: "range".into(),
8259                        args,
8260                    },
8261                    line,
8262                })
8263            }
8264            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
8265            "say" => {
8266                if !crate::compat_mode() {
8267                    return Err(self.syntax_err("stryke uses `p` instead of `say` (this is not Perl 5)", line));
8268                }
8269                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
8270            }
8271            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
8272            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
8273            "die" => {
8274                let args = self.parse_list_until_terminator()?;
8275                Ok(Expr {
8276                    kind: ExprKind::Die(args),
8277                    line,
8278                })
8279            }
8280            "warn" => {
8281                let args = self.parse_list_until_terminator()?;
8282                Ok(Expr {
8283                    kind: ExprKind::Warn(args),
8284                    line,
8285                })
8286            }
8287            // `croak` / `confess` — `Carp` builtins available without `use Carp`
8288            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
8289            // `die` — TODO: croak should report caller's file/line, confess
8290            // should append a full stack trace.
8291            "croak" | "confess" => {
8292                let args = self.parse_list_until_terminator()?;
8293                Ok(Expr {
8294                    kind: ExprKind::Die(args),
8295                    line,
8296                })
8297            }
8298            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
8299            "carp" | "cluck" => {
8300                let args = self.parse_list_until_terminator()?;
8301                Ok(Expr {
8302                    kind: ExprKind::Warn(args),
8303                    line,
8304                })
8305            }
8306            "chomp" => {
8307                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8308                    return Ok(e);
8309                }
8310                let a = self.parse_one_arg_or_default()?;
8311                Ok(Expr {
8312                    kind: ExprKind::Chomp(Box::new(a)),
8313                    line,
8314                })
8315            }
8316            "chop" => {
8317                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8318                    return Ok(e);
8319                }
8320                let a = self.parse_one_arg_or_default()?;
8321                Ok(Expr {
8322                    kind: ExprKind::Chop(Box::new(a)),
8323                    line,
8324                })
8325            }
8326            "length" => {
8327                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8328                    return Ok(e);
8329                }
8330                let a = self.parse_one_arg_or_default()?;
8331                Ok(Expr {
8332                    kind: ExprKind::Length(Box::new(a)),
8333                    line,
8334                })
8335            }
8336            "defined" => {
8337                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8338                    return Ok(e);
8339                }
8340                let a = self.parse_one_arg_or_default()?;
8341                Ok(Expr {
8342                    kind: ExprKind::Defined(Box::new(a)),
8343                    line,
8344                })
8345            }
8346            "ref" => {
8347                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8348                    return Ok(e);
8349                }
8350                let a = self.parse_one_arg_or_default()?;
8351                Ok(Expr {
8352                    kind: ExprKind::Ref(Box::new(a)),
8353                    line,
8354                })
8355            }
8356            "undef" => {
8357                // `undef $var` sets `$var` to undef — but a variable on a new line
8358                // is a separate statement (implicit semicolon), not an argument.
8359                if self.peek_line() == self.prev_line()
8360                    && matches!(
8361                        self.peek(),
8362                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
8363                    )
8364                {
8365                    let target = self.parse_primary()?;
8366                    return Ok(Expr {
8367                        kind: ExprKind::Assign {
8368                            target: Box::new(target),
8369                            value: Box::new(Expr {
8370                                kind: ExprKind::Undef,
8371                                line,
8372                            }),
8373                        },
8374                        line,
8375                    });
8376                }
8377                Ok(Expr {
8378                    kind: ExprKind::Undef,
8379                    line,
8380                })
8381            }
8382            "scalar" => {
8383                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8384                    return Ok(e);
8385                }
8386                let a = self.parse_one_arg_or_default()?;
8387                Ok(Expr {
8388                    kind: ExprKind::ScalarContext(Box::new(a)),
8389                    line,
8390                })
8391            }
8392            "abs" => {
8393                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8394                    return Ok(e);
8395                }
8396                let a = self.parse_one_arg_or_default()?;
8397                Ok(Expr {
8398                    kind: ExprKind::Abs(Box::new(a)),
8399                    line,
8400                })
8401            }
8402            // stryke unary numeric extensions — treat like `abs` so a bare
8403            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
8404            // call with implicit `$_` rather than falling through to the
8405            // generic `Bareword` arm (which stringifies to `"inc"`).
8406            "inc" | "dec" => {
8407                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8408                    return Ok(e);
8409                }
8410                let a = self.parse_one_arg_or_default()?;
8411                Ok(Expr {
8412                    kind: ExprKind::FuncCall {
8413                        name,
8414                        args: vec![a],
8415                    },
8416                    line,
8417                })
8418            }
8419            "int" => {
8420                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8421                    return Ok(e);
8422                }
8423                let a = self.parse_one_arg_or_default()?;
8424                Ok(Expr {
8425                    kind: ExprKind::Int(Box::new(a)),
8426                    line,
8427                })
8428            }
8429            "sqrt" => {
8430                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8431                    return Ok(e);
8432                }
8433                let a = self.parse_one_arg_or_default()?;
8434                Ok(Expr {
8435                    kind: ExprKind::Sqrt(Box::new(a)),
8436                    line,
8437                })
8438            }
8439            "sin" => {
8440                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8441                    return Ok(e);
8442                }
8443                let a = self.parse_one_arg_or_default()?;
8444                Ok(Expr {
8445                    kind: ExprKind::Sin(Box::new(a)),
8446                    line,
8447                })
8448            }
8449            "cos" => {
8450                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8451                    return Ok(e);
8452                }
8453                let a = self.parse_one_arg_or_default()?;
8454                Ok(Expr {
8455                    kind: ExprKind::Cos(Box::new(a)),
8456                    line,
8457                })
8458            }
8459            "atan2" => {
8460                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8461                    return Ok(e);
8462                }
8463                let args = self.parse_builtin_args()?;
8464                if args.len() != 2 {
8465                    return Err(self.syntax_err("atan2 requires two arguments", line));
8466                }
8467                Ok(Expr {
8468                    kind: ExprKind::Atan2 {
8469                        y: Box::new(args[0].clone()),
8470                        x: Box::new(args[1].clone()),
8471                    },
8472                    line,
8473                })
8474            }
8475            "exp" => {
8476                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8477                    return Ok(e);
8478                }
8479                let a = self.parse_one_arg_or_default()?;
8480                Ok(Expr {
8481                    kind: ExprKind::Exp(Box::new(a)),
8482                    line,
8483                })
8484            }
8485            "log" => {
8486                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8487                    return Ok(e);
8488                }
8489                let a = self.parse_one_arg_or_default()?;
8490                Ok(Expr {
8491                    kind: ExprKind::Log(Box::new(a)),
8492                    line,
8493                })
8494            }
8495            "input" => {
8496                let args = if matches!(
8497                    self.peek(),
8498                    Token::Semicolon
8499                        | Token::RBrace
8500                        | Token::RParen
8501                        | Token::Eof
8502                        | Token::Comma
8503                        | Token::PipeForward
8504                ) {
8505                    vec![]
8506                } else if matches!(self.peek(), Token::LParen) {
8507                    self.advance();
8508                    if matches!(self.peek(), Token::RParen) {
8509                        self.advance();
8510                        vec![]
8511                    } else {
8512                        let a = self.parse_expression()?;
8513                        self.expect(&Token::RParen)?;
8514                        vec![a]
8515                    }
8516                } else {
8517                    let a = self.parse_one_arg()?;
8518                    vec![a]
8519                };
8520                Ok(Expr {
8521                    kind: ExprKind::FuncCall {
8522                        name: "input".to_string(),
8523                        args,
8524                    },
8525                    line,
8526                })
8527            }
8528            "rand" => {
8529                if matches!(
8530                    self.peek(),
8531                    Token::Semicolon
8532                        | Token::RBrace
8533                        | Token::RParen
8534                        | Token::Eof
8535                        | Token::Comma
8536                        | Token::PipeForward
8537                ) {
8538                    Ok(Expr {
8539                        kind: ExprKind::Rand(None),
8540                        line,
8541                    })
8542                } else if matches!(self.peek(), Token::LParen) {
8543                    self.advance();
8544                    if matches!(self.peek(), Token::RParen) {
8545                        self.advance();
8546                        Ok(Expr {
8547                            kind: ExprKind::Rand(None),
8548                            line,
8549                        })
8550                    } else {
8551                        let a = self.parse_expression()?;
8552                        self.expect(&Token::RParen)?;
8553                        Ok(Expr {
8554                            kind: ExprKind::Rand(Some(Box::new(a))),
8555                            line,
8556                        })
8557                    }
8558                } else {
8559                    let a = self.parse_one_arg()?;
8560                    Ok(Expr {
8561                        kind: ExprKind::Rand(Some(Box::new(a))),
8562                        line,
8563                    })
8564                }
8565            }
8566            "srand" => {
8567                if matches!(
8568                    self.peek(),
8569                    Token::Semicolon
8570                        | Token::RBrace
8571                        | Token::RParen
8572                        | Token::Eof
8573                        | Token::Comma
8574                        | Token::PipeForward
8575                ) {
8576                    Ok(Expr {
8577                        kind: ExprKind::Srand(None),
8578                        line,
8579                    })
8580                } else if matches!(self.peek(), Token::LParen) {
8581                    self.advance();
8582                    if matches!(self.peek(), Token::RParen) {
8583                        self.advance();
8584                        Ok(Expr {
8585                            kind: ExprKind::Srand(None),
8586                            line,
8587                        })
8588                    } else {
8589                        let a = self.parse_expression()?;
8590                        self.expect(&Token::RParen)?;
8591                        Ok(Expr {
8592                            kind: ExprKind::Srand(Some(Box::new(a))),
8593                            line,
8594                        })
8595                    }
8596                } else {
8597                    let a = self.parse_one_arg()?;
8598                    Ok(Expr {
8599                        kind: ExprKind::Srand(Some(Box::new(a))),
8600                        line,
8601                    })
8602                }
8603            }
8604            "hex" => {
8605                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8606                    return Ok(e);
8607                }
8608                let a = self.parse_one_arg_or_default()?;
8609                Ok(Expr {
8610                    kind: ExprKind::Hex(Box::new(a)),
8611                    line,
8612                })
8613            }
8614            "oct" => {
8615                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8616                    return Ok(e);
8617                }
8618                let a = self.parse_one_arg_or_default()?;
8619                Ok(Expr {
8620                    kind: ExprKind::Oct(Box::new(a)),
8621                    line,
8622                })
8623            }
8624            "chr" => {
8625                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8626                    return Ok(e);
8627                }
8628                let a = self.parse_one_arg_or_default()?;
8629                Ok(Expr {
8630                    kind: ExprKind::Chr(Box::new(a)),
8631                    line,
8632                })
8633            }
8634            "ord" => {
8635                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8636                    return Ok(e);
8637                }
8638                let a = self.parse_one_arg_or_default()?;
8639                Ok(Expr {
8640                    kind: ExprKind::Ord(Box::new(a)),
8641                    line,
8642                })
8643            }
8644            "lc" => {
8645                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8646                    return Ok(e);
8647                }
8648                let a = self.parse_one_arg_or_default()?;
8649                Ok(Expr {
8650                    kind: ExprKind::Lc(Box::new(a)),
8651                    line,
8652                })
8653            }
8654            "uc" => {
8655                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8656                    return Ok(e);
8657                }
8658                let a = self.parse_one_arg_or_default()?;
8659                Ok(Expr {
8660                    kind: ExprKind::Uc(Box::new(a)),
8661                    line,
8662                })
8663            }
8664            "lcfirst" => {
8665                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8666                    return Ok(e);
8667                }
8668                let a = self.parse_one_arg_or_default()?;
8669                Ok(Expr {
8670                    kind: ExprKind::Lcfirst(Box::new(a)),
8671                    line,
8672                })
8673            }
8674            "ucfirst" => {
8675                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8676                    return Ok(e);
8677                }
8678                let a = self.parse_one_arg_or_default()?;
8679                Ok(Expr {
8680                    kind: ExprKind::Ucfirst(Box::new(a)),
8681                    line,
8682                })
8683            }
8684            "fc" => {
8685                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8686                    return Ok(e);
8687                }
8688                let a = self.parse_one_arg_or_default()?;
8689                Ok(Expr {
8690                    kind: ExprKind::Fc(Box::new(a)),
8691                    line,
8692                })
8693            }
8694            "crypt" => {
8695                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8696                    return Ok(e);
8697                }
8698                let args = self.parse_builtin_args()?;
8699                if args.len() != 2 {
8700                    return Err(self.syntax_err("crypt requires two arguments", line));
8701                }
8702                Ok(Expr {
8703                    kind: ExprKind::Crypt {
8704                        plaintext: Box::new(args[0].clone()),
8705                        salt: Box::new(args[1].clone()),
8706                    },
8707                    line,
8708                })
8709            }
8710            "pos" => {
8711                if matches!(
8712                    self.peek(),
8713                    Token::Semicolon
8714                        | Token::RBrace
8715                        | Token::RParen
8716                        | Token::Eof
8717                        | Token::Comma
8718                        | Token::PipeForward
8719                ) {
8720                    Ok(Expr {
8721                        kind: ExprKind::Pos(None),
8722                        line,
8723                    })
8724                } else if matches!(self.peek(), Token::Assign) {
8725                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
8726                    self.advance();
8727                    let rhs = self.parse_assign_expr()?;
8728                    Ok(Expr {
8729                        kind: ExprKind::Assign {
8730                            target: Box::new(Expr {
8731                                kind: ExprKind::Pos(Some(Box::new(Expr {
8732                                    kind: ExprKind::ScalarVar("_".into()),
8733                                    line,
8734                                }))),
8735                                line,
8736                            }),
8737                            value: Box::new(rhs),
8738                        },
8739                        line,
8740                    })
8741                } else if matches!(self.peek(), Token::LParen) {
8742                    self.advance();
8743                    if matches!(self.peek(), Token::RParen) {
8744                        self.advance();
8745                        Ok(Expr {
8746                            kind: ExprKind::Pos(None),
8747                            line,
8748                        })
8749                    } else {
8750                        let a = self.parse_expression()?;
8751                        self.expect(&Token::RParen)?;
8752                        Ok(Expr {
8753                            kind: ExprKind::Pos(Some(Box::new(a))),
8754                            line,
8755                        })
8756                    }
8757                } else {
8758                    let saved = self.pos;
8759                    let subj = self.parse_unary()?;
8760                    if matches!(self.peek(), Token::Assign) {
8761                        self.advance();
8762                        let rhs = self.parse_assign_expr()?;
8763                        Ok(Expr {
8764                            kind: ExprKind::Assign {
8765                                target: Box::new(Expr {
8766                                    kind: ExprKind::Pos(Some(Box::new(subj))),
8767                                    line,
8768                                }),
8769                                value: Box::new(rhs),
8770                            },
8771                            line,
8772                        })
8773                    } else {
8774                        self.pos = saved;
8775                        let a = self.parse_one_arg()?;
8776                        Ok(Expr {
8777                            kind: ExprKind::Pos(Some(Box::new(a))),
8778                            line,
8779                        })
8780                    }
8781                }
8782            }
8783            "study" => {
8784                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8785                    return Ok(e);
8786                }
8787                let a = self.parse_one_arg_or_default()?;
8788                Ok(Expr {
8789                    kind: ExprKind::Study(Box::new(a)),
8790                    line,
8791                })
8792            }
8793            "push" => {
8794                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8795                    return Ok(e);
8796                }
8797                let args = self.parse_builtin_args()?;
8798                let (first, rest) = args
8799                    .split_first()
8800                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
8801                Ok(Expr {
8802                    kind: ExprKind::Push {
8803                        array: Box::new(first.clone()),
8804                        values: rest.to_vec(),
8805                    },
8806                    line,
8807                })
8808            }
8809            "pop" => {
8810                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8811                    return Ok(e);
8812                }
8813                let a = self.parse_one_arg_or_argv()?;
8814                Ok(Expr {
8815                    kind: ExprKind::Pop(Box::new(a)),
8816                    line,
8817                })
8818            }
8819            "shift" => {
8820                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8821                    return Ok(e);
8822                }
8823                let a = self.parse_one_arg_or_argv()?;
8824                Ok(Expr {
8825                    kind: ExprKind::Shift(Box::new(a)),
8826                    line,
8827                })
8828            }
8829            "unshift" => {
8830                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8831                    return Ok(e);
8832                }
8833                let args = self.parse_builtin_args()?;
8834                let (first, rest) = args
8835                    .split_first()
8836                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
8837                Ok(Expr {
8838                    kind: ExprKind::Unshift {
8839                        array: Box::new(first.clone()),
8840                        values: rest.to_vec(),
8841                    },
8842                    line,
8843                })
8844            }
8845            "splice" => {
8846                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8847                    return Ok(e);
8848                }
8849                let args = self.parse_builtin_args()?;
8850                let mut iter = args.into_iter();
8851                let array = Box::new(
8852                    iter.next()
8853                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
8854                );
8855                let offset = iter.next().map(Box::new);
8856                let length = iter.next().map(Box::new);
8857                let replacement: Vec<Expr> = iter.collect();
8858                Ok(Expr {
8859                    kind: ExprKind::Splice {
8860                        array,
8861                        offset,
8862                        length,
8863                        replacement,
8864                    },
8865                    line,
8866                })
8867            }
8868            "delete" => {
8869                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8870                    return Ok(e);
8871                }
8872                let a = self.parse_postfix()?;
8873                Ok(Expr {
8874                    kind: ExprKind::Delete(Box::new(a)),
8875                    line,
8876                })
8877            }
8878            "exists" => {
8879                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8880                    return Ok(e);
8881                }
8882                let a = self.parse_postfix()?;
8883                Ok(Expr {
8884                    kind: ExprKind::Exists(Box::new(a)),
8885                    line,
8886                })
8887            }
8888            "keys" => {
8889                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8890                    return Ok(e);
8891                }
8892                let a = self.parse_one_arg_or_default()?;
8893                Ok(Expr {
8894                    kind: ExprKind::Keys(Box::new(a)),
8895                    line,
8896                })
8897            }
8898            "values" => {
8899                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8900                    return Ok(e);
8901                }
8902                let a = self.parse_one_arg_or_default()?;
8903                Ok(Expr {
8904                    kind: ExprKind::Values(Box::new(a)),
8905                    line,
8906                })
8907            }
8908            "each" => {
8909                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8910                    return Ok(e);
8911                }
8912                let a = self.parse_one_arg_or_default()?;
8913                Ok(Expr {
8914                    kind: ExprKind::Each(Box::new(a)),
8915                    line,
8916                })
8917            }
8918            "fore" | "e" | "ep" => {
8919                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
8920                if matches!(self.peek(), Token::LBrace) {
8921                    let (block, list) = self.parse_block_list()?;
8922                    Ok(Expr {
8923                        kind: ExprKind::ForEachExpr {
8924                            block,
8925                            list: Box::new(list),
8926                        },
8927                        line,
8928                    })
8929                } else if self.in_pipe_rhs() {
8930                    // `|> ep` — bare ep at end of pipe: default to `say $_`
8931                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
8932                    let is_terminal = matches!(
8933                        self.peek(),
8934                        Token::Semicolon
8935                            | Token::RParen
8936                            | Token::Eof
8937                            | Token::PipeForward
8938                            | Token::RBrace
8939                    );
8940                    let block = if name == "ep" && is_terminal {
8941                        vec![Statement {
8942                            label: None,
8943                            kind: StmtKind::Expression(Expr {
8944                                kind: ExprKind::Say {
8945                                    handle: None,
8946                                    args: vec![Expr {
8947                                        kind: ExprKind::ScalarVar("_".into()),
8948                                        line,
8949                                    }],
8950                                },
8951                                line,
8952                            }),
8953                            line,
8954                        }]
8955                    } else {
8956                        let expr = self.parse_assign_expr_stop_at_pipe()?;
8957                        let expr = Self::lift_bareword_to_topic_call(expr);
8958                        vec![Statement {
8959                            label: None,
8960                            kind: StmtKind::Expression(expr),
8961                            line,
8962                        }]
8963                    };
8964                    let list = self.pipe_placeholder_list(line);
8965                    Ok(Expr {
8966                        kind: ExprKind::ForEachExpr {
8967                            block,
8968                            list: Box::new(list),
8969                        },
8970                        line,
8971                    })
8972                } else {
8973                    // `fore EXPR, LIST` — comma form
8974                    let expr = self.parse_assign_expr()?;
8975                    let expr = Self::lift_bareword_to_topic_call(expr);
8976                    self.expect(&Token::Comma)?;
8977                    let list_parts = self.parse_list_until_terminator()?;
8978                    let list_expr = if list_parts.len() == 1 {
8979                        list_parts.into_iter().next().unwrap()
8980                    } else {
8981                        Expr {
8982                            kind: ExprKind::List(list_parts),
8983                            line,
8984                        }
8985                    };
8986                    let block = vec![Statement {
8987                        label: None,
8988                        kind: StmtKind::Expression(expr),
8989                        line,
8990                    }];
8991                    Ok(Expr {
8992                        kind: ExprKind::ForEachExpr {
8993                            block,
8994                            list: Box::new(list_expr),
8995                        },
8996                        line,
8997                    })
8998                }
8999            }
9000            "rev" => {
9001                // `rev` — context-aware reverse: string in scalar, list in list context.
9002                // Defaults to $_ when no argument given.
9003                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
9004                // RBrace means we're inside a block like `map { rev }` - use $_ default.
9005                let a = if self.in_pipe_rhs()
9006                    && matches!(
9007                        self.peek(),
9008                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
9009                    ) {
9010                    self.pipe_placeholder_list(line)
9011                } else {
9012                    self.parse_one_arg_or_default()?
9013                };
9014                Ok(Expr {
9015                    kind: ExprKind::Rev(Box::new(a)),
9016                    line,
9017                })
9018            }
9019            "reverse" => {
9020                if !crate::compat_mode() {
9021                    return Err(
9022                        self.syntax_err("stryke uses `rev` instead of `reverse` (this is not Perl 5)", line)
9023                    );
9024                }
9025                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9026                let a = if self.in_pipe_rhs()
9027                    && matches!(
9028                        self.peek(),
9029                        Token::Semicolon
9030                            | Token::RBrace
9031                            | Token::RParen
9032                            | Token::Eof
9033                            | Token::PipeForward
9034                    ) {
9035                    self.pipe_placeholder_list(line)
9036                } else {
9037                    self.parse_one_arg()?
9038                };
9039                Ok(Expr {
9040                    kind: ExprKind::ReverseExpr(Box::new(a)),
9041                    line,
9042                })
9043            }
9044            "reversed" | "rv" => {
9045                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9046                let a = if self.in_pipe_rhs()
9047                    && matches!(
9048                        self.peek(),
9049                        Token::Semicolon
9050                            | Token::RBrace
9051                            | Token::RParen
9052                            | Token::Eof
9053                            | Token::PipeForward
9054                    ) {
9055                    self.pipe_placeholder_list(line)
9056                } else {
9057                    self.parse_one_arg()?
9058                };
9059                Ok(Expr {
9060                    kind: ExprKind::Rev(Box::new(a)),
9061                    line,
9062                })
9063            }
9064            "join" => {
9065                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9066                    return Ok(e);
9067                }
9068                let args = self.parse_builtin_args()?;
9069                if args.is_empty() {
9070                    return Err(self.syntax_err("join requires separator and list", line));
9071                }
9072                // `@list |> join(",")` — list slot is filled by the piped LHS.
9073                if args.len() < 2 && !self.in_pipe_rhs() {
9074                    return Err(self.syntax_err("join requires separator and list", line));
9075                }
9076                Ok(Expr {
9077                    kind: ExprKind::JoinExpr {
9078                        separator: Box::new(args[0].clone()),
9079                        list: Box::new(Expr {
9080                            kind: ExprKind::List(args[1..].to_vec()),
9081                            line,
9082                        }),
9083                    },
9084                    line,
9085                })
9086            }
9087            "split" => {
9088                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9089                    return Ok(e);
9090                }
9091                let args = self.parse_builtin_args()?;
9092                let pattern = args.first().cloned().unwrap_or(Expr {
9093                    kind: ExprKind::String(" ".into()),
9094                    line,
9095                });
9096                let string = args.get(1).cloned().unwrap_or(Expr {
9097                    kind: ExprKind::ScalarVar("_".into()),
9098                    line,
9099                });
9100                let limit = args.get(2).cloned().map(Box::new);
9101                Ok(Expr {
9102                    kind: ExprKind::SplitExpr {
9103                        pattern: Box::new(pattern),
9104                        string: Box::new(string),
9105                        limit,
9106                    },
9107                    line,
9108                })
9109            }
9110            "substr" => {
9111                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9112                    return Ok(e);
9113                }
9114                let args = self.parse_builtin_args()?;
9115                Ok(Expr {
9116                    kind: ExprKind::Substr {
9117                        string: Box::new(args[0].clone()),
9118                        offset: Box::new(args[1].clone()),
9119                        length: args.get(2).cloned().map(Box::new),
9120                        replacement: args.get(3).cloned().map(Box::new),
9121                    },
9122                    line,
9123                })
9124            }
9125            "index" => {
9126                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9127                    return Ok(e);
9128                }
9129                let args = self.parse_builtin_args()?;
9130                Ok(Expr {
9131                    kind: ExprKind::Index {
9132                        string: Box::new(args[0].clone()),
9133                        substr: Box::new(args[1].clone()),
9134                        position: args.get(2).cloned().map(Box::new),
9135                    },
9136                    line,
9137                })
9138            }
9139            "rindex" => {
9140                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9141                    return Ok(e);
9142                }
9143                let args = self.parse_builtin_args()?;
9144                Ok(Expr {
9145                    kind: ExprKind::Rindex {
9146                        string: Box::new(args[0].clone()),
9147                        substr: Box::new(args[1].clone()),
9148                        position: args.get(2).cloned().map(Box::new),
9149                    },
9150                    line,
9151                })
9152            }
9153            "sprintf" => {
9154                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9155                    return Ok(e);
9156                }
9157                let args = self.parse_builtin_args()?;
9158                let (first, rest) = args
9159                    .split_first()
9160                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
9161                Ok(Expr {
9162                    kind: ExprKind::Sprintf {
9163                        format: Box::new(first.clone()),
9164                        args: rest.to_vec(),
9165                    },
9166                    line,
9167                })
9168            }
9169            "map" | "flat_map" | "maps" | "flat_maps" => {
9170                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
9171                let stream = matches!(name.as_str(), "maps" | "flat_maps");
9172                if matches!(self.peek(), Token::LBrace) {
9173                    let (block, list) = self.parse_block_list()?;
9174                    Ok(Expr {
9175                        kind: ExprKind::MapExpr {
9176                            block,
9177                            list: Box::new(list),
9178                            flatten_array_refs,
9179                            stream,
9180                        },
9181                        line,
9182                    })
9183                } else {
9184                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9185                    // Lift bareword to FuncCall($_) so `map sha512, @list`
9186                    // calls sha512($_) for each element instead of stringifying.
9187                    let expr = Self::lift_bareword_to_topic_call(expr);
9188                    let list_expr = if self.in_pipe_rhs()
9189                        && matches!(
9190                            self.peek(),
9191                            Token::Semicolon
9192                                | Token::RBrace
9193                                | Token::RParen
9194                                | Token::Eof
9195                                | Token::PipeForward
9196                        ) {
9197                        self.pipe_placeholder_list(line)
9198                    } else {
9199                        self.expect(&Token::Comma)?;
9200                        let list_parts = self.parse_list_until_terminator()?;
9201                        if list_parts.len() == 1 {
9202                            list_parts.into_iter().next().unwrap()
9203                        } else {
9204                            Expr {
9205                                kind: ExprKind::List(list_parts),
9206                                line,
9207                            }
9208                        }
9209                    };
9210                    Ok(Expr {
9211                        kind: ExprKind::MapExprComma {
9212                            expr: Box::new(expr),
9213                            list: Box::new(list_expr),
9214                            flatten_array_refs,
9215                            stream,
9216                        },
9217                        line,
9218                    })
9219                }
9220            }
9221            "cond" => {
9222                if crate::compat_mode() {
9223                    return Err(self.syntax_err(
9224                        "`cond` is a stryke extension (disabled by --compat)",
9225                        line,
9226                    ));
9227                }
9228                self.parse_cond_expr(line)
9229            }
9230            "match" => {
9231                if crate::compat_mode() {
9232                    return Err(self.syntax_err(
9233                        "algebraic `match` is a stryke extension (disabled by --compat)",
9234                        line,
9235                    ));
9236                }
9237                self.parse_algebraic_match_expr(line)
9238            }
9239            "grep" | "greps" | "filter" | "fi" | "find_all" => {
9240                let keyword = match name.as_str() {
9241                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
9242                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
9243                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
9244                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
9245                    _ => unreachable!(),
9246                };
9247                if matches!(self.peek(), Token::LBrace) {
9248                    let (block, list) = self.parse_block_list()?;
9249                    Ok(Expr {
9250                        kind: ExprKind::GrepExpr {
9251                            block,
9252                            list: Box::new(list),
9253                            keyword,
9254                        },
9255                        line,
9256                    })
9257                } else {
9258                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9259                    if self.in_pipe_rhs()
9260                        && matches!(
9261                            self.peek(),
9262                            Token::Semicolon
9263                                | Token::RBrace
9264                                | Token::RParen
9265                                | Token::Eof
9266                                | Token::PipeForward
9267                        )
9268                    {
9269                        // Pipe-RHS blockless form: `|> grep EXPR`
9270                        // For literals, desugar to `$_ eq/== EXPR` so
9271                        // `|> filter 't'` keeps only elements equal to 't'.
9272                        // For regexes, desugar to `$_ =~ EXPR`.
9273                        let list = self.pipe_placeholder_list(line);
9274                        let topic = Expr {
9275                            kind: ExprKind::ScalarVar("_".into()),
9276                            line,
9277                        };
9278                        let test = match &expr.kind {
9279                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
9280                                kind: ExprKind::BinOp {
9281                                    op: BinOp::NumEq,
9282                                    left: Box::new(topic),
9283                                    right: Box::new(expr),
9284                                },
9285                                line,
9286                            },
9287                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
9288                                kind: ExprKind::BinOp {
9289                                    op: BinOp::StrEq,
9290                                    left: Box::new(topic),
9291                                    right: Box::new(expr),
9292                                },
9293                                line,
9294                            },
9295                            ExprKind::Regex { .. } => Expr {
9296                                kind: ExprKind::BinOp {
9297                                    op: BinOp::BindMatch,
9298                                    left: Box::new(topic),
9299                                    right: Box::new(expr),
9300                                },
9301                                line,
9302                            },
9303                            _ => {
9304                                // Non-literal (e.g. `defined`): lift bareword to call
9305                                Self::lift_bareword_to_topic_call(expr)
9306                            }
9307                        };
9308                        let block = vec![Statement {
9309                            label: None,
9310                            kind: StmtKind::Expression(test),
9311                            line,
9312                        }];
9313                        Ok(Expr {
9314                            kind: ExprKind::GrepExpr {
9315                                block,
9316                                list: Box::new(list),
9317                                keyword,
9318                            },
9319                            line,
9320                        })
9321                    } else {
9322                        let expr = Self::lift_bareword_to_topic_call(expr);
9323                        self.expect(&Token::Comma)?;
9324                        let list_parts = self.parse_list_until_terminator()?;
9325                        let list_expr = if list_parts.len() == 1 {
9326                            list_parts.into_iter().next().unwrap()
9327                        } else {
9328                            Expr {
9329                                kind: ExprKind::List(list_parts),
9330                                line,
9331                            }
9332                        };
9333                        Ok(Expr {
9334                            kind: ExprKind::GrepExprComma {
9335                                expr: Box::new(expr),
9336                                list: Box::new(list_expr),
9337                                keyword,
9338                            },
9339                            line,
9340                        })
9341                    }
9342                }
9343            }
9344            "sort" => {
9345                use crate::ast::SortComparator;
9346                if matches!(self.peek(), Token::LBrace) {
9347                    let block = self.parse_block()?;
9348                    let _ = self.eat(&Token::Comma);
9349                    let list = if self.in_pipe_rhs()
9350                        && matches!(
9351                            self.peek(),
9352                            Token::Semicolon
9353                                | Token::RBrace
9354                                | Token::RParen
9355                                | Token::Eof
9356                                | Token::PipeForward
9357                        ) {
9358                        self.pipe_placeholder_list(line)
9359                    } else {
9360                        self.parse_expression()?
9361                    };
9362                    Ok(Expr {
9363                        kind: ExprKind::SortExpr {
9364                            cmp: Some(SortComparator::Block(block)),
9365                            list: Box::new(list),
9366                        },
9367                        line,
9368                    })
9369                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
9370                    // Blockless comparator: `sort $a <=> $b, @list`
9371                    let block = self.parse_block_or_bareword_cmp_block()?;
9372                    let _ = self.eat(&Token::Comma);
9373                    let list = if self.in_pipe_rhs()
9374                        && matches!(
9375                            self.peek(),
9376                            Token::Semicolon
9377                                | Token::RBrace
9378                                | Token::RParen
9379                                | Token::Eof
9380                                | Token::PipeForward
9381                        ) {
9382                        self.pipe_placeholder_list(line)
9383                    } else {
9384                        self.parse_expression()?
9385                    };
9386                    Ok(Expr {
9387                        kind: ExprKind::SortExpr {
9388                            cmp: Some(SortComparator::Block(block)),
9389                            list: Box::new(list),
9390                        },
9391                        line,
9392                    })
9393                } else if matches!(self.peek(), Token::ScalarVar(_)) {
9394                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized
9395                    self.suppress_indirect_paren_call =
9396                        self.suppress_indirect_paren_call.saturating_add(1);
9397                    let code = self.parse_assign_expr()?;
9398                    self.suppress_indirect_paren_call =
9399                        self.suppress_indirect_paren_call.saturating_sub(1);
9400                    let list = if matches!(self.peek(), Token::LParen) {
9401                        self.advance();
9402                        let e = self.parse_expression()?;
9403                        self.expect(&Token::RParen)?;
9404                        e
9405                    } else {
9406                        self.parse_expression()?
9407                    };
9408                    Ok(Expr {
9409                        kind: ExprKind::SortExpr {
9410                            cmp: Some(SortComparator::Code(Box::new(code))),
9411                            list: Box::new(list),
9412                        },
9413                        line,
9414                    })
9415                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9416                {
9417                    // Blockless comparator via bare sub name: `sort my_cmp @list`
9418                    let block = self.parse_block_or_bareword_cmp_block()?;
9419                    let _ = self.eat(&Token::Comma);
9420                    let list = if self.in_pipe_rhs()
9421                        && matches!(
9422                            self.peek(),
9423                            Token::Semicolon
9424                                | Token::RBrace
9425                                | Token::RParen
9426                                | Token::Eof
9427                                | Token::PipeForward
9428                        ) {
9429                        self.pipe_placeholder_list(line)
9430                    } else {
9431                        self.parse_expression()?
9432                    };
9433                    Ok(Expr {
9434                        kind: ExprKind::SortExpr {
9435                            cmp: Some(SortComparator::Block(block)),
9436                            list: Box::new(list),
9437                        },
9438                        line,
9439                    })
9440                } else {
9441                    // Bare `sort` with no comparator and no list: only allowed
9442                    // as the RHS of `|>`, where the list comes from the LHS.
9443                    let list = if self.in_pipe_rhs()
9444                        && matches!(
9445                            self.peek(),
9446                            Token::Semicolon
9447                                | Token::RBrace
9448                                | Token::RParen
9449                                | Token::Eof
9450                                | Token::PipeForward
9451                        ) {
9452                        self.pipe_placeholder_list(line)
9453                    } else {
9454                        self.parse_expression()?
9455                    };
9456                    Ok(Expr {
9457                        kind: ExprKind::SortExpr {
9458                            cmp: None,
9459                            list: Box::new(list),
9460                        },
9461                        line,
9462                    })
9463                }
9464            }
9465            "reduce" | "fold" | "inject" => {
9466                let (block, list) = self.parse_block_list()?;
9467                Ok(Expr {
9468                    kind: ExprKind::ReduceExpr {
9469                        block,
9470                        list: Box::new(list),
9471                    },
9472                    line,
9473                })
9474            }
9475            // Parallel extensions
9476            "pmap" => {
9477                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9478                Ok(Expr {
9479                    kind: ExprKind::PMapExpr {
9480                        block,
9481                        list: Box::new(list),
9482                        progress: progress.map(Box::new),
9483                        flat_outputs: false,
9484                        on_cluster: None,
9485                        stream: false,
9486                    },
9487                    line,
9488                })
9489            }
9490            "pmap_on" => {
9491                let (cluster, block, list, progress) =
9492                    self.parse_cluster_block_then_list_optional_progress()?;
9493                Ok(Expr {
9494                    kind: ExprKind::PMapExpr {
9495                        block,
9496                        list: Box::new(list),
9497                        progress: progress.map(Box::new),
9498                        flat_outputs: false,
9499                        on_cluster: Some(Box::new(cluster)),
9500                        stream: false,
9501                    },
9502                    line,
9503                })
9504            }
9505            "pflat_map" => {
9506                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9507                Ok(Expr {
9508                    kind: ExprKind::PMapExpr {
9509                        block,
9510                        list: Box::new(list),
9511                        progress: progress.map(Box::new),
9512                        flat_outputs: true,
9513                        on_cluster: None,
9514                        stream: false,
9515                    },
9516                    line,
9517                })
9518            }
9519            "pflat_map_on" => {
9520                let (cluster, block, list, progress) =
9521                    self.parse_cluster_block_then_list_optional_progress()?;
9522                Ok(Expr {
9523                    kind: ExprKind::PMapExpr {
9524                        block,
9525                        list: Box::new(list),
9526                        progress: progress.map(Box::new),
9527                        flat_outputs: true,
9528                        on_cluster: Some(Box::new(cluster)),
9529                        stream: false,
9530                    },
9531                    line,
9532                })
9533            }
9534            "pmaps" => {
9535                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9536                Ok(Expr {
9537                    kind: ExprKind::PMapExpr {
9538                        block,
9539                        list: Box::new(list),
9540                        progress: progress.map(Box::new),
9541                        flat_outputs: false,
9542                        on_cluster: None,
9543                        stream: true,
9544                    },
9545                    line,
9546                })
9547            }
9548            "pflat_maps" => {
9549                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9550                Ok(Expr {
9551                    kind: ExprKind::PMapExpr {
9552                        block,
9553                        list: Box::new(list),
9554                        progress: progress.map(Box::new),
9555                        flat_outputs: true,
9556                        on_cluster: None,
9557                        stream: true,
9558                    },
9559                    line,
9560                })
9561            }
9562            "pgreps" => {
9563                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9564                Ok(Expr {
9565                    kind: ExprKind::PGrepExpr {
9566                        block,
9567                        list: Box::new(list),
9568                        progress: progress.map(Box::new),
9569                        stream: true,
9570                    },
9571                    line,
9572                })
9573            }
9574            "pmap_chunked" => {
9575                let chunk_size = self.parse_assign_expr()?;
9576                let block = self.parse_block_or_bareword_block()?;
9577                self.eat(&Token::Comma);
9578                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9579                Ok(Expr {
9580                    kind: ExprKind::PMapChunkedExpr {
9581                        chunk_size: Box::new(chunk_size),
9582                        block,
9583                        list: Box::new(list),
9584                        progress: progress.map(Box::new),
9585                    },
9586                    line,
9587                })
9588            }
9589            "pgrep" => {
9590                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9591                Ok(Expr {
9592                    kind: ExprKind::PGrepExpr {
9593                        block,
9594                        list: Box::new(list),
9595                        progress: progress.map(Box::new),
9596                        stream: false,
9597                    },
9598                    line,
9599                })
9600            }
9601            "pfor" => {
9602                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9603                Ok(Expr {
9604                    kind: ExprKind::PForExpr {
9605                        block,
9606                        list: Box::new(list),
9607                        progress: progress.map(Box::new),
9608                    },
9609                    line,
9610                })
9611            }
9612            "par_lines" | "par_walk" => {
9613                let args = self.parse_builtin_args()?;
9614                if args.len() < 2 {
9615                    return Err(
9616                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9617                    );
9618                }
9619
9620                if name == "par_lines" {
9621                    Ok(Expr {
9622                        kind: ExprKind::ParLinesExpr {
9623                            path: Box::new(args[0].clone()),
9624                            callback: Box::new(args[1].clone()),
9625                            progress: None,
9626                        },
9627                        line,
9628                    })
9629                } else {
9630                    Ok(Expr {
9631                        kind: ExprKind::ParWalkExpr {
9632                            path: Box::new(args[0].clone()),
9633                            callback: Box::new(args[1].clone()),
9634                            progress: None,
9635                        },
9636                        line,
9637                    })
9638                }
9639            }
9640            "pwatch" | "watch" => {
9641                let args = self.parse_builtin_args()?;
9642                if args.len() < 2 {
9643                    return Err(
9644                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9645                    );
9646                }
9647                Ok(Expr {
9648                    kind: ExprKind::PwatchExpr {
9649                        path: Box::new(args[0].clone()),
9650                        callback: Box::new(args[1].clone()),
9651                    },
9652                    line,
9653                })
9654            }
9655            "fan" => {
9656                // fan { BLOCK }            — no count, block body
9657                // fan COUNT { BLOCK }      — count + block body
9658                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
9659                // fan COUNT EXPR;          — count + blockless body
9660                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
9661                let (count, block) = self.parse_fan_count_and_block(line)?;
9662                let progress = self.parse_fan_optional_progress("fan")?;
9663                Ok(Expr {
9664                    kind: ExprKind::FanExpr {
9665                        count,
9666                        block,
9667                        progress,
9668                        capture: false,
9669                    },
9670                    line,
9671                })
9672            }
9673            "fan_cap" => {
9674                let (count, block) = self.parse_fan_count_and_block(line)?;
9675                let progress = self.parse_fan_optional_progress("fan_cap")?;
9676                Ok(Expr {
9677                    kind: ExprKind::FanExpr {
9678                        count,
9679                        block,
9680                        progress,
9681                        capture: true,
9682                    },
9683                    line,
9684                })
9685            }
9686            "async" => {
9687                if !matches!(self.peek(), Token::LBrace) {
9688                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
9689                }
9690                let block = self.parse_block()?;
9691                Ok(Expr {
9692                    kind: ExprKind::AsyncBlock { body: block },
9693                    line,
9694                })
9695            }
9696            "spawn" => {
9697                if !matches!(self.peek(), Token::LBrace) {
9698                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
9699                }
9700                let block = self.parse_block()?;
9701                Ok(Expr {
9702                    kind: ExprKind::SpawnBlock { body: block },
9703                    line,
9704                })
9705            }
9706            "trace" => {
9707                if !matches!(self.peek(), Token::LBrace) {
9708                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
9709                }
9710                let block = self.parse_block()?;
9711                Ok(Expr {
9712                    kind: ExprKind::Trace { body: block },
9713                    line,
9714                })
9715            }
9716            "timer" => {
9717                let block = self.parse_block_or_bareword_block_no_args()?;
9718                Ok(Expr {
9719                    kind: ExprKind::Timer { body: block },
9720                    line,
9721                })
9722            }
9723            "bench" => {
9724                let block = self.parse_block_or_bareword_block_no_args()?;
9725                let times = Box::new(self.parse_expression()?);
9726                Ok(Expr {
9727                    kind: ExprKind::Bench { body: block, times },
9728                    line,
9729                })
9730            }
9731            "spinner" => {
9732                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
9733                let (message, body) = if matches!(self.peek(), Token::LBrace) {
9734                    let body = self.parse_block()?;
9735                    (
9736                        Box::new(Expr {
9737                            kind: ExprKind::String("working".to_string()),
9738                            line,
9739                        }),
9740                        body,
9741                    )
9742                } else {
9743                    let msg = self.parse_assign_expr()?;
9744                    let body = self.parse_block()?;
9745                    (Box::new(msg), body)
9746                };
9747                Ok(Expr {
9748                    kind: ExprKind::Spinner { message, body },
9749                    line,
9750                })
9751            }
9752            "thread" | "t" => {
9753                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
9754                // `t` is a short alias for `thread`
9755                // Each stage is either:
9756                //   - `ident` — bare function call
9757                //   - `ident { block }` — function with block arg
9758                //   - `ident arg1 arg2 { block }` — function with args and optional block
9759                //   - `fn { block }` — standalone anonymous block
9760                //   - `>{ block }` — shorthand for standalone anonymous block
9761                // Desugars to: EXPR |> stage1 |> stage2 |> ...
9762                self.parse_thread_macro(line, false)
9763            }
9764            "retry" => {
9765                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
9766                // An optional comma before `times` is allowed in both forms.
9767                let body = if matches!(self.peek(), Token::LBrace) {
9768                    self.parse_block()?
9769                } else {
9770                    let bw_line = self.peek_line();
9771                    let Token::Ident(ref name) = self.peek().clone() else {
9772                        return Err(self
9773                            .syntax_err("retry: expected block or bareword function name", line));
9774                    };
9775                    let name = name.clone();
9776                    self.advance();
9777                    vec![Statement::new(
9778                        StmtKind::Expression(Expr {
9779                            kind: ExprKind::FuncCall { name, args: vec![] },
9780                            line: bw_line,
9781                        }),
9782                        bw_line,
9783                    )]
9784                };
9785                self.eat(&Token::Comma);
9786                match self.peek() {
9787                    Token::Ident(ref s) if s == "times" => {
9788                        self.advance();
9789                    }
9790                    _ => {
9791                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
9792                    }
9793                }
9794                self.expect(&Token::FatArrow)?;
9795                let times = Box::new(self.parse_assign_expr()?);
9796                let mut backoff = RetryBackoff::None;
9797                if self.eat(&Token::Comma) {
9798                    match self.peek() {
9799                        Token::Ident(ref s) if s == "backoff" => {
9800                            self.advance();
9801                        }
9802                        _ => {
9803                            return Err(
9804                                self.syntax_err("retry: expected `backoff =>` after comma", line)
9805                            );
9806                        }
9807                    }
9808                    self.expect(&Token::FatArrow)?;
9809                    let Token::Ident(mode) = self.peek().clone() else {
9810                        return Err(self.syntax_err(
9811                            "retry: expected backoff mode (none, linear, exponential)",
9812                            line,
9813                        ));
9814                    };
9815                    backoff = match mode.as_str() {
9816                        "none" => RetryBackoff::None,
9817                        "linear" => RetryBackoff::Linear,
9818                        "exponential" => RetryBackoff::Exponential,
9819                        _ => {
9820                            return Err(
9821                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
9822                            );
9823                        }
9824                    };
9825                    self.advance();
9826                }
9827                Ok(Expr {
9828                    kind: ExprKind::RetryBlock {
9829                        body,
9830                        times,
9831                        backoff,
9832                    },
9833                    line,
9834                })
9835            }
9836            "rate_limit" => {
9837                self.expect(&Token::LParen)?;
9838                let max = Box::new(self.parse_assign_expr()?);
9839                self.expect(&Token::Comma)?;
9840                let window = Box::new(self.parse_assign_expr()?);
9841                self.expect(&Token::RParen)?;
9842                let body = self.parse_block_or_bareword_block_no_args()?;
9843                let slot = self.alloc_rate_limit_slot();
9844                Ok(Expr {
9845                    kind: ExprKind::RateLimitBlock {
9846                        slot,
9847                        max,
9848                        window,
9849                        body,
9850                    },
9851                    line,
9852                })
9853            }
9854            "every" => {
9855                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
9856                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
9857                let has_paren = self.eat(&Token::LParen);
9858                let interval = Box::new(self.parse_assign_expr()?);
9859                if has_paren {
9860                    self.expect(&Token::RParen)?;
9861                }
9862                let body = if matches!(self.peek(), Token::LBrace) {
9863                    self.parse_block()?
9864                } else {
9865                    let bline = self.peek_line();
9866                    let expr = self.parse_assign_expr()?;
9867                    vec![Statement::new(StmtKind::Expression(expr), bline)]
9868                };
9869                Ok(Expr {
9870                    kind: ExprKind::EveryBlock { interval, body },
9871                    line,
9872                })
9873            }
9874            "gen" => {
9875                if !matches!(self.peek(), Token::LBrace) {
9876                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
9877                }
9878                let body = self.parse_block()?;
9879                Ok(Expr {
9880                    kind: ExprKind::GenBlock { body },
9881                    line,
9882                })
9883            }
9884            "yield" => {
9885                let e = self.parse_assign_expr()?;
9886                Ok(Expr {
9887                    kind: ExprKind::Yield(Box::new(e)),
9888                    line,
9889                })
9890            }
9891            "await" => {
9892                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9893                    return Ok(e);
9894                }
9895                // `await` defaults to `$_` so `map { await } @tasks` works
9896                // (Perl-style topic-defaulting unary).
9897                let a = self.parse_one_arg_or_default()?;
9898                Ok(Expr {
9899                    kind: ExprKind::Await(Box::new(a)),
9900                    line,
9901                })
9902            }
9903            "slurp" | "cat" | "c" => {
9904                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9905                    return Ok(e);
9906                }
9907                let a = self.parse_one_arg_or_default()?;
9908                Ok(Expr {
9909                    kind: ExprKind::Slurp(Box::new(a)),
9910                    line,
9911                })
9912            }
9913            "capture" => {
9914                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9915                    return Ok(e);
9916                }
9917                let a = self.parse_one_arg()?;
9918                Ok(Expr {
9919                    kind: ExprKind::Capture(Box::new(a)),
9920                    line,
9921                })
9922            }
9923            "fetch_url" => {
9924                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9925                    return Ok(e);
9926                }
9927                let a = self.parse_one_arg()?;
9928                Ok(Expr {
9929                    kind: ExprKind::FetchUrl(Box::new(a)),
9930                    line,
9931                })
9932            }
9933            "pchannel" => {
9934                let capacity = if self.eat(&Token::LParen) {
9935                    if matches!(self.peek(), Token::RParen) {
9936                        self.advance();
9937                        None
9938                    } else {
9939                        let e = self.parse_expression()?;
9940                        self.expect(&Token::RParen)?;
9941                        Some(Box::new(e))
9942                    }
9943                } else {
9944                    None
9945                };
9946                Ok(Expr {
9947                    kind: ExprKind::Pchannel { capacity },
9948                    line,
9949                })
9950            }
9951            "psort" => {
9952                if matches!(self.peek(), Token::LBrace)
9953                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
9954                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9955                {
9956                    let block = self.parse_block_or_bareword_cmp_block()?;
9957                    self.eat(&Token::Comma);
9958                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9959                    Ok(Expr {
9960                        kind: ExprKind::PSortExpr {
9961                            cmp: Some(block),
9962                            list: Box::new(list),
9963                            progress: progress.map(Box::new),
9964                        },
9965                        line,
9966                    })
9967                } else {
9968                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9969                    Ok(Expr {
9970                        kind: ExprKind::PSortExpr {
9971                            cmp: None,
9972                            list: Box::new(list),
9973                            progress: progress.map(Box::new),
9974                        },
9975                        line,
9976                    })
9977                }
9978            }
9979            "preduce" => {
9980                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9981                Ok(Expr {
9982                    kind: ExprKind::PReduceExpr {
9983                        block,
9984                        list: Box::new(list),
9985                        progress: progress.map(Box::new),
9986                    },
9987                    line,
9988                })
9989            }
9990            "preduce_init" => {
9991                let (init, block, list, progress) =
9992                    self.parse_init_block_then_list_optional_progress()?;
9993                Ok(Expr {
9994                    kind: ExprKind::PReduceInitExpr {
9995                        init: Box::new(init),
9996                        block,
9997                        list: Box::new(list),
9998                        progress: progress.map(Box::new),
9999                    },
10000                    line,
10001                })
10002            }
10003            "pmap_reduce" => {
10004                let map_block = self.parse_block_or_bareword_block()?;
10005                // After the map block, expect either a `{ REDUCE }` block, or
10006                // after an eaten comma, a blockless reduce expr (`$a + $b`).
10007                let reduce_block = if matches!(self.peek(), Token::LBrace) {
10008                    self.parse_block()?
10009                } else {
10010                    // comma separates blockless map from blockless reduce
10011                    self.expect(&Token::Comma)?;
10012                    self.parse_block_or_bareword_cmp_block()?
10013                };
10014                self.eat(&Token::Comma);
10015                let line = self.peek_line();
10016                if let Token::Ident(ref kw) = self.peek().clone() {
10017                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10018                        self.advance();
10019                        self.expect(&Token::FatArrow)?;
10020                        let prog = self.parse_assign_expr()?;
10021                        return Ok(Expr {
10022                            kind: ExprKind::PMapReduceExpr {
10023                                map_block,
10024                                reduce_block,
10025                                list: Box::new(Expr {
10026                                    kind: ExprKind::List(vec![]),
10027                                    line,
10028                                }),
10029                                progress: Some(Box::new(prog)),
10030                            },
10031                            line,
10032                        });
10033                    }
10034                }
10035                if matches!(
10036                    self.peek(),
10037                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10038                ) {
10039                    return Ok(Expr {
10040                        kind: ExprKind::PMapReduceExpr {
10041                            map_block,
10042                            reduce_block,
10043                            list: Box::new(Expr {
10044                                kind: ExprKind::List(vec![]),
10045                                line,
10046                            }),
10047                            progress: None,
10048                        },
10049                        line,
10050                    });
10051                }
10052                let mut parts = vec![self.parse_assign_expr()?];
10053                loop {
10054                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10055                        break;
10056                    }
10057                    if matches!(
10058                        self.peek(),
10059                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10060                    ) {
10061                        break;
10062                    }
10063                    if let Token::Ident(ref kw) = self.peek().clone() {
10064                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10065                            self.advance();
10066                            self.expect(&Token::FatArrow)?;
10067                            let prog = self.parse_assign_expr()?;
10068                            return Ok(Expr {
10069                                kind: ExprKind::PMapReduceExpr {
10070                                    map_block,
10071                                    reduce_block,
10072                                    list: Box::new(merge_expr_list(parts)),
10073                                    progress: Some(Box::new(prog)),
10074                                },
10075                                line,
10076                            });
10077                        }
10078                    }
10079                    parts.push(self.parse_assign_expr()?);
10080                }
10081                Ok(Expr {
10082                    kind: ExprKind::PMapReduceExpr {
10083                        map_block,
10084                        reduce_block,
10085                        list: Box::new(merge_expr_list(parts)),
10086                        progress: None,
10087                    },
10088                    line,
10089                })
10090            }
10091            "puniq" => {
10092                if self.pipe_supplies_slurped_list_operand() {
10093                    return Ok(Expr {
10094                        kind: ExprKind::FuncCall {
10095                            name: "puniq".to_string(),
10096                            args: vec![],
10097                        },
10098                        line,
10099                    });
10100                }
10101                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10102                let mut args = vec![list];
10103                if let Some(p) = progress {
10104                    args.push(p);
10105                }
10106                Ok(Expr {
10107                    kind: ExprKind::FuncCall {
10108                        name: "puniq".to_string(),
10109                        args,
10110                    },
10111                    line,
10112                })
10113            }
10114            "pfirst" => {
10115                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10116                let cr = Expr {
10117                    kind: ExprKind::CodeRef {
10118                        params: vec![],
10119                        body: block,
10120                    },
10121                    line,
10122                };
10123                let mut args = vec![cr, list];
10124                if let Some(p) = progress {
10125                    args.push(p);
10126                }
10127                Ok(Expr {
10128                    kind: ExprKind::FuncCall {
10129                        name: "pfirst".to_string(),
10130                        args,
10131                    },
10132                    line,
10133                })
10134            }
10135            "pany" => {
10136                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10137                let cr = Expr {
10138                    kind: ExprKind::CodeRef {
10139                        params: vec![],
10140                        body: block,
10141                    },
10142                    line,
10143                };
10144                let mut args = vec![cr, list];
10145                if let Some(p) = progress {
10146                    args.push(p);
10147                }
10148                Ok(Expr {
10149                    kind: ExprKind::FuncCall {
10150                        name: "pany".to_string(),
10151                        args,
10152                    },
10153                    line,
10154                })
10155            }
10156            "uniq" | "distinct" => {
10157                if self.pipe_supplies_slurped_list_operand() {
10158                    return Ok(Expr {
10159                        kind: ExprKind::FuncCall {
10160                            name: name.clone(),
10161                            args: vec![],
10162                        },
10163                        line,
10164                    });
10165                }
10166                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10167                if progress.is_some() {
10168                    return Err(self.syntax_err(
10169                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
10170                        line,
10171                    ));
10172                }
10173                Ok(Expr {
10174                    kind: ExprKind::FuncCall {
10175                        name: name.clone(),
10176                        args: vec![list],
10177                    },
10178                    line,
10179                })
10180            }
10181            "flatten" => {
10182                if self.pipe_supplies_slurped_list_operand() {
10183                    return Ok(Expr {
10184                        kind: ExprKind::FuncCall {
10185                            name: "flatten".to_string(),
10186                            args: vec![],
10187                        },
10188                        line,
10189                    });
10190                }
10191                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10192                if progress.is_some() {
10193                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
10194                }
10195                Ok(Expr {
10196                    kind: ExprKind::FuncCall {
10197                        name: "flatten".to_string(),
10198                        args: vec![list],
10199                    },
10200                    line,
10201                })
10202            }
10203            "set" => {
10204                if self.pipe_supplies_slurped_list_operand() {
10205                    return Ok(Expr {
10206                        kind: ExprKind::FuncCall {
10207                            name: "set".to_string(),
10208                            args: vec![],
10209                        },
10210                        line,
10211                    });
10212                }
10213                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10214                if progress.is_some() {
10215                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
10216                }
10217                Ok(Expr {
10218                    kind: ExprKind::FuncCall {
10219                        name: "set".to_string(),
10220                        args: vec![list],
10221                    },
10222                    line,
10223                })
10224            }
10225            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
10226            // Defaults to `$_` when no arg is given, like `length`. See
10227            // `builtin_file_size` in builtins.rs for the runtime behavior.
10228            "size" => {
10229                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10230                    return Ok(e);
10231                }
10232                if self.pipe_supplies_slurped_list_operand() {
10233                    return Ok(Expr {
10234                        kind: ExprKind::FuncCall {
10235                            name: "size".to_string(),
10236                            args: vec![],
10237                        },
10238                        line,
10239                    });
10240                }
10241                let a = self.parse_one_arg_or_default()?;
10242                Ok(Expr {
10243                    kind: ExprKind::FuncCall {
10244                        name: "size".to_string(),
10245                        args: vec![a],
10246                    },
10247                    line,
10248                })
10249            }
10250            "list_count" | "list_size" | "count" | "len" | "cnt" => {
10251                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10252                    return Ok(e);
10253                }
10254                if self.pipe_supplies_slurped_list_operand() {
10255                    return Ok(Expr {
10256                        kind: ExprKind::FuncCall {
10257                            name: name.clone(),
10258                            args: vec![],
10259                        },
10260                        line,
10261                    });
10262                }
10263                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10264                if progress.is_some() {
10265                    return Err(self.syntax_err(
10266                        "`progress =>` is not supported for list_count / list_size / count / cnt",
10267                        line,
10268                    ));
10269                }
10270                Ok(Expr {
10271                    kind: ExprKind::FuncCall {
10272                        name: name.clone(),
10273                        args: vec![list],
10274                    },
10275                    line,
10276                })
10277            }
10278            "shuffle" | "shuffled" => {
10279                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10280                    return Ok(e);
10281                }
10282                if self.pipe_supplies_slurped_list_operand() {
10283                    return Ok(Expr {
10284                        kind: ExprKind::FuncCall {
10285                            name: "shuffle".to_string(),
10286                            args: vec![],
10287                        },
10288                        line,
10289                    });
10290                }
10291                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10292                if progress.is_some() {
10293                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
10294                }
10295                Ok(Expr {
10296                    kind: ExprKind::FuncCall {
10297                        name: "shuffle".to_string(),
10298                        args: vec![list],
10299                    },
10300                    line,
10301                })
10302            }
10303            "chunked" => {
10304                let mut parts = Vec::new();
10305                if self.eat(&Token::LParen) {
10306                    if !matches!(self.peek(), Token::RParen) {
10307                        parts.push(self.parse_assign_expr()?);
10308                        while self.eat(&Token::Comma) {
10309                            if matches!(self.peek(), Token::RParen) {
10310                                break;
10311                            }
10312                            parts.push(self.parse_assign_expr()?);
10313                        }
10314                    }
10315                    self.expect(&Token::RParen)?;
10316                } else {
10317                    // Paren-less `chunked N`: `|>` is a hard terminator, not
10318                    // an operator inside the arg (see
10319                    // `parse_assign_expr_stop_at_pipe`).
10320                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
10321                    loop {
10322                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10323                            break;
10324                        }
10325                        if matches!(
10326                            self.peek(),
10327                            Token::Semicolon
10328                                | Token::RBrace
10329                                | Token::RParen
10330                                | Token::Eof
10331                                | Token::PipeForward
10332                        ) {
10333                            break;
10334                        }
10335                        if self.peek_is_postfix_stmt_modifier_keyword() {
10336                            break;
10337                        }
10338                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
10339                    }
10340                }
10341                if parts.len() == 1 {
10342                    let n = parts.pop().unwrap();
10343                    return Ok(Expr {
10344                        kind: ExprKind::FuncCall {
10345                            name: "chunked".to_string(),
10346                            args: vec![n],
10347                        },
10348                        line,
10349                    });
10350                }
10351                if parts.is_empty() {
10352                    return Ok(Expr {
10353                        kind: ExprKind::FuncCall {
10354                            name: "chunked".to_string(),
10355                            args: parts,
10356                        },
10357                        line,
10358                    });
10359                }
10360                if parts.len() == 2 {
10361                    let n = parts.pop().unwrap();
10362                    let list = parts.pop().unwrap();
10363                    return Ok(Expr {
10364                        kind: ExprKind::FuncCall {
10365                            name: "chunked".to_string(),
10366                            args: vec![list, n],
10367                        },
10368                        line,
10369                    });
10370                }
10371                Err(self.syntax_err(
10372                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
10373                    line,
10374                ))
10375            }
10376            "windowed" => {
10377                let mut parts = Vec::new();
10378                if self.eat(&Token::LParen) {
10379                    if !matches!(self.peek(), Token::RParen) {
10380                        parts.push(self.parse_assign_expr()?);
10381                        while self.eat(&Token::Comma) {
10382                            if matches!(self.peek(), Token::RParen) {
10383                                break;
10384                            }
10385                            parts.push(self.parse_assign_expr()?);
10386                        }
10387                    }
10388                    self.expect(&Token::RParen)?;
10389                } else {
10390                    // Paren-less `windowed N`: same `|>`-terminator rule as
10391                    // `chunked` above.
10392                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
10393                    loop {
10394                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10395                            break;
10396                        }
10397                        if matches!(
10398                            self.peek(),
10399                            Token::Semicolon
10400                                | Token::RBrace
10401                                | Token::RParen
10402                                | Token::Eof
10403                                | Token::PipeForward
10404                        ) {
10405                            break;
10406                        }
10407                        if self.peek_is_postfix_stmt_modifier_keyword() {
10408                            break;
10409                        }
10410                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
10411                    }
10412                }
10413                if parts.len() == 1 {
10414                    let n = parts.pop().unwrap();
10415                    return Ok(Expr {
10416                        kind: ExprKind::FuncCall {
10417                            name: "windowed".to_string(),
10418                            args: vec![n],
10419                        },
10420                        line,
10421                    });
10422                }
10423                if parts.is_empty() {
10424                    return Ok(Expr {
10425                        kind: ExprKind::FuncCall {
10426                            name: "windowed".to_string(),
10427                            args: parts,
10428                        },
10429                        line,
10430                    });
10431                }
10432                if parts.len() == 2 {
10433                    let n = parts.pop().unwrap();
10434                    let list = parts.pop().unwrap();
10435                    return Ok(Expr {
10436                        kind: ExprKind::FuncCall {
10437                            name: "windowed".to_string(),
10438                            args: vec![list, n],
10439                        },
10440                        line,
10441                    });
10442                }
10443                Err(self.syntax_err(
10444                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
10445                    line,
10446                ))
10447            }
10448            "any" | "all" | "none" => {
10449                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10450                if progress.is_some() {
10451                    return Err(self.syntax_err(
10452                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
10453                        line,
10454                    ));
10455                }
10456                let cr = Expr {
10457                    kind: ExprKind::CodeRef {
10458                        params: vec![],
10459                        body: block,
10460                    },
10461                    line,
10462                };
10463                Ok(Expr {
10464                    kind: ExprKind::FuncCall {
10465                        name: name.clone(),
10466                        args: vec![cr, list],
10467                    },
10468                    line,
10469                })
10470            }
10471            // Ruby `detect` / `find` — same as `List::Util::first` (first element matching block).
10472            "first" | "detect" | "find" => {
10473                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10474                if progress.is_some() {
10475                    return Err(self.syntax_err(
10476                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
10477                        line,
10478                    ));
10479                }
10480                let cr = Expr {
10481                    kind: ExprKind::CodeRef {
10482                        params: vec![],
10483                        body: block,
10484                    },
10485                    line,
10486                };
10487                Ok(Expr {
10488                    kind: ExprKind::FuncCall {
10489                        name: "first".to_string(),
10490                        args: vec![cr, list],
10491                    },
10492                    line,
10493                })
10494            }
10495            "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
10496            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
10497                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10498                if progress.is_some() {
10499                    return Err(
10500                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
10501                    );
10502                }
10503                let cr = Expr {
10504                    kind: ExprKind::CodeRef {
10505                        params: vec![],
10506                        body: block,
10507                    },
10508                    line,
10509                };
10510                Ok(Expr {
10511                    kind: ExprKind::FuncCall {
10512                        name: name.to_string(),
10513                        args: vec![cr, list],
10514                    },
10515                    line,
10516                })
10517            }
10518            "group_by" | "chunk_by" => {
10519                if matches!(self.peek(), Token::LBrace) {
10520                    let (block, list) = self.parse_block_list()?;
10521                    let cr = Expr {
10522                        kind: ExprKind::CodeRef {
10523                            params: vec![],
10524                            body: block,
10525                        },
10526                        line,
10527                    };
10528                    Ok(Expr {
10529                        kind: ExprKind::FuncCall {
10530                            name: name.to_string(),
10531                            args: vec![cr, list],
10532                        },
10533                        line,
10534                    })
10535                } else {
10536                    let key_expr = self.parse_assign_expr()?;
10537                    self.expect(&Token::Comma)?;
10538                    let list_parts = self.parse_list_until_terminator()?;
10539                    let list_expr = if list_parts.len() == 1 {
10540                        list_parts.into_iter().next().unwrap()
10541                    } else {
10542                        Expr {
10543                            kind: ExprKind::List(list_parts),
10544                            line,
10545                        }
10546                    };
10547                    Ok(Expr {
10548                        kind: ExprKind::FuncCall {
10549                            name: name.to_string(),
10550                            args: vec![key_expr, list_expr],
10551                        },
10552                        line,
10553                    })
10554                }
10555            }
10556            "with_index" => {
10557                if self.pipe_supplies_slurped_list_operand() {
10558                    return Ok(Expr {
10559                        kind: ExprKind::FuncCall {
10560                            name: "with_index".to_string(),
10561                            args: vec![],
10562                        },
10563                        line,
10564                    });
10565                }
10566                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10567                if progress.is_some() {
10568                    return Err(
10569                        self.syntax_err("`progress =>` is not supported for with_index", line)
10570                    );
10571                }
10572                Ok(Expr {
10573                    kind: ExprKind::FuncCall {
10574                        name: "with_index".to_string(),
10575                        args: vec![list],
10576                    },
10577                    line,
10578                })
10579            }
10580            "pcache" => {
10581                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10582                Ok(Expr {
10583                    kind: ExprKind::PcacheExpr {
10584                        block,
10585                        list: Box::new(list),
10586                        progress: progress.map(Box::new),
10587                    },
10588                    line,
10589                })
10590            }
10591            "pselect" => {
10592                let paren = self.eat(&Token::LParen);
10593                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
10594                if paren {
10595                    self.expect(&Token::RParen)?;
10596                }
10597                if receivers.is_empty() {
10598                    return Err(self.syntax_err("pselect needs at least one receiver", line));
10599                }
10600                Ok(Expr {
10601                    kind: ExprKind::PselectExpr {
10602                        receivers,
10603                        timeout: timeout.map(Box::new),
10604                    },
10605                    line,
10606                })
10607            }
10608            "open" => {
10609                let paren = matches!(self.peek(), Token::LParen);
10610                if paren {
10611                    self.advance();
10612                }
10613                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
10614                    self.advance();
10615                    let name = self.parse_scalar_var_name()?;
10616                    self.expect(&Token::Comma)?;
10617                    let mode = self.parse_assign_expr()?;
10618                    let file = if self.eat(&Token::Comma) {
10619                        Some(self.parse_assign_expr()?)
10620                    } else {
10621                        None
10622                    };
10623                    if paren {
10624                        self.expect(&Token::RParen)?;
10625                    }
10626                    Ok(Expr {
10627                        kind: ExprKind::Open {
10628                            handle: Box::new(Expr {
10629                                kind: ExprKind::OpenMyHandle { name },
10630                                line,
10631                            }),
10632                            mode: Box::new(mode),
10633                            file: file.map(Box::new),
10634                        },
10635                        line,
10636                    })
10637                } else {
10638                    let args = if paren {
10639                        self.parse_arg_list()?
10640                    } else {
10641                        self.parse_list_until_terminator()?
10642                    };
10643                    if paren {
10644                        self.expect(&Token::RParen)?;
10645                    }
10646                    if args.len() < 2 {
10647                        return Err(self.syntax_err("open requires at least 2 arguments", line));
10648                    }
10649                    Ok(Expr {
10650                        kind: ExprKind::Open {
10651                            handle: Box::new(args[0].clone()),
10652                            mode: Box::new(args[1].clone()),
10653                            file: args.get(2).cloned().map(Box::new),
10654                        },
10655                        line,
10656                    })
10657                }
10658            }
10659            "close" => {
10660                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10661                    return Ok(e);
10662                }
10663                let a = self.parse_one_arg_or_default()?;
10664                Ok(Expr {
10665                    kind: ExprKind::Close(Box::new(a)),
10666                    line,
10667                })
10668            }
10669            "opendir" => {
10670                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10671                    return Ok(e);
10672                }
10673                let args = self.parse_builtin_args()?;
10674                if args.len() != 2 {
10675                    return Err(self.syntax_err("opendir requires two arguments", line));
10676                }
10677                Ok(Expr {
10678                    kind: ExprKind::Opendir {
10679                        handle: Box::new(args[0].clone()),
10680                        path: Box::new(args[1].clone()),
10681                    },
10682                    line,
10683                })
10684            }
10685            "readdir" => {
10686                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10687                    return Ok(e);
10688                }
10689                let a = self.parse_one_arg()?;
10690                Ok(Expr {
10691                    kind: ExprKind::Readdir(Box::new(a)),
10692                    line,
10693                })
10694            }
10695            "closedir" => {
10696                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10697                    return Ok(e);
10698                }
10699                let a = self.parse_one_arg()?;
10700                Ok(Expr {
10701                    kind: ExprKind::Closedir(Box::new(a)),
10702                    line,
10703                })
10704            }
10705            "rewinddir" => {
10706                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10707                    return Ok(e);
10708                }
10709                let a = self.parse_one_arg()?;
10710                Ok(Expr {
10711                    kind: ExprKind::Rewinddir(Box::new(a)),
10712                    line,
10713                })
10714            }
10715            "telldir" => {
10716                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10717                    return Ok(e);
10718                }
10719                let a = self.parse_one_arg()?;
10720                Ok(Expr {
10721                    kind: ExprKind::Telldir(Box::new(a)),
10722                    line,
10723                })
10724            }
10725            "seekdir" => {
10726                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10727                    return Ok(e);
10728                }
10729                let args = self.parse_builtin_args()?;
10730                if args.len() != 2 {
10731                    return Err(self.syntax_err("seekdir requires two arguments", line));
10732                }
10733                Ok(Expr {
10734                    kind: ExprKind::Seekdir {
10735                        handle: Box::new(args[0].clone()),
10736                        position: Box::new(args[1].clone()),
10737                    },
10738                    line,
10739                })
10740            }
10741            "eof" => {
10742                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10743                    return Ok(e);
10744                }
10745                if matches!(self.peek(), Token::LParen) {
10746                    self.advance();
10747                    if matches!(self.peek(), Token::RParen) {
10748                        self.advance();
10749                        Ok(Expr {
10750                            kind: ExprKind::Eof(None),
10751                            line,
10752                        })
10753                    } else {
10754                        let a = self.parse_expression()?;
10755                        self.expect(&Token::RParen)?;
10756                        Ok(Expr {
10757                            kind: ExprKind::Eof(Some(Box::new(a))),
10758                            line,
10759                        })
10760                    }
10761                } else {
10762                    Ok(Expr {
10763                        kind: ExprKind::Eof(None),
10764                        line,
10765                    })
10766                }
10767            }
10768            "system" => {
10769                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10770                    return Ok(e);
10771                }
10772                let args = self.parse_builtin_args()?;
10773                Ok(Expr {
10774                    kind: ExprKind::System(args),
10775                    line,
10776                })
10777            }
10778            "exec" => {
10779                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10780                    return Ok(e);
10781                }
10782                let args = self.parse_builtin_args()?;
10783                Ok(Expr {
10784                    kind: ExprKind::Exec(args),
10785                    line,
10786                })
10787            }
10788            "eval" => {
10789                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10790                    return Ok(e);
10791                }
10792                let a = if matches!(self.peek(), Token::LBrace) {
10793                    let block = self.parse_block()?;
10794                    Expr {
10795                        kind: ExprKind::CodeRef {
10796                            params: vec![],
10797                            body: block,
10798                        },
10799                        line,
10800                    }
10801                } else {
10802                    self.parse_one_arg_or_default()?
10803                };
10804                Ok(Expr {
10805                    kind: ExprKind::Eval(Box::new(a)),
10806                    line,
10807                })
10808            }
10809            "do" => {
10810                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10811                    return Ok(e);
10812                }
10813                let a = self.parse_one_arg()?;
10814                Ok(Expr {
10815                    kind: ExprKind::Do(Box::new(a)),
10816                    line,
10817                })
10818            }
10819            "require" => {
10820                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10821                    return Ok(e);
10822                }
10823                let a = self.parse_one_arg()?;
10824                Ok(Expr {
10825                    kind: ExprKind::Require(Box::new(a)),
10826                    line,
10827                })
10828            }
10829            "exit" => {
10830                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10831                    return Ok(e);
10832                }
10833                if matches!(
10834                    self.peek(),
10835                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
10836                ) {
10837                    Ok(Expr {
10838                        kind: ExprKind::Exit(None),
10839                        line,
10840                    })
10841                } else {
10842                    let a = self.parse_one_arg()?;
10843                    Ok(Expr {
10844                        kind: ExprKind::Exit(Some(Box::new(a))),
10845                        line,
10846                    })
10847                }
10848            }
10849            "chdir" => {
10850                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10851                    return Ok(e);
10852                }
10853                let a = self.parse_one_arg_or_default()?;
10854                Ok(Expr {
10855                    kind: ExprKind::Chdir(Box::new(a)),
10856                    line,
10857                })
10858            }
10859            "mkdir" => {
10860                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10861                    return Ok(e);
10862                }
10863                let args = self.parse_builtin_args()?;
10864                Ok(Expr {
10865                    kind: ExprKind::Mkdir {
10866                        path: Box::new(args[0].clone()),
10867                        mode: args.get(1).cloned().map(Box::new),
10868                    },
10869                    line,
10870                })
10871            }
10872            "unlink" | "rm" => {
10873                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10874                    return Ok(e);
10875                }
10876                let args = self.parse_builtin_args()?;
10877                Ok(Expr {
10878                    kind: ExprKind::Unlink(args),
10879                    line,
10880                })
10881            }
10882            "rename" => {
10883                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10884                    return Ok(e);
10885                }
10886                let args = self.parse_builtin_args()?;
10887                if args.len() != 2 {
10888                    return Err(self.syntax_err("rename requires two arguments", line));
10889                }
10890                Ok(Expr {
10891                    kind: ExprKind::Rename {
10892                        old: Box::new(args[0].clone()),
10893                        new: Box::new(args[1].clone()),
10894                    },
10895                    line,
10896                })
10897            }
10898            "chmod" => {
10899                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10900                    return Ok(e);
10901                }
10902                let args = self.parse_builtin_args()?;
10903                if args.len() < 2 {
10904                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
10905                }
10906                Ok(Expr {
10907                    kind: ExprKind::Chmod(args),
10908                    line,
10909                })
10910            }
10911            "chown" => {
10912                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10913                    return Ok(e);
10914                }
10915                let args = self.parse_builtin_args()?;
10916                if args.len() < 3 {
10917                    return Err(
10918                        self.syntax_err("chown requires uid, gid, and at least one file", line)
10919                    );
10920                }
10921                Ok(Expr {
10922                    kind: ExprKind::Chown(args),
10923                    line,
10924                })
10925            }
10926            "stat" => {
10927                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10928                    return Ok(e);
10929                }
10930                let args = self.parse_builtin_args()?;
10931                let arg = if args.len() == 1 {
10932                    args[0].clone()
10933                } else if args.is_empty() {
10934                    Expr {
10935                        kind: ExprKind::ScalarVar("_".into()),
10936                        line,
10937                    }
10938                } else {
10939                    return Err(self.syntax_err("stat requires zero or one argument", line));
10940                };
10941                Ok(Expr {
10942                    kind: ExprKind::Stat(Box::new(arg)),
10943                    line,
10944                })
10945            }
10946            "lstat" => {
10947                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10948                    return Ok(e);
10949                }
10950                let args = self.parse_builtin_args()?;
10951                let arg = if args.len() == 1 {
10952                    args[0].clone()
10953                } else if args.is_empty() {
10954                    Expr {
10955                        kind: ExprKind::ScalarVar("_".into()),
10956                        line,
10957                    }
10958                } else {
10959                    return Err(self.syntax_err("lstat requires zero or one argument", line));
10960                };
10961                Ok(Expr {
10962                    kind: ExprKind::Lstat(Box::new(arg)),
10963                    line,
10964                })
10965            }
10966            "link" => {
10967                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10968                    return Ok(e);
10969                }
10970                let args = self.parse_builtin_args()?;
10971                if args.len() != 2 {
10972                    return Err(self.syntax_err("link requires two arguments", line));
10973                }
10974                Ok(Expr {
10975                    kind: ExprKind::Link {
10976                        old: Box::new(args[0].clone()),
10977                        new: Box::new(args[1].clone()),
10978                    },
10979                    line,
10980                })
10981            }
10982            "symlink" => {
10983                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10984                    return Ok(e);
10985                }
10986                let args = self.parse_builtin_args()?;
10987                if args.len() != 2 {
10988                    return Err(self.syntax_err("symlink requires two arguments", line));
10989                }
10990                Ok(Expr {
10991                    kind: ExprKind::Symlink {
10992                        old: Box::new(args[0].clone()),
10993                        new: Box::new(args[1].clone()),
10994                    },
10995                    line,
10996                })
10997            }
10998            "readlink" => {
10999                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11000                    return Ok(e);
11001                }
11002                let args = self.parse_builtin_args()?;
11003                let arg = if args.len() == 1 {
11004                    args[0].clone()
11005                } else if args.is_empty() {
11006                    Expr {
11007                        kind: ExprKind::ScalarVar("_".into()),
11008                        line,
11009                    }
11010                } else {
11011                    return Err(self.syntax_err("readlink requires zero or one argument", line));
11012                };
11013                Ok(Expr {
11014                    kind: ExprKind::Readlink(Box::new(arg)),
11015                    line,
11016                })
11017            }
11018            "files" => {
11019                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11020                    return Ok(e);
11021                }
11022                let args = self.parse_builtin_args()?;
11023                Ok(Expr {
11024                    kind: ExprKind::Files(args),
11025                    line,
11026                })
11027            }
11028            "filesf" | "f" => {
11029                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11030                    return Ok(e);
11031                }
11032                let args = self.parse_builtin_args()?;
11033                Ok(Expr {
11034                    kind: ExprKind::Filesf(args),
11035                    line,
11036                })
11037            }
11038            "fr" => {
11039                let args = self.parse_builtin_args()?;
11040                Ok(Expr {
11041                    kind: ExprKind::FilesfRecursive(args),
11042                    line,
11043                })
11044            }
11045            "dirs" | "d" => {
11046                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11047                    return Ok(e);
11048                }
11049                let args = self.parse_builtin_args()?;
11050                Ok(Expr {
11051                    kind: ExprKind::Dirs(args),
11052                    line,
11053                })
11054            }
11055            "dr" => {
11056                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11057                    return Ok(e);
11058                }
11059                let args = self.parse_builtin_args()?;
11060                Ok(Expr {
11061                    kind: ExprKind::DirsRecursive(args),
11062                    line,
11063                })
11064            }
11065            "sym_links" => {
11066                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11067                    return Ok(e);
11068                }
11069                let args = self.parse_builtin_args()?;
11070                Ok(Expr {
11071                    kind: ExprKind::SymLinks(args),
11072                    line,
11073                })
11074            }
11075            "sockets" => {
11076                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11077                    return Ok(e);
11078                }
11079                let args = self.parse_builtin_args()?;
11080                Ok(Expr {
11081                    kind: ExprKind::Sockets(args),
11082                    line,
11083                })
11084            }
11085            "pipes" => {
11086                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11087                    return Ok(e);
11088                }
11089                let args = self.parse_builtin_args()?;
11090                Ok(Expr {
11091                    kind: ExprKind::Pipes(args),
11092                    line,
11093                })
11094            }
11095            "block_devices" => {
11096                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11097                    return Ok(e);
11098                }
11099                let args = self.parse_builtin_args()?;
11100                Ok(Expr {
11101                    kind: ExprKind::BlockDevices(args),
11102                    line,
11103                })
11104            }
11105            "char_devices" => {
11106                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11107                    return Ok(e);
11108                }
11109                let args = self.parse_builtin_args()?;
11110                Ok(Expr {
11111                    kind: ExprKind::CharDevices(args),
11112                    line,
11113                })
11114            }
11115            "exe" | "executables" => {
11116                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11117                    return Ok(e);
11118                }
11119                let args = self.parse_builtin_args()?;
11120                Ok(Expr {
11121                    kind: ExprKind::Executables(args),
11122                    line,
11123                })
11124            }
11125            "glob" => {
11126                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11127                    return Ok(e);
11128                }
11129                let args = self.parse_builtin_args()?;
11130                Ok(Expr {
11131                    kind: ExprKind::Glob(args),
11132                    line,
11133                })
11134            }
11135            "glob_par" => {
11136                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11137                    return Ok(e);
11138                }
11139                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11140                Ok(Expr {
11141                    kind: ExprKind::GlobPar { args, progress },
11142                    line,
11143                })
11144            }
11145            "par_sed" => {
11146                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11147                    return Ok(e);
11148                }
11149                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11150                Ok(Expr {
11151                    kind: ExprKind::ParSed { args, progress },
11152                    line,
11153                })
11154            }
11155            "bless" => {
11156                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11157                    return Ok(e);
11158                }
11159                let args = self.parse_builtin_args()?;
11160                Ok(Expr {
11161                    kind: ExprKind::Bless {
11162                        ref_expr: Box::new(args[0].clone()),
11163                        class: args.get(1).cloned().map(Box::new),
11164                    },
11165                    line,
11166                })
11167            }
11168            "caller" => {
11169                if matches!(self.peek(), Token::LParen) {
11170                    self.advance();
11171                    if matches!(self.peek(), Token::RParen) {
11172                        self.advance();
11173                        Ok(Expr {
11174                            kind: ExprKind::Caller(None),
11175                            line,
11176                        })
11177                    } else {
11178                        let a = self.parse_expression()?;
11179                        self.expect(&Token::RParen)?;
11180                        Ok(Expr {
11181                            kind: ExprKind::Caller(Some(Box::new(a))),
11182                            line,
11183                        })
11184                    }
11185                } else {
11186                    Ok(Expr {
11187                        kind: ExprKind::Caller(None),
11188                        line,
11189                    })
11190                }
11191            }
11192            "wantarray" => {
11193                if matches!(self.peek(), Token::LParen) {
11194                    self.advance();
11195                    self.expect(&Token::RParen)?;
11196                }
11197                Ok(Expr {
11198                    kind: ExprKind::Wantarray,
11199                    line,
11200                })
11201            }
11202            "sub" => {
11203                // In non-compat mode, `fn {}` is not valid — must use `fn {}`
11204                if !crate::compat_mode() {
11205                    return Err(self.syntax_err(
11206                        "stryke uses `fn {}` instead of `fn {}` (this is not Perl 5)",
11207                        line,
11208                    ));
11209                }
11210                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
11211                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11212                let body = self.parse_block()?;
11213                Ok(Expr {
11214                    kind: ExprKind::CodeRef { params, body },
11215                    line,
11216                })
11217            }
11218            "fn" => {
11219                // Anonymous fn — stryke syntax for anonymous subroutines
11220                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11221                let body = self.parse_block()?;
11222                Ok(Expr {
11223                    kind: ExprKind::CodeRef { params, body },
11224                    line,
11225                })
11226            }
11227            _ => {
11228                // Generic function call
11229                // Check for fat arrow (bareword string in hash)
11230                if matches!(self.peek(), Token::FatArrow) {
11231                    return Ok(Expr {
11232                        kind: ExprKind::String(name),
11233                        line,
11234                    });
11235                }
11236                // Bare `_` in expression position → topic variable `$_`.
11237                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
11238                if name == "_" {
11239                    return Ok(Expr {
11240                        kind: ExprKind::ScalarVar("_".to_string()),
11241                        line,
11242                    });
11243                }
11244                // Function call with optional parens
11245                if matches!(self.peek(), Token::LParen) {
11246                    self.advance();
11247                    let args = self.parse_arg_list()?;
11248                    self.expect(&Token::RParen)?;
11249                    Ok(Expr {
11250                        kind: ExprKind::FuncCall { name, args },
11251                        line,
11252                    })
11253                } else if self.peek().is_term_start()
11254                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
11255                        && matches!(self.peek_at(1), Token::Ident(_)))
11256                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
11257                    && !(matches!(self.peek(), Token::LBrace)
11258                        && self.peek_line() > self.prev_line())
11259                {
11260                    // Perl allows func arg without parens
11261                    // Guard: `sub <name> { }` is a named sub declaration (new
11262                    // statement), not an argument to the preceding call.
11263                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
11264                    // barewords (used by thread macro so `t Color::Red p` treats
11265                    // `p` as a stage, not an argument to the enum variant), but
11266                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
11267                    // Guard: `{` on a new line is a new statement (hashref/block),
11268                    // not an argument to the preceding bareword call.
11269                    let args = self.parse_list_until_terminator()?;
11270                    Ok(Expr {
11271                        kind: ExprKind::FuncCall { name, args },
11272                        line,
11273                    })
11274                } else {
11275                    // No parens, no visible arguments — emit a Bareword.
11276                    // At runtime, Bareword tries sub resolution first (zero-arg
11277                    // call) and falls back to a string value.  stryke extension
11278                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
11279                    // with `$_` injection separately.
11280                    Ok(Expr {
11281                        kind: ExprKind::Bareword(name),
11282                        line,
11283                    })
11284                }
11285            }
11286        }
11287    }
11288
11289    fn parse_print_like(
11290        &mut self,
11291        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
11292    ) -> PerlResult<Expr> {
11293        let line = self.peek_line();
11294        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
11295        let handle = if let Token::Ident(ref h) = self.peek().clone() {
11296            if h.chars().all(|c| c.is_uppercase() || c == '_')
11297                && !matches!(self.peek(), Token::LParen)
11298            {
11299                let h = h.clone();
11300                let saved = self.pos;
11301                self.advance();
11302                // Verify next token is a term start (not operator)
11303                if self.peek().is_term_start()
11304                    || matches!(
11305                        self.peek(),
11306                        Token::DoubleString(_) | Token::BacktickString(_) | Token::SingleString(_)
11307                    )
11308                {
11309                    Some(h)
11310                } else {
11311                    self.pos = saved;
11312                    None
11313                }
11314            } else {
11315                None
11316            }
11317        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
11318            // `print $fh "msg"` — scalar variable as indirect filehandle.
11319            // Treat as handle when the next token (after $var) is a term-start or
11320            // string literal *without* a preceding comma/operator, matching Perl's
11321            // indirect-object heuristic.
11322            // Exclude `$_` — it's virtually always the topic variable, not a handle.
11323            // Exclude `[` and `{` — those are array/hash subscripts on the variable
11324            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
11325            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
11326            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
11327            let v = v.clone();
11328            if v == "_" {
11329                None
11330            } else {
11331                let saved = self.pos;
11332                self.advance();
11333                let next = self.peek().clone();
11334                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
11335                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
11336                if !is_stmt_modifier
11337                    && !matches!(next, Token::LBracket | Token::LBrace)
11338                    && (next.is_term_start()
11339                        || matches!(
11340                            next,
11341                            Token::DoubleString(_)
11342                                | Token::BacktickString(_)
11343                                | Token::SingleString(_)
11344                        ))
11345                {
11346                    // Next token looks like a print argument — $var is the handle.
11347                    Some(format!("${v}"))
11348                } else {
11349                    self.pos = saved;
11350                    None
11351                }
11352            }
11353        } else {
11354            None
11355        };
11356        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
11357        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
11358        // are given, prints $_." (Same convention as the topic-default unary
11359        // builtins handled in `parse_one_arg_or_default`.)
11360        let args =
11361            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
11362                let line_topic = self.peek_line();
11363                self.advance(); // (
11364                self.advance(); // )
11365                vec![Expr {
11366                    kind: ExprKind::ScalarVar("_".into()),
11367                    line: line_topic,
11368                }]
11369            } else {
11370                self.parse_list_until_terminator()?
11371            };
11372        Ok(Expr {
11373            kind: make(handle, args),
11374            line,
11375        })
11376    }
11377
11378    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
11379        let block = self.parse_block()?;
11380        let block_end_line = self.prev_line();
11381        self.eat(&Token::Comma);
11382        // On the RHS of `|>`, the list operand is supplied by the piped LHS
11383        // and will be substituted at desugar time — accept a placeholder when
11384        // we're at a terminator here or on a new line (implicit semicolon).
11385        if self.in_pipe_rhs()
11386            && (matches!(
11387                self.peek(),
11388                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11389            ) || self.peek_line() > block_end_line)
11390        {
11391            let line = self.peek_line();
11392            return Ok((block, self.pipe_placeholder_list(line)));
11393        }
11394        let list = self.parse_expression()?;
11395        Ok((block, list))
11396    }
11397
11398    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
11399    /// When `paren` is true, stops at `)` as well as normal terminators.
11400    fn parse_comma_expr_list_with_timeout_tail(
11401        &mut self,
11402        paren: bool,
11403    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
11404        let mut parts = vec![self.parse_assign_expr()?];
11405        loop {
11406            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11407                break;
11408            }
11409            if paren && matches!(self.peek(), Token::RParen) {
11410                break;
11411            }
11412            if matches!(
11413                self.peek(),
11414                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11415            ) {
11416                break;
11417            }
11418            if self.peek_is_postfix_stmt_modifier_keyword() {
11419                break;
11420            }
11421            if let Token::Ident(ref kw) = self.peek().clone() {
11422                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
11423                    self.advance();
11424                    self.expect(&Token::FatArrow)?;
11425                    let t = self.parse_assign_expr()?;
11426                    return Ok((parts, Some(t)));
11427                }
11428            }
11429            parts.push(self.parse_assign_expr()?);
11430        }
11431        Ok((parts, None))
11432    }
11433
11434    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
11435    fn parse_init_block_then_list_optional_progress(
11436        &mut self,
11437    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
11438        let init = self.parse_assign_expr()?;
11439        self.expect(&Token::Comma)?;
11440        let block = self.parse_block_or_bareword_block()?;
11441        self.eat(&Token::Comma);
11442        let line = self.peek_line();
11443        if let Token::Ident(ref kw) = self.peek().clone() {
11444            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11445                self.advance();
11446                self.expect(&Token::FatArrow)?;
11447                let prog = self.parse_assign_expr()?;
11448                return Ok((
11449                    init,
11450                    block,
11451                    Expr {
11452                        kind: ExprKind::List(vec![]),
11453                        line,
11454                    },
11455                    Some(prog),
11456                ));
11457            }
11458        }
11459        if matches!(
11460            self.peek(),
11461            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11462        ) {
11463            return Ok((
11464                init,
11465                block,
11466                Expr {
11467                    kind: ExprKind::List(vec![]),
11468                    line,
11469                },
11470                None,
11471            ));
11472        }
11473        let mut parts = vec![self.parse_assign_expr()?];
11474        loop {
11475            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11476                break;
11477            }
11478            if matches!(
11479                self.peek(),
11480                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11481            ) {
11482                break;
11483            }
11484            if self.peek_is_postfix_stmt_modifier_keyword() {
11485                break;
11486            }
11487            if let Token::Ident(ref kw) = self.peek().clone() {
11488                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11489                    self.advance();
11490                    self.expect(&Token::FatArrow)?;
11491                    let prog = self.parse_assign_expr()?;
11492                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
11493                }
11494            }
11495            parts.push(self.parse_assign_expr()?);
11496        }
11497        Ok((init, block, merge_expr_list(parts), None))
11498    }
11499
11500    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
11501    fn parse_cluster_block_then_list_optional_progress(
11502        &mut self,
11503    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
11504        let cluster = self.parse_assign_expr()?;
11505        let block = self.parse_block_or_bareword_block()?;
11506        self.eat(&Token::Comma);
11507        let line = self.peek_line();
11508        if let Token::Ident(ref kw) = self.peek().clone() {
11509            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11510                self.advance();
11511                self.expect(&Token::FatArrow)?;
11512                let prog = self.parse_assign_expr_stop_at_pipe()?;
11513                return Ok((
11514                    cluster,
11515                    block,
11516                    Expr {
11517                        kind: ExprKind::List(vec![]),
11518                        line,
11519                    },
11520                    Some(prog),
11521                ));
11522            }
11523        }
11524        let empty_list_ok = matches!(
11525            self.peek(),
11526            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11527        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
11528        if empty_list_ok {
11529            return Ok((
11530                cluster,
11531                block,
11532                Expr {
11533                    kind: ExprKind::List(vec![]),
11534                    line,
11535                },
11536                None,
11537            ));
11538        }
11539        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11540        loop {
11541            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11542                break;
11543            }
11544            if matches!(
11545                self.peek(),
11546                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11547            ) {
11548                break;
11549            }
11550            if self.peek_is_postfix_stmt_modifier_keyword() {
11551                break;
11552            }
11553            if let Token::Ident(ref kw) = self.peek().clone() {
11554                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11555                    self.advance();
11556                    self.expect(&Token::FatArrow)?;
11557                    let prog = self.parse_assign_expr_stop_at_pipe()?;
11558                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
11559                }
11560            }
11561            parts.push(self.parse_assign_expr_stop_at_pipe()?);
11562        }
11563        Ok((cluster, block, merge_expr_list(parts), None))
11564    }
11565
11566    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
11567    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
11568    ///
11569    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
11570    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
11571    /// stage — individual list parts and the progress value parse through
11572    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
11573    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
11574    fn parse_block_then_list_optional_progress(
11575        &mut self,
11576    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
11577        let block = self.parse_block_or_bareword_block()?;
11578        self.eat(&Token::Comma);
11579        let line = self.peek_line();
11580        if let Token::Ident(ref kw) = self.peek().clone() {
11581            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11582                self.advance();
11583                self.expect(&Token::FatArrow)?;
11584                let prog = self.parse_assign_expr_stop_at_pipe()?;
11585                return Ok((
11586                    block,
11587                    Expr {
11588                        kind: ExprKind::List(vec![]),
11589                        line,
11590                    },
11591                    Some(prog),
11592                ));
11593            }
11594        }
11595        // An empty list operand is allowed when the next token terminates the
11596        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
11597        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
11598        // terminator — left-associative chaining leaves the outer `|>` for
11599        // the enclosing pipe-forward loop.
11600        let empty_list_ok = matches!(
11601            self.peek(),
11602            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11603        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
11604        if empty_list_ok {
11605            return Ok((
11606                block,
11607                Expr {
11608                    kind: ExprKind::List(vec![]),
11609                    line,
11610                },
11611                None,
11612            ));
11613        }
11614        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11615        loop {
11616            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11617                break;
11618            }
11619            if matches!(
11620                self.peek(),
11621                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11622            ) {
11623                break;
11624            }
11625            if self.peek_is_postfix_stmt_modifier_keyword() {
11626                break;
11627            }
11628            if let Token::Ident(ref kw) = self.peek().clone() {
11629                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11630                    self.advance();
11631                    self.expect(&Token::FatArrow)?;
11632                    let prog = self.parse_assign_expr_stop_at_pipe()?;
11633                    return Ok((block, merge_expr_list(parts), Some(prog)));
11634                }
11635            }
11636            parts.push(self.parse_assign_expr_stop_at_pipe()?);
11637        }
11638        Ok((block, merge_expr_list(parts), None))
11639    }
11640
11641    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
11642    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
11643        // `fan { BLOCK }` — no count
11644        if matches!(self.peek(), Token::LBrace) {
11645            let block = self.parse_block()?;
11646            return Ok((None, block));
11647        }
11648        let saved = self.pos;
11649        // Not a brace — first expr could be count or body
11650        let first = self.parse_postfix()?;
11651        if matches!(self.peek(), Token::LBrace) {
11652            // `fan COUNT { BLOCK }`
11653            let block = self.parse_block()?;
11654            Ok((Some(Box::new(first)), block))
11655        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
11656            || (matches!(self.peek(), Token::Comma)
11657                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
11658        {
11659            // `fan EXPR;` — no count, first is the body
11660            let block = self.bareword_to_no_arg_block(first);
11661            Ok((None, block))
11662        } else if matches!(first.kind, ExprKind::Integer(_)) {
11663            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
11664            self.eat(&Token::Comma);
11665            let body = self.parse_fan_blockless_body(line)?;
11666            Ok((Some(Box::new(first)), body))
11667        } else {
11668            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
11669            // — backtrack and re-parse as a full body expression.
11670            self.pos = saved;
11671            let body = self.parse_fan_blockless_body(line)?;
11672            Ok((None, body))
11673        }
11674    }
11675
11676    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
11677    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
11678        if matches!(self.peek(), Token::LBrace) {
11679            return self.parse_block();
11680        }
11681        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
11682        if let Token::Ident(ref name) = self.peek().clone() {
11683            if matches!(
11684                self.peek_at(1),
11685                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11686            ) {
11687                let name = name.clone();
11688                self.advance();
11689                let body = Expr {
11690                    kind: ExprKind::FuncCall { name, args: vec![] },
11691                    line,
11692                };
11693                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11694            }
11695        }
11696        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
11697        let expr = self.parse_assign_expr_stop_at_pipe()?;
11698        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11699    }
11700
11701    /// Wrap a parsed expression as a single-statement block, converting bare
11702    /// identifiers to zero-arg calls (`work` → `work()`).
11703    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
11704        let line = expr.line;
11705        let body = match &expr.kind {
11706            ExprKind::Bareword(name) => Expr {
11707                kind: ExprKind::FuncCall {
11708                    name: name.clone(),
11709                    args: vec![],
11710                },
11711                line,
11712            },
11713            _ => expr,
11714        };
11715        vec![Statement::new(StmtKind::Expression(body), line)]
11716    }
11717
11718    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
11719    ///
11720    /// When the next token is `{`, delegates to [`Self::parse_block`].
11721    /// Otherwise parses a single postfix expression and wraps it as a call
11722    /// with `$_` as argument (for barewords) or a plain expression statement:
11723    ///
11724    /// - Bareword `foo` → `{ foo($_) }`
11725    /// - Other expr     → `{ EXPR }`
11726    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
11727        if matches!(self.peek(), Token::LBrace) {
11728            return self.parse_block();
11729        }
11730        let line = self.peek_line();
11731        // A lone identifier followed by a list-terminator is a bare sub name:
11732        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
11733        if let Token::Ident(ref name) = self.peek().clone() {
11734            if matches!(
11735                self.peek_at(1),
11736                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11737            ) {
11738                let name = name.clone();
11739                self.advance();
11740                let body = Expr {
11741                    kind: ExprKind::FuncCall {
11742                        name,
11743                        args: vec![Expr {
11744                            kind: ExprKind::ScalarVar("_".to_string()),
11745                            line,
11746                        }],
11747                    },
11748                    line,
11749                };
11750                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11751            }
11752        }
11753        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
11754        let expr = self.parse_assign_expr_stop_at_pipe()?;
11755        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11756    }
11757
11758    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
11759    /// bare function takes no args (body runs stand-alone, not per-element).
11760    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
11761    /// greedily swallow subsequent tokens as function arguments.
11762    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
11763        if matches!(self.peek(), Token::LBrace) {
11764            return self.parse_block();
11765        }
11766        let line = self.peek_line();
11767        if let Token::Ident(ref name) = self.peek().clone() {
11768            if matches!(
11769                self.peek_at(1),
11770                Token::Comma
11771                    | Token::Semicolon
11772                    | Token::RBrace
11773                    | Token::Eof
11774                    | Token::PipeForward
11775                    | Token::Integer(_)
11776            ) {
11777                let name = name.clone();
11778                self.advance();
11779                let body = Expr {
11780                    kind: ExprKind::FuncCall { name, args: vec![] },
11781                    line,
11782                };
11783                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11784            }
11785        }
11786        let expr = self.parse_postfix()?;
11787        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11788    }
11789
11790    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
11791    /// treated as a bare sub name (e.g. inside `sort`).
11792    /// True for any bareword the parser treats as a known builtin / keyword —
11793    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
11794    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
11795    /// as a comparator name if it *isn't* a known bareword). Previously named
11796    /// `is_perl_keyword`, which was misleading.
11797    fn is_known_bareword(name: &str) -> bool {
11798        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
11799    }
11800
11801    /// True iff `name` appears as any spelling (primary *or* alias) in a
11802    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
11803    /// up in the parser-level keyword lists but are still callable at
11804    /// runtime — so `map { tj }` can default to `tj($_)` the same way
11805    /// `map { to_json }` does.
11806    fn is_try_builtin_name(name: &str) -> bool {
11807        crate::builtins::BUILTIN_ARMS
11808            .iter()
11809            .any(|arm| arm.contains(&name))
11810    }
11811
11812    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
11813    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
11814    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
11815    /// is derived from this list by `build.rs`.
11816    fn is_perl5_core(name: &str) -> bool {
11817        matches!(
11818            name,
11819            // ── array / list ────────────────────────────────────────────
11820            "map" | "grep" | "sort" | "reverse" | "join" | "split"
11821            | "push" | "pop" | "shift" | "unshift" | "splice"
11822            | "pack" | "unpack"
11823            // ── hash ────────────────────────────────────────────────────
11824            | "keys" | "values" | "each"
11825            // ── string ──────────────────────────────────────────────────
11826            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
11827            | "lc" | "uc" | "lcfirst" | "ucfirst"
11828            | "length" | "substr" | "index" | "rindex"
11829            | "sprintf" | "printf" | "print" | "say"
11830            | "pos" | "quotemeta" | "study"
11831            // ── numeric ─────────────────────────────────────────────────
11832            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
11833            | "exp" | "log" | "rand" | "srand"
11834            // ── time ────────────────────────────────────────────────────
11835            | "time" | "localtime" | "gmtime"
11836            // ── type / reflection ───────────────────────────────────────
11837            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
11838            | "caller" | "delete" | "exists" | "bless" | "prototype"
11839            | "tie" | "untie" | "tied"
11840            // ── io ──────────────────────────────────────────────────────
11841            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
11842            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
11843            | "format" | "formline" | "select" | "vec"
11844            | "sysopen" | "sysread" | "sysseek" | "syswrite"
11845            // ── filesystem ──────────────────────────────────────────────
11846            | "stat" | "lstat" | "rename" | "unlink" | "utime"
11847            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
11848            | "glob" | "opendir" | "readdir" | "closedir"
11849            | "link" | "readlink" | "symlink"
11850            // ── ipc ─────────────────────────────────────────────────────
11851            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
11852            // ── sysv ipc ────────────────────────────────────────────────
11853            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
11854            | "semctl" | "semget" | "semop"
11855            | "shmctl" | "shmget" | "shmread" | "shmwrite"
11856            // ── process / system ────────────────────────────────────────
11857            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
11858            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
11859            | "chroot" | "times" | "umask" | "reset"
11860            | "getpgrp" | "setpgrp" | "getppid"
11861            | "getpriority" | "setpriority"
11862            // ── socket ──────────────────────────────────────────────────
11863            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
11864            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
11865            | "getpeername" | "getsockname"
11866            // ── posix metadata ──────────────────────────────────────────
11867            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
11868            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
11869            | "getlogin"
11870            | "gethostbyname" | "gethostbyaddr" | "gethostent"
11871            | "getnetbyname" | "getnetent"
11872            | "getprotobyname" | "getprotoent"
11873            | "getservbyname" | "getservent"
11874            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
11875            | "endpwent" | "endgrent"
11876            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
11877            // ── control flow ────────────────────────────────────────────
11878            | "return" | "do" | "eval" | "require"
11879            | "my" | "our" | "local" | "use" | "no"
11880            | "sub" | "if" | "unless" | "while" | "until"
11881            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
11882            | "not" | "and" | "or"
11883            // ── quoting ─────────────────────────────────────────────────
11884            | "qw" | "qq" | "q"
11885            // ── phase blocks ────────────────────────────────────────────
11886            | "BEGIN" | "END"
11887        )
11888    }
11889
11890    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
11891    /// Used by `--compat` to reject extensions at parse time.
11892    fn stryke_extension_name(name: &str) -> Option<&str> {
11893        match name {
11894            // ── parallel ────────────────────────────────────────────────────
11895            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
11896            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
11897            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
11898            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
11899            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
11900            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
11901            | "pmaps" | "pflat_maps" | "pgreps"
11902            // ── functional / iterator ───────────────────────────────────────
11903            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
11904            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
11905            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "flatten" | "set"
11906            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
11907            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
11908            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
11909            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
11910            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
11911            | "zip_with" | "count_by" | "skip" | "first_or"
11912            // ── pipeline / string helpers ───────────────────────────────────
11913            | "input" | "lines" | "words" | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
11914            | "punctuation" | "punct"
11915            | "sentences" | "sents"
11916            | "paragraphs" | "paras" | "sections" | "sects"
11917            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
11918            | "trim" | "avg" | "stddev"
11919            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
11920            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
11921            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
11922            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
11923            | "to_html" | "to_markdown" | "to_table" | "xopen"
11924            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
11925            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
11926            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
11927            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
11928            | "to_hash" | "to_set"
11929            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
11930            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
11931            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
11932            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
11933            | "inc" | "dec" | "elapsed"
11934            // ── filesystem extensions ───────────────────────────────────────
11935            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
11936            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
11937            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
11938            | "copy" | "move" | "spurt" | "read_bytes" | "which"
11939            | "getcwd" | "touch" | "gethostname" | "uname"
11940            // ── data / network ──────────────────────────────────────────────
11941            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
11942            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
11943            | "par_fetch" | "par_csv_read" | "par_pipeline"
11944            | "json_encode" | "json_decode" | "json_jq"
11945            | "http_request" | "serve" | "ssh"
11946            | "html_parse" | "css_select" | "xml_parse" | "xpath"
11947            | "smtp_send"
11948            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
11949            | "net_public_ip" | "net_dns" | "net_reverse_dns"
11950            | "net_ping" | "net_port_open" | "net_ports_scan"
11951            | "net_latency" | "net_download" | "net_headers"
11952            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
11953            // ── git ─────────────────────────────────────────────────────────
11954            | "git_log" | "git_status" | "git_diff" | "git_branches"
11955            | "git_tags" | "git_blame" | "git_authors" | "git_files"
11956            | "git_show" | "git_root"
11957            // ── audio / media ───────────────────────────────────────────────
11958            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
11959            // ── pdf ─────────────────────────────────────────────────────────
11960            | "to_pdf" | "pdf_text" | "pdf_pages"
11961            // ── serialization (stryke-only encoders) ────────────────────────
11962            | "toml_encode" | "toml_decode"
11963            | "yaml_encode" | "yaml_decode"
11964            | "xml_encode" | "xml_decode"
11965            // ── crypto / encoding ───────────────────────────────────────────
11966            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
11967            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
11968            | "shake128" | "shake256"
11969            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
11970            | "uuid" | "crc32"
11971            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
11972            | "ripemd160" | "rmd160" | "md4"
11973            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
11974            | "murmur3" | "murmur3_32" | "murmur3_128"
11975            | "siphash" | "siphash_keyed"
11976            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
11977            | "poly1305" | "poly1305_mac"
11978            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
11979            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
11980            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
11981            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
11982            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
11983            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
11984            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
11985            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
11986            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
11987            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
11988            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
11989            | "secretbox" | "secretbox_seal" | "secretbox_open"
11990            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
11991            | "nacl_box_open" | "box_open"
11992            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
11993            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
11994            | "barcode_ean13" | "ean13" | "barcode_svg"
11995            | "argon2_hash" | "argon2" | "argon2_verify"
11996            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
11997            | "scrypt_hash" | "scrypt" | "scrypt_verify"
11998            | "pbkdf2" | "pbkdf2_derive"
11999            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
12000            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
12001            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
12002            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
12003            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
12004            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
12005            | "ecdsa_p256_verify" | "p256_verify"
12006            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
12007            | "ecdsa_p384_verify" | "p384_verify"
12008            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
12009            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
12010            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
12011            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
12012            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
12013            | "ed25519_verify" | "ed_verify"
12014            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
12015            | "base64_encode" | "base64_decode"
12016            | "hex_encode" | "hex_decode"
12017            | "url_encode" | "url_decode"
12018            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
12019            | "brotli" | "br" | "brotli_decode" | "ubr"
12020            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
12021            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
12022            | "lz4" | "lz4_decode" | "unlz4"
12023            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
12024            | "lzw" | "lzw_decode" | "unlzw"
12025            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
12026            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
12027            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
12028            // ── special math functions ────────────────────────────────────────
12029            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
12030            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
12031            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
12032            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
12033            | "gammaincc_reg" | "gamma_ur"
12034            // ── date / time ─────────────────────────────────────────────────
12035            | "datetime_utc" | "datetime_now_tz"
12036            | "datetime_format_tz" | "datetime_add_seconds"
12037            | "datetime_from_epoch"
12038            | "datetime_parse_rfc3339" | "datetime_parse_local"
12039            | "datetime_strftime"
12040            | "dateseq" | "dategrep" | "dateround" | "datesort"
12041            // ── jwt ─────────────────────────────────────────────────────────
12042            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
12043            // ── logging ─────────────────────────────────────────────────────
12044            | "log_info" | "log_warn" | "log_error"
12045            | "log_debug" | "log_trace" | "log_json" | "log_level"
12046            // ── concurrency / timing ────────────────────────────────────────
12047            | "async" | "spawn" | "trace" | "timer" | "bench"
12048            | "eval_timeout" | "retry" | "rate_limit" | "every"
12049            | "gen" | "watch"
12050            // ── testing framework ────────────────────────────────────────────
12051            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
12052            | "assert_true" | "assert_false"
12053            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
12054            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
12055            | "test_run"
12056            // ── system info ─────────────────────────────────────────────────
12057            | "mounts" | "du" | "du_tree" | "process_list"
12058            | "thread_count" | "pool_info" | "par_bench"
12059            // ── I/O extensions ──────────────────────────────────────────────
12060            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
12061            | "stdin"
12062            // ── internal ────────────────────────────────────────────────────
12063            | "__stryke_rust_compile"
12064            // ── short aliases ───────────────────────────────────────────────
12065            | "p" | "rev"
12066            // ── trivial numeric / predicate builtins ────────────────────────
12067            | "even" | "odd" | "zero" | "nonzero"
12068            | "positive" | "pos_n" | "negative" | "neg_n"
12069            | "sign" | "negate" | "double" | "triple" | "half"
12070            | "identity" | "id"
12071            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
12072            | "gcd" | "lcm" | "min2" | "max2"
12073            | "log2" | "log10" | "hypot"
12074            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
12075            | "pow2" | "abs_diff"
12076            | "factorial" | "fact" | "fibonacci" | "fib"
12077            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
12078            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
12079            | "median" | "mode_val" | "variance"
12080            // ── trivial string ops ──────────────────────────────────────────
12081            | "is_empty" | "is_blank" | "is_numeric"
12082            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
12083            | "is_space" | "is_whitespace"
12084            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
12085            | "capitalize" | "cap" | "swap_case" | "repeat"
12086            | "title_case" | "title" | "squish"
12087            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
12088            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
12089            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
12090            // ── trivial type predicates ─────────────────────────────────────
12091            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
12092            | "is_code" | "is_coderef" | "is_ref"
12093            | "is_undef" | "is_defined" | "is_def"
12094            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
12095            // ── hash helpers ────────────────────────────────────────────────
12096            | "invert" | "merge_hash"
12097            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
12098            // ── boolean combinators ─────────────────────────────────────────
12099            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
12100            // ── collection helpers (trivial) ────────────────────────────────
12101            | "riffle" | "intersperse" | "every_nth"
12102            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
12103            // ── base conversion ─────────────────────────────────────────────
12104            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
12105            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
12106            | "bits_count" | "popcount" | "leading_zeros" | "lz"
12107            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
12108            // ── bit ops ─────────────────────────────────────────────────────
12109            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
12110            | "shift_left" | "shl" | "shift_right" | "shr"
12111            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
12112            // ── unit conversions: temperature ───────────────────────────────
12113            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
12114            // ── unit conversions: distance ──────────────────────────────────
12115            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
12116            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
12117            | "yards_to_m" | "m_to_yards"
12118            // ── unit conversions: mass ──────────────────────────────────────
12119            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
12120            | "stone_to_kg" | "kg_to_stone"
12121            // ── unit conversions: digital ───────────────────────────────────
12122            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
12123            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
12124            | "kb_to_mb" | "mb_to_gb"
12125            | "bits_to_bytes" | "bytes_to_bits"
12126            // ── unit conversions: time ──────────────────────────────────────
12127            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
12128            | "seconds_to_hours" | "hours_to_seconds"
12129            | "seconds_to_days" | "days_to_seconds"
12130            | "minutes_to_hours" | "hours_to_minutes"
12131            | "hours_to_days" | "days_to_hours"
12132            // ── date helpers ────────────────────────────────────────────────
12133            | "is_leap_year" | "is_leap" | "days_in_month"
12134            | "month_name" | "month_short"
12135            | "weekday_name" | "weekday_short" | "quarter_of"
12136            // ── now / timestamp ─────────────────────────────────────────────
12137            | "now_ms" | "now_us" | "now_ns"
12138            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
12139            // ── color / ANSI ────────────────────────────────────────────────
12140            | "rgb_to_hex" | "hex_to_rgb"
12141            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
12142            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
12143            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
12144            | "strip_ansi"
12145            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
12146            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
12147            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
12148            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
12149            | "bright_magenta" | "bright_cyan" | "bright_white"
12150            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
12151            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
12152            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
12153            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
12154            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
12155            | "white_bold" | "bold_white"
12156            | "blink" | "rapid_blink" | "hidden" | "overline"
12157            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
12158            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
12159            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
12160            // ── network / validation ────────────────────────────────────────
12161            | "ipv4_to_int" | "int_to_ipv4"
12162            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
12163            // ── path helpers ────────────────────────────────────────────────
12164            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
12165            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
12166            // ── functional primitives ───────────────────────────────────────
12167            | "const_fn" | "always_true" | "always_false"
12168            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
12169            // ── more list helpers ───────────────────────────────────────────
12170            | "count_eq" | "count_ne" | "all_eq"
12171            | "all_distinct" | "all_unique" | "has_duplicates"
12172            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
12173            // ── string quote / escape ───────────────────────────────────────
12174            | "quote" | "single_quote" | "unquote"
12175            | "extract_between" | "ellipsis"
12176            // ── random ──────────────────────────────────────────────────────
12177            | "coin_flip" | "dice_roll"
12178            | "random_int" | "random_float" | "random_bool"
12179            | "random_choice" | "random_between"
12180            | "random_string" | "random_alpha" | "random_digit"
12181            // ── system introspection ────────────────────────────────────────
12182            | "os_name" | "os_arch" | "num_cpus"
12183            | "pid" | "ppid" | "uid" | "gid"
12184            | "username" | "home_dir" | "temp_dir"
12185            | "mem_total" | "mem_free" | "mem_used"
12186            | "swap_total" | "swap_free" | "swap_used"
12187            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
12188            | "load_avg" | "sys_uptime" | "page_size"
12189            | "os_version" | "os_family" | "endianness" | "pointer_width"
12190            | "proc_mem" | "rss"
12191            // ── collection more ─────────────────────────────────────────────
12192            | "transpose" | "unzip"
12193            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
12194            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
12195            // ── trig / math (batch 2) ───────────────────────────────────────
12196            | "tan" | "asin" | "acos" | "atan"
12197            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
12198            | "sqr" | "cube_fn"
12199            | "mod_op" | "ceil_div" | "floor_div"
12200            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
12201            | "degrees" | "radians"
12202            | "min_abs" | "max_abs"
12203            | "saturate" | "sat01" | "wrap_around"
12204            // ── string (batch 2) ────────────────────────────────────────────
12205            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
12206            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
12207            | "first_word" | "last_word"
12208            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
12209            | "lowercase" | "uppercase"
12210            | "pascal_case" | "pc_case"
12211            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
12212            | "is_palindrome" | "hamming_distance"
12213            | "longest_common_prefix" | "lcp"
12214            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
12215            | "replace_first" | "replace_all_str"
12216            | "contains_any" | "contains_all"
12217            | "starts_with_any" | "ends_with_any"
12218            // ── predicates (batch 2) ────────────────────────────────────────
12219            | "is_pair" | "is_triple"
12220            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
12221            | "is_empty_arr" | "is_empty_hash"
12222            | "is_subset" | "is_superset" | "is_permutation"
12223            // ── collection (batch 2) ────────────────────────────────────────
12224            | "first_eq" | "last_eq"
12225            | "index_of" | "last_index_of" | "positions_of"
12226            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
12227            | "distinct_count" | "longest" | "shortest"
12228            | "array_union" | "list_union"
12229            | "array_intersection" | "list_intersection"
12230            | "array_difference" | "list_difference"
12231            | "symmetric_diff" | "group_of_n" | "chunk_n"
12232            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
12233            // ── hash ops (batch 2) ──────────────────────────────────────────
12234            | "pick_keys" | "pick" | "omit_keys" | "omit"
12235            | "map_keys_fn" | "map_values_fn"
12236            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
12237            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
12238            // ── date (batch 2) ──────────────────────────────────────────────
12239            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
12240            // ── json helpers ────────────────────────────────────────────────
12241            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
12242            // ── process / env ───────────────────────────────────────────────
12243            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
12244            | "argc" | "script_name"
12245            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
12246            // ── id helpers ──────────────────────────────────────────────────
12247            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
12248            // ── url / email parts ───────────────────────────────────────────
12249            | "email_domain" | "email_local"
12250            | "url_host" | "url_path" | "url_query" | "url_scheme"
12251            // ── file stat / path ────────────────────────────────────────────
12252            | "file_size" | "fsize" | "file_mtime" | "mtime"
12253            | "file_atime" | "atime" | "file_ctime" | "ctime"
12254            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
12255            | "path_is_abs" | "path_is_rel"
12256            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
12257            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
12258            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
12259            | "reverse_list" | "list_reverse"
12260            | "without" | "without_nth" | "take_last" | "drop_last"
12261            | "pairwise" | "zipmap"
12262            | "format_bytes" | "human_bytes"
12263            | "format_duration" | "human_duration"
12264            | "format_number" | "group_number"
12265            | "format_percent" | "pad_number"
12266            | "spaceship" | "cmp_num" | "cmp_str"
12267            | "compare_versions" | "version_cmp"
12268            | "hash_insert" | "hash_update" | "hash_delete"
12269            | "matches_regex" | "re_match"
12270            | "count_regex_matches" | "regex_extract"
12271            | "regex_split_str" | "regex_replace_str"
12272            | "shuffle_chars" | "random_char" | "nth_word"
12273            | "head_lines" | "tail_lines" | "count_substring"
12274            | "is_valid_hex" | "hex_upper" | "hex_lower"
12275            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
12276            | "us_to_ns" | "ns_to_us"
12277            | "liters_to_gallons" | "gallons_to_liters"
12278            | "liters_to_ml" | "ml_to_liters"
12279            | "cups_to_ml" | "ml_to_cups"
12280            | "newtons_to_lbf" | "lbf_to_newtons"
12281            | "joules_to_cal" | "cal_to_joules"
12282            | "watts_to_hp" | "hp_to_watts"
12283            | "pascals_to_psi" | "psi_to_pascals"
12284            | "bar_to_pascals" | "pascals_to_bar"
12285            // ── algebraic match ─────────────────────────────────────────────
12286            | "match"
12287            // ── clojure stdlib (only names not matched above) ─────────────────
12288            | "fst" | "rest" | "rst" | "second" | "snd"
12289            | "last_clj" | "lastc" | "butlast" | "bl"
12290            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
12291            | "cons" | "conj"
12292            | "peek_clj" | "pkc" | "pop_clj" | "popc"
12293            | "some" | "not_any" | "not_every"
12294            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
12295            | "fnil" | "juxt"
12296            | "memoize" | "memo" | "curry" | "once"
12297            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
12298            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
12299            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
12300            | "reductions" | "rdcs"
12301            | "partition_by" | "pby" | "partition_all" | "pall"
12302            | "split_at" | "spat" | "split_with" | "spw"
12303            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
12304            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
12305            | "apply" | "appl"
12306            // ── python/ruby stdlib ───────────────────────────────────────────
12307            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
12308            | "zip_longest" | "zipl" | "combinations" | "comb" | "permutations" | "perm"
12309            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
12310            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
12311            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
12312            | "each_slice" | "eslice" | "each_cons" | "econs"
12313            | "one" | "none_match" | "nonem"
12314            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
12315            | "minmax" | "mmx" | "minmax_by" | "mmxb"
12316            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
12317            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
12318            | "sum_by" | "sumb" | "uniq_by" | "uqb"
12319            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
12320            | "step" | "upto" | "downto"
12321            // ── javascript array/object methods ─────────────────────────────
12322            | "find_last" | "fndl" | "find_last_index" | "fndli"
12323            | "at_index" | "ati" | "replace_at" | "repa"
12324            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
12325            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
12326            | "object_keys" | "okeys" | "object_values" | "ovals"
12327            | "object_entries" | "oents" | "object_from_entries" | "ofents"
12328            // ── haskell list functions ──────────────────────────────────────
12329            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
12330            | "nub" | "sort_on" | "srton"
12331            | "intersperse_val" | "isp" | "intercalate" | "ical"
12332            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
12333            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
12334            // ── rust iterator methods ───────────────────────────────────────
12335            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
12336            | "partition_either" | "peith" | "try_fold" | "tfld"
12337            | "map_while" | "mapw" | "inspect" | "insp"
12338            // ── ruby enumerable extras ──────────────────────────────────────
12339            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
12340            // ── go/general functional utilities ─────────────────────────────
12341            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
12342            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
12343            | "lines_from" | "lfrm" | "unlines" | "unlns"
12344            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
12345            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
12346            | "interpose" | "ipos" | "partition_n" | "partn"
12347            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
12348            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
12349            // ── additional missing stdlib functions ─────────────────────────
12350            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
12351            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
12352            | "each_with_object" | "ewo" | "reduce_right" | "redr"
12353            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
12354            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
12355            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
12356            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
12357            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
12358            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
12359            | "union_list" | "unionl" | "intersect_list" | "intl"
12360            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
12361            // ── Extended stdlib: Text Processing ─────────────────────────────
12362            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
12363            | "split_regex" | "splre" | "replace_regex" | "replre"
12364            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
12365            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
12366            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
12367            | "pluralize" | "plur" | "ordinalize" | "ordn"
12368            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
12369            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
12370            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
12371            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
12372            // ── Extended stdlib: Advanced Numeric ────────────────────────────
12373            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
12374            | "dot_product" | "dotp" | "cross_product" | "crossp"
12375            | "matrix_mul" | "matmul" | "mm"
12376            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
12377            | "distance" | "dist" | "manhattan_distance" | "mdist"
12378            | "covariance" | "cov" | "correlation" | "corr"
12379            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
12380            | "in_range" | "inrng" | "wrap_range" | "wrprng"
12381            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
12382            // ── Extended stdlib: Date/Time ───────────────────────────────────
12383            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
12384            | "diff_days" | "diffd" | "diff_hours" | "diffh"
12385            | "start_of_day" | "sod" | "end_of_day" | "eod"
12386            | "start_of_hour" | "soh" | "start_of_minute" | "som"
12387            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
12388            | "urle" | "urld"
12389            | "html_encode" | "htmle" | "html_decode" | "htmld"
12390            | "adler32" | "adl32" | "fnv1a" | "djb2"
12391            // ── Extended stdlib: Validation ──────────────────────────────────
12392            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
12393            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
12394            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
12395            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
12396            // ── Extended stdlib: Collection Advanced ─────────────────────────
12397            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
12398            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
12399            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
12400            | "partition_point" | "ppt" | "lower_bound" | "lbound"
12401            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
12402            // ── Extended stdlib: Matrix Operations ───────────────────────────
12403            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
12404            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
12405            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
12406            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
12407            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
12408            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
12409            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
12410            // ── Extended stdlib: Graph Algorithms ────────────────────────────
12411            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
12412            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
12413            | "connected_components_graph" | "ccgraph"
12414            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
12415            // ── Extended stdlib: Data Validation ─────────────────────────────
12416            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
12417            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
12418            | "is_hostname_valid" | "ishost"
12419            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
12420            | "is_iso_datetime" | "isisodtm"
12421            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
12422            // ── Extended stdlib: String Utilities Novel ──────────────────────
12423            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
12424            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
12425            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
12426            | "find_all_indices" | "fndalli"
12427            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
12428            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
12429            // ── Extended stdlib: Math Novel ──────────────────────────────────
12430            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
12431            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
12432            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
12433            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
12434            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
12435            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
12436            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
12437            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
12438            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
12439            | "longest_run" | "lrun" | "longest_increasing" | "linc"
12440            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
12441            | "majority_element" | "majority" | "kth_largest" | "kthl"
12442            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
12443            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
12444            // ── Extended stdlib batch 3: Set Operations ──────────────────────
12445            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
12446            | "overlap_coefficient" | "overlapcoef"
12447            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
12448            // ── Extended stdlib batch 3: Advanced String ─────────────────────
12449            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
12450            | "hamdist" | "jaro_similarity" | "jarosim"
12451            | "longest_common_substring" | "lcsub"
12452            | "longest_common_subsequence" | "lcseq"
12453            | "count_words" | "wcount" | "count_lines" | "lcount"
12454            | "count_chars" | "ccount" | "count_bytes" | "bcount"
12455            // ── Extended stdlib batch 3: More Math ───────────────────────────
12456            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
12457            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
12458            | "mobius" | "mob" | "is_squarefree" | "issqfr"
12459            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
12460            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
12461            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
12462            | "day_of_year" | "doy" | "week_of_year" | "woy"
12463            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
12464            | "age_in_years" | "ageyrs"
12465            // ── functional combinators ──────────────────────────────────────
12466
12467            | "when_true" | "when_false" | "if_else" | "clamp_fn"
12468            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
12469            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
12470            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
12471            | "coalesce" | "default_to" | "fallback"
12472            | "apply_list" | "zip_apply" | "scan"
12473            | "keep_if" | "reject_if" | "group_consecutive"
12474            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
12475
12476            // ── matrix / linear algebra ─────────────────────────────────────
12477
12478
12479            | "matrix_multiply" | "mat_mul"
12480            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
12481
12482
12483
12484            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
12485            | "linspace" | "arange"
12486            // ── more regex ──────────────────────────────────────────────────
12487            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
12488            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
12489            // ── more process / system ───────────────────────────────────────
12490            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
12491            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
12492            // ── data structure helpers ───────────────────────────────────────
12493            | "stack_new" | "queue_new" | "lru_new"
12494            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
12495            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
12496            // ── trivial numeric helpers (batch 4) ─────────────────────────────
12497            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
12498            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
12499            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
12500            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
12501            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
12502            // ── math / physics constants ──────────────────────────────────────
12503            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
12504            | "planck" | "speed_of_light" | "sqrt2"
12505            // ── physics formulas ──────────────────────────────────────────────
12506            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
12507            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
12508            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
12509            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
12510            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
12511            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
12512            // ── math functions ────────────────────────────────────────────────
12513            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
12514            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
12515            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
12516            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
12517            | "sigmoid" | "signum" | "square_root"
12518            // ── sequences ─────────────────────────────────────────────────────
12519            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
12520            | "squares_seq" | "triangular_seq"
12521            // ── string helpers (batch 4) ──────────────────────────────────────
12522            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
12523            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
12524            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
12525            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
12526            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
12527            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
12528            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
12529            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
12530            | "xor_strings"
12531            // ── list helpers (batch 4) ─────────────────────────────────────────
12532            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
12533            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
12534            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
12535            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
12536            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
12537            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
12538            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
12539            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
12540            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
12541            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
12542            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
12543            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
12544            | "wrap_index" | "digits_of"
12545            // ── predicates (batch 4) ──────────────────────────────────────────
12546            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
12547            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
12548            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
12549            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
12550            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
12551            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
12552            // ── counters (batch 4) ────────────────────────────────────────────
12553            | "count_digits" | "count_letters" | "count_lower" | "count_match"
12554            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
12555            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
12556            | "truthy_count" | "undef_count"
12557            // ── conversion / utility (batch 4) ────────────────────────────────
12558            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
12559            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
12560            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
12561            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
12562            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
12563            | "range_exclusive" | "range_inclusive"
12564            // ── math / numeric (uncategorized batch) ────────────────────────────
12565            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
12566            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
12567            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
12568            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
12569            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
12570            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
12571            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
12572            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
12573            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
12574            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
12575            | "tribonacci" | "weighted_mean" | "winsorize"
12576            // ── statistics (extended) ─────────────────────────────────────────
12577            | "chi_square_stat" | "describe" | "five_number_summary"
12578            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
12579            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
12580            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
12581            | "z_score" | "z_scores"
12582            // ── number theory / primes ──────────────────────────────────────────
12583            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
12584            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
12585            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
12586            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
12587            // ── geometry / physics ──────────────────────────────────────────────
12588            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
12589            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
12590            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
12591            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
12592            // ── geometry (extended) ───────────────────────────────────────────
12593            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
12594            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
12595            | "frustum_volume" | "haversine_distance" | "line_intersection"
12596            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
12597            | "reflect_point" | "scale_point" | "sector_area"
12598            | "torus_surface" | "torus_volume" | "translate_point"
12599            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
12600            // ── constants ───────────────────────────────────────────────────────
12601            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
12602            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
12603            | "sol" | "tau"
12604            // ── finance ─────────────────────────────────────────────────────────
12605            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
12606            // ── finance (extended) ────────────────────────────────────────────
12607            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
12608            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
12609            | "discounted_payback" | "duration" | "irr"
12610            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
12611            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
12612            | "wacc" | "xirr"
12613            // ── string processing (uncategorized batch) ─────────────────────────
12614            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
12615            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
12616            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
12617            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
12618            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
12619            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
12620            // ── encoding / phonetics ────────────────────────────────────────────
12621            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
12622            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
12623            | "to_emoji_num"
12624            // ── roman numerals ──────────────────────────────────────────────────
12625            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
12626            // ── base / gray code ────────────────────────────────────────────────
12627            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
12628            // ── color operations ────────────────────────────────────────────────
12629            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
12630            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
12631            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
12632            | "rgb_to_hsl" | "rgb_to_hsv"
12633            // ── matrix operations (uncategorized batch) ─────────────────────────
12634            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
12635            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
12636            | "matrix_transpose"
12637            // ── array / list operations (uncategorized batch) ───────────────────
12638            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
12639            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
12640            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
12641            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
12642            | "zero_crossings"
12643            // ── DSP / signal (extended) ───────────────────────────────────────
12644            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
12645            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
12646            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
12647            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
12648            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
12649            // ── validation predicates (uncategorized batch) ─────────────────────
12650            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
12651            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
12652            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
12653            // ── algorithms / puzzles ────────────────────────────────────────────
12654            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
12655            | "sierpinski" | "tower_of_hanoi" | "truth_table"
12656            // ── misc / utility ──────────────────────────────────────────────────
12657            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
12658            // ── math formulas ───────────────────────────────────────────────────
12659            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
12660            | "geometric_series" | "stirling_approx"
12661            | "double_factorial" | "rising_factorial" | "falling_factorial"
12662            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
12663            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
12664            | "map_range"
12665            // ── physics formulas ────────────────────────────────────────────────
12666            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
12667            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
12668            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
12669            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
12670            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
12671            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
12672            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
12673            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
12674            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
12675            | "projectile_range" | "projectile_max_height" | "projectile_time"
12676            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
12677            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
12678            | "lens_power" | "thin_lens" | "magnification_lens"
12679            // ── math constants ──────────────────────────────────────────────────
12680            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
12681            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
12682            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
12683            // ── physics constants ───────────────────────────────────────────────
12684            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
12685            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
12686            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
12687            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
12688            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
12689            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
12690            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
12691            // ── linear algebra (extended) ──────────────────────────────────
12692            | "matrix_solve" | "msolve" | "solve"
12693            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
12694            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
12695            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
12696            | "matrix_pinv" | "mpinv" | "pinv"
12697            | "matrix_cholesky" | "mchol" | "cholesky"
12698            | "matrix_det_general" | "mdetg" | "det"
12699            // ── statistics tests (extended) ────────────────────────────────
12700            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
12701            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
12702            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
12703            | "confidence_interval" | "ci"
12704            // ── distributions (extended) ──────────────────────────────────
12705            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
12706            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
12707            | "t_pdf" | "tpdf" | "student_pdf"
12708            | "f_pdf" | "fpdf" | "fisher_pdf"
12709            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
12710            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
12711            | "pareto_pdf" | "paretopdf"
12712            // ── interpolation & curve fitting ─────────────────────────────
12713            | "lagrange_interp" | "lagrange" | "linterp"
12714            | "cubic_spline" | "cspline" | "spline"
12715            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
12716            // ── numerical integration & differentiation ───────────────────
12717            | "trapz" | "trapezoid" | "simpson" | "simps"
12718            | "numerical_diff" | "numdiff" | "diff_array"
12719            | "cumtrapz" | "cumulative_trapz"
12720            // ── optimization / root finding ────────────────────────────────
12721            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
12722            | "golden_section" | "golden" | "gss"
12723            // ── ODE solvers ───────────────────────────────────────────────
12724            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
12725            // ── graph algorithms (extended) ────────────────────────────────
12726            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
12727            | "floyd_warshall" | "floydwarshall" | "apsp"
12728            | "prim_mst" | "mst" | "prim"
12729            // ── trig extensions ───────────────────────────────────────────
12730            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
12731            // ── ML activation functions ───────────────────────────────────
12732            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
12733            | "silu" | "swish" | "mish" | "softplus"
12734            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
12735            // ── special functions ─────────────────────────────────────────
12736            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
12737            | "lambert_w" | "lambertw" | "productlog"
12738            // ── number theory (extended) ──────────────────────────────────
12739            | "mod_exp" | "modexp" | "powmod"
12740            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
12741            | "miller_rabin" | "millerrabin" | "is_probable_prime"
12742            // ── combinatorics (extended) ──────────────────────────────────
12743            | "derangements" | "stirling2" | "stirling_second"
12744            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
12745            // ── physics (new) ─────────────────────────────────────────────
12746            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
12747            // ── financial greeks & risk ───────────────────────────────────
12748            | "bs_delta" | "bsdelta" | "option_delta"
12749            | "bs_gamma" | "bsgamma" | "option_gamma"
12750            | "bs_vega" | "bsvega" | "option_vega"
12751            | "bs_theta" | "bstheta" | "option_theta"
12752            | "bs_rho" | "bsrho" | "option_rho"
12753            | "bond_duration" | "mac_duration"
12754            // ── DSP extensions ────────────────────────────────────────────
12755            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
12756            // ── encoding extensions ───────────────────────────────────────
12757            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
12758            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
12759            // ── R base: distributions ─────────────────────────────────────
12760            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
12761            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
12762            // ── R base: matrix ops ────────────────────────────────────────
12763            | "rbind" | "cbind"
12764            | "row_sums" | "rowSums" | "col_sums" | "colSums"
12765            | "row_means" | "rowMeans" | "col_means" | "colMeans"
12766            | "outer_product" | "outer" | "crossprod" | "tcrossprod"
12767            | "nrow" | "ncol" | "prop_table" | "proptable"
12768            // ── R base: vector ops ────────────────────────────────────────
12769            | "cummax" | "cummin" | "scale_vec" | "scale"
12770            | "which_fn" | "tabulate"
12771            | "duplicated" | "duped" | "rev_vec"
12772            | "seq_fn" | "rep_fn" | "rep"
12773            | "cut_bins" | "cut" | "find_interval" | "findInterval"
12774            | "ecdf_fn" | "ecdf" | "density_est" | "density"
12775            | "embed_ts" | "embed"
12776            // ── R base: stats tests ───────────────────────────────────────
12777            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
12778            | "wilcox_test" | "wilcox" | "mann_whitney"
12779            | "prop_test" | "proptest" | "binom_test" | "binomtest"
12780            // ── R base: apply / functional ────────────────────────────────
12781            | "sapply" | "tapply" | "do_call" | "docall"
12782            // ── R base: ML / clustering ───────────────────────────────────
12783            | "kmeans" | "prcomp" | "pca"
12784            // ── R base: random generators ─────────────────────────────────
12785            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
12786            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
12787            | "rweibull" | "rlnorm" | "rcauchy"
12788            // ── R base: quantile functions ────────────────────────────────
12789            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
12790            // ── R base: additional CDFs ───────────────────────────────────
12791            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
12792            // ── R base: additional PMFs ───────────────────────────────────
12793            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
12794            // ── R base: smoothing / interpolation ─────────────────────────
12795            | "lowess" | "loess" | "approx_fn" | "approx"
12796            // ── R base: linear models ─────────────────────────────────────
12797            | "lm_fit" | "lm"
12798            // ── R base: remaining quantiles ───────────────────────────────
12799            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
12800            | "qbinom" | "qpois"
12801            // ── R base: time series ───────────────────────────────────────
12802            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
12803            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
12804            // ── R base: regression diagnostics ────────────────────────────
12805            | "predict_lm" | "predict" | "confint_lm" | "confint"
12806            // ── R base: multivariate stats ────────────────────────────────
12807            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
12808            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
12809            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
12810            // ── SVG plotting ──────────────────────────────────────────────
12811            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
12812            | "plot_svg" | "hist_svg" | "histogram_svg"
12813            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
12814            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
12815            | "donut_svg" | "donut" | "area_svg" | "area_chart"
12816            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
12817            | "candlestick_svg" | "candlestick" | "ohlc"
12818            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
12819            | "stacked_bar_svg" | "stacked_bar"
12820            | "wordcloud_svg" | "wordcloud" | "wcloud"
12821            | "treemap_svg" | "treemap"
12822            | "pvw"
12823            // ── Cyberpunk terminal art ────────────────────────────────
12824            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
12825            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
12826            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
12827            => Some(name),
12828            _ => None,
12829        }
12830    }
12831
12832    /// Reserved hash names that cannot be shadowed by user declarations.
12833    /// These are stryke's reflection hashes populated from builtins metadata.
12834    fn is_reserved_hash_name(name: &str) -> bool {
12835        matches!(
12836            name,
12837            "b" | "pc"
12838                | "e"
12839                | "a"
12840                | "d"
12841                | "c"
12842                | "p"
12843                | "all"
12844                | "stryke::builtins"
12845                | "stryke::perl_compats"
12846                | "stryke::extensions"
12847                | "stryke::aliases"
12848                | "stryke::descriptions"
12849                | "stryke::categories"
12850                | "stryke::primaries"
12851                | "stryke::all"
12852        )
12853    }
12854
12855    /// Check if a UDF name shadows a stryke builtin and error if so.
12856    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
12857    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> PerlResult<()> {
12858        if Self::is_known_bareword(name) || Self::is_try_builtin_name(name) {
12859            return Err(self.syntax_err(
12860                format!(
12861"`{name}` is a stryke builtin and cannot be redefined (this is not Perl 5; use `fn` not `sub`, or pass --compat)"
12862                ),
12863                line,
12864            ));
12865        }
12866        Ok(())
12867    }
12868
12869    /// Check if a hash name shadows a reserved stryke hash and error if so.
12870    /// Called only in non-compat mode.
12871    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> PerlResult<()> {
12872        if Self::is_reserved_hash_name(name) {
12873            return Err(self.syntax_err(
12874                format!(
12875"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
12876                ),
12877                line,
12878            ));
12879        }
12880        Ok(())
12881    }
12882
12883    /// Validate assignment to %hash in non-compat mode.
12884    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
12885    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
12886        match &value.kind {
12887            ExprKind::Integer(_) | ExprKind::Float(_) => {
12888                return Err(self.syntax_err(
12889                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
12890                    line,
12891                ));
12892            }
12893            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
12894                return Err(self.syntax_err(
12895                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
12896                    line,
12897                ));
12898            }
12899            ExprKind::ArrayRef(_) => {
12900                return Err(self.syntax_err(
12901                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
12902                    line,
12903                ));
12904            }
12905            ExprKind::ScalarRef(inner) => {
12906                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
12907                    return Err(self.syntax_err(
12908                        "cannot assign \\@array to hash — use %h = @array for even-length list",
12909                        line,
12910                    ));
12911                }
12912                if matches!(inner.kind, ExprKind::HashVar(_)) {
12913                    return Err(self.syntax_err(
12914                        "cannot assign \\%hash to hash — use %h = %other directly",
12915                        line,
12916                    ));
12917                }
12918            }
12919            ExprKind::HashRef(_) => {
12920                return Err(self.syntax_err(
12921                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
12922                    line,
12923                ));
12924            }
12925            ExprKind::CodeRef { .. } => {
12926                return Err(self.syntax_err("cannot assign coderef to hash", line));
12927            }
12928            ExprKind::Undef => {
12929                return Err(
12930                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
12931                );
12932            }
12933            ExprKind::List(items)
12934                if items.len() % 2 != 0
12935                    && !items.iter().any(|e| {
12936                        matches!(
12937                            e.kind,
12938                            ExprKind::ArrayVar(_)
12939                                | ExprKind::HashVar(_)
12940                                | ExprKind::FuncCall { .. }
12941                                | ExprKind::Deref { .. }
12942                                | ExprKind::ScalarVar(_)
12943                        )
12944                    }) =>
12945            {
12946                return Err(self.syntax_err(
12947                        format!(
12948                            "odd-length list ({} elements) in hash assignment — missing value for last key",
12949                            items.len()
12950                        ),
12951                        line,
12952                    ));
12953            }
12954            _ => {}
12955        }
12956        Ok(())
12957    }
12958
12959    /// Validate assignment to @array in non-compat mode.
12960    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
12961    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
12962    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
12963    fn validate_array_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
12964        if let ExprKind::Undef = &value.kind {
12965            return Err(
12966                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
12967            );
12968        }
12969        Ok(())
12970    }
12971
12972    /// Validate assignment to $scalar in non-compat mode.
12973    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
12974    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
12975        if let ExprKind::List(items) = &value.kind {
12976            if items.len() > 1 {
12977                return Err(self.syntax_err(
12978                    format!(
12979                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
12980                        items.len()
12981                    ),
12982                    line,
12983                ));
12984            }
12985        }
12986        Ok(())
12987    }
12988
12989    /// Validate an assignment based on target type (in non-compat mode only).
12990    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> PerlResult<()> {
12991        if crate::compat_mode() {
12992            return Ok(());
12993        }
12994        match &target.kind {
12995            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
12996            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
12997            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
12998            _ => Ok(()),
12999        }
13000    }
13001
13002    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
13003    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
13004    /// Also accepts a bare function name: `psort my_cmp, @list`.
13005    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
13006        if matches!(self.peek(), Token::LBrace) {
13007            return self.parse_block();
13008        }
13009        let line = self.peek_line();
13010        // Bare sub name: `psort my_cmp, @list`
13011        if let Token::Ident(ref name) = self.peek().clone() {
13012            if matches!(
13013                self.peek_at(1),
13014                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13015            ) {
13016                let name = name.clone();
13017                self.advance();
13018                let body = Expr {
13019                    kind: ExprKind::FuncCall {
13020                        name,
13021                        args: vec![
13022                            Expr {
13023                                kind: ExprKind::ScalarVar("a".to_string()),
13024                                line,
13025                            },
13026                            Expr {
13027                                kind: ExprKind::ScalarVar("b".to_string()),
13028                                line,
13029                            },
13030                        ],
13031                    },
13032                    line,
13033                };
13034                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13035            }
13036        }
13037        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
13038        let expr = self.parse_assign_expr_stop_at_pipe()?;
13039        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13040    }
13041
13042    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
13043    fn parse_fan_optional_progress(
13044        &mut self,
13045        which: &'static str,
13046    ) -> PerlResult<Option<Box<Expr>>> {
13047        let line = self.peek_line();
13048        if self.eat(&Token::Comma) {
13049            match self.peek() {
13050                Token::Ident(ref kw)
13051                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
13052                {
13053                    self.advance();
13054                    self.expect(&Token::FatArrow)?;
13055                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
13056                }
13057                _ => {
13058                    return Err(self.syntax_err(
13059                        format!("{which}: expected `progress => EXPR` after comma"),
13060                        line,
13061                    ));
13062                }
13063            }
13064        }
13065        if let Token::Ident(ref kw) = self.peek().clone() {
13066            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13067                self.advance();
13068                self.expect(&Token::FatArrow)?;
13069                return Ok(Some(Box::new(self.parse_assign_expr()?)));
13070            }
13071        }
13072        Ok(None)
13073    }
13074
13075    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
13076    /// (for `pmap_chunked`, `psort`, etc.).
13077    ///
13078    /// Paren-less — individual parts parse through
13079    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
13080    /// the enclosing pipe-forward loop (left-associative chaining).
13081    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
13082        // On the RHS of `|>`, list-taking builtins may be written bare with no
13083        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
13084        // When the next token is a list-terminator, yield an empty placeholder
13085        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
13086        // desugar time, so the placeholder is never evaluated.
13087        if self.in_pipe_rhs()
13088            && matches!(
13089                self.peek(),
13090                Token::Semicolon
13091                    | Token::RBrace
13092                    | Token::RParen
13093                    | Token::Eof
13094                    | Token::PipeForward
13095                    | Token::Comma
13096            )
13097        {
13098            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
13099        }
13100        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13101        loop {
13102            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13103                break;
13104            }
13105            if matches!(
13106                self.peek(),
13107                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13108            ) {
13109                break;
13110            }
13111            if self.peek_is_postfix_stmt_modifier_keyword() {
13112                break;
13113            }
13114            if let Token::Ident(ref kw) = self.peek().clone() {
13115                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13116                    self.advance();
13117                    self.expect(&Token::FatArrow)?;
13118                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13119                    return Ok((merge_expr_list(parts), Some(prog)));
13120                }
13121            }
13122            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13123        }
13124        Ok((merge_expr_list(parts), None))
13125    }
13126
13127    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
13128        if matches!(self.peek(), Token::LParen) {
13129            self.advance();
13130            let expr = self.parse_expression()?;
13131            self.expect(&Token::RParen)?;
13132            Ok(expr)
13133        } else {
13134            self.parse_assign_expr_stop_at_pipe()
13135        }
13136    }
13137
13138    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
13139        // Default to `$_` when the next token cannot start an argument expression
13140        // because it has lower precedence than a named unary operator. Perl 5
13141        // named unary precedence sits above ternary / comparison / logical / bitwise
13142        // / assignment / list ops; everything below should terminate the implicit
13143        // argument and let the surrounding expression continue.
13144        // See `perldoc perlop` ("Named Unary Operators").
13145        if matches!(
13146            self.peek(),
13147            // Statement / list / call boundaries
13148            Token::Semicolon
13149                | Token::RBrace
13150                | Token::RParen
13151                | Token::RBracket
13152                | Token::Eof
13153                | Token::Comma
13154                | Token::FatArrow
13155                | Token::PipeForward
13156            // Ternary `? :`
13157                | Token::Question
13158                | Token::Colon
13159            // Comparison / equality (numeric + string)
13160                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
13161                | Token::NumLe | Token::NumGe | Token::Spaceship
13162                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
13163                | Token::StrLe | Token::StrGe | Token::StrCmp
13164            // Logical (symbolic and word forms) + defined-or
13165                | Token::LogAnd | Token::LogOr | Token::LogNot
13166                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
13167                | Token::DefinedOr
13168            // Range (lower precedence than named unary)
13169                | Token::Range | Token::RangeExclusive
13170            // Assignment (any compound form)
13171                | Token::Assign | Token::PlusAssign | Token::MinusAssign
13172                | Token::MulAssign | Token::DivAssign | Token::ModAssign
13173                | Token::PowAssign | Token::DotAssign | Token::AndAssign
13174                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
13175                | Token::ShiftLeftAssign | Token::ShiftRightAssign
13176                | Token::BitAndAssign | Token::BitOrAssign
13177        ) {
13178            return Ok(Expr {
13179                kind: ExprKind::ScalarVar("_".into()),
13180                line: self.peek_line(),
13181            });
13182        }
13183        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
13184        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
13185        // Perl accepts both `length` and `length()` as `length($_)`.
13186        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
13187            let line = self.peek_line();
13188            self.advance(); // (
13189            self.advance(); // )
13190            return Ok(Expr {
13191                kind: ExprKind::ScalarVar("_".into()),
13192                line,
13193            });
13194        }
13195        self.parse_one_arg()
13196    }
13197
13198    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
13199    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
13200        let line = self.prev_line(); // line where shift/pop keyword was
13201        if matches!(self.peek(), Token::LParen) {
13202            self.advance();
13203            if matches!(self.peek(), Token::RParen) {
13204                self.advance();
13205                return Ok(Expr {
13206                    kind: ExprKind::ArrayVar("_".into()),
13207                    line: self.peek_line(),
13208                });
13209            }
13210            let expr = self.parse_expression()?;
13211            self.expect(&Token::RParen)?;
13212            return Ok(expr);
13213        }
13214        // Implicit semicolon: if next token is on a different line, don't consume it
13215        if matches!(
13216            self.peek(),
13217            Token::Semicolon
13218                | Token::RBrace
13219                | Token::RParen
13220                | Token::Eof
13221                | Token::Comma
13222                | Token::PipeForward
13223        ) || self.peek_line() > line
13224        {
13225            Ok(Expr {
13226                kind: ExprKind::ArrayVar("_".into()),
13227                line,
13228            })
13229        } else {
13230            self.parse_assign_expr()
13231        }
13232    }
13233
13234    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
13235        if matches!(self.peek(), Token::LParen) {
13236            self.advance();
13237            let args = self.parse_arg_list()?;
13238            self.expect(&Token::RParen)?;
13239            Ok(args)
13240        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
13241            // In thread context, don't consume barewords as arguments
13242            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
13243            Ok(vec![])
13244        } else {
13245            self.parse_list_until_terminator()
13246        }
13247    }
13248
13249    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
13250    /// should be treated as an auto-quoted string (hash key), not a function call.
13251    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
13252    #[inline]
13253    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
13254        if matches!(self.peek(), Token::FatArrow) {
13255            Some(Expr {
13256                kind: ExprKind::String(name.to_string()),
13257                line,
13258            })
13259        } else {
13260            None
13261        }
13262    }
13263
13264    /// Parse a hash subscript key inside `{…}`.
13265    ///
13266    /// Perl auto-quotes a single bareword before `}`, even for keywords:
13267    /// `$h{print}`, `$r->{f}` etc. all yield the string key.
13268    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
13269        let line = self.peek_line();
13270        if let Token::Ident(ref k) = self.peek().clone() {
13271            if matches!(self.peek_at(1), Token::RBrace) {
13272                let s = k.clone();
13273                self.advance();
13274                return Ok(Expr {
13275                    kind: ExprKind::String(s),
13276                    line,
13277                });
13278            }
13279        }
13280        self.parse_expression()
13281    }
13282
13283    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
13284    #[inline]
13285    fn peek_is_glob_par_progress_kw(&self) -> bool {
13286        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
13287            && matches!(self.peek_at(1), Token::FatArrow)
13288    }
13289
13290    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
13291    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
13292        let mut args = Vec::new();
13293        loop {
13294            if matches!(self.peek(), Token::RParen | Token::Eof) {
13295                break;
13296            }
13297            if self.peek_is_glob_par_progress_kw() {
13298                break;
13299            }
13300            args.push(self.parse_assign_expr()?);
13301            match self.peek() {
13302                Token::RParen => break,
13303                Token::Comma => {
13304                    self.advance();
13305                    if matches!(self.peek(), Token::RParen) {
13306                        break;
13307                    }
13308                    if self.peek_is_glob_par_progress_kw() {
13309                        break;
13310                    }
13311                }
13312                _ => {
13313                    return Err(self.syntax_err(
13314                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
13315                        self.peek_line(),
13316                    ));
13317                }
13318            }
13319        }
13320        Ok(args)
13321    }
13322
13323    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
13324    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
13325        let mut args = Vec::new();
13326        loop {
13327            if matches!(
13328                self.peek(),
13329                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13330            ) {
13331                break;
13332            }
13333            if self.peek_is_postfix_stmt_modifier_keyword() {
13334                break;
13335            }
13336            if self.peek_is_glob_par_progress_kw() {
13337                break;
13338            }
13339            args.push(self.parse_assign_expr()?);
13340            if !self.eat(&Token::Comma) {
13341                break;
13342            }
13343            if self.peek_is_glob_par_progress_kw() {
13344                break;
13345            }
13346        }
13347        Ok(args)
13348    }
13349
13350    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
13351    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
13352        if matches!(self.peek(), Token::LParen) {
13353            self.advance();
13354            let args = self.parse_pattern_list_until_rparen_or_progress()?;
13355            let progress = if self.peek_is_glob_par_progress_kw() {
13356                self.advance();
13357                self.expect(&Token::FatArrow)?;
13358                Some(Box::new(self.parse_assign_expr()?))
13359            } else {
13360                None
13361            };
13362            self.expect(&Token::RParen)?;
13363            Ok((args, progress))
13364        } else {
13365            let args = self.parse_pattern_list_glob_par_bare()?;
13366            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
13367            let progress = if self.peek_is_glob_par_progress_kw() {
13368                self.advance();
13369                self.expect(&Token::FatArrow)?;
13370                Some(Box::new(self.parse_assign_expr()?))
13371            } else {
13372                None
13373            };
13374            Ok((args, progress))
13375        }
13376    }
13377
13378    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
13379        let mut args = Vec::new();
13380        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
13381        // so shadow any outer paren-less-arg suppression from
13382        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
13383        let saved_no_pf = self.no_pipe_forward_depth;
13384        self.no_pipe_forward_depth = 0;
13385        while !matches!(
13386            self.peek(),
13387            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
13388        ) {
13389            let arg = match self.parse_assign_expr() {
13390                Ok(e) => e,
13391                Err(err) => {
13392                    self.no_pipe_forward_depth = saved_no_pf;
13393                    return Err(err);
13394                }
13395            };
13396            args.push(arg);
13397            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13398                break;
13399            }
13400        }
13401        self.no_pipe_forward_depth = saved_no_pf;
13402        Ok(args)
13403    }
13404
13405    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
13406    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
13407    /// no-arg method call; we must not consume that `+` as the start of a first argument.
13408    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
13409        let mut args = Vec::new();
13410        let call_line = self.prev_line();
13411        loop {
13412            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
13413            // hash argument to `next` (paren-less method call has no args here).
13414            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
13415                break;
13416            }
13417            if matches!(
13418                self.peek(),
13419                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13420            ) {
13421                break;
13422            }
13423            if let Token::Ident(ref kw) = self.peek().clone() {
13424                if matches!(
13425                    kw.as_str(),
13426                    "if" | "unless" | "while" | "until" | "for" | "foreach"
13427                ) {
13428                    break;
13429                }
13430            }
13431            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
13432            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
13433            if args.is_empty()
13434                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
13435            {
13436                break;
13437            }
13438            // Implicit semicolon: if no args collected yet and next token is on a different
13439            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
13440            if args.is_empty() && self.peek_line() > call_line {
13441                break;
13442            }
13443            args.push(self.parse_assign_expr()?);
13444            if !self.eat(&Token::Comma) {
13445                break;
13446            }
13447        }
13448        Ok(args)
13449    }
13450
13451    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
13452    /// the whole `->meth` expression).
13453    fn peek_method_arg_infix_terminator(&self) -> bool {
13454        matches!(
13455            self.peek(),
13456            Token::Plus
13457                | Token::Minus
13458                | Token::Star
13459                | Token::Slash
13460                | Token::Percent
13461                | Token::Power
13462                | Token::Dot
13463                | Token::X
13464                | Token::NumEq
13465                | Token::NumNe
13466                | Token::NumLt
13467                | Token::NumGt
13468                | Token::NumLe
13469                | Token::NumGe
13470                | Token::Spaceship
13471                | Token::StrEq
13472                | Token::StrNe
13473                | Token::StrLt
13474                | Token::StrGt
13475                | Token::StrLe
13476                | Token::StrGe
13477                | Token::StrCmp
13478                | Token::LogAnd
13479                | Token::LogOr
13480                | Token::LogAndWord
13481                | Token::LogOrWord
13482                | Token::DefinedOr
13483                | Token::BitAnd
13484                | Token::BitOr
13485                | Token::BitXor
13486                | Token::ShiftLeft
13487                | Token::ShiftRight
13488                | Token::Range
13489                | Token::RangeExclusive
13490                | Token::BindMatch
13491                | Token::BindNotMatch
13492                | Token::Arrow
13493                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
13494                | Token::Question
13495                | Token::Colon
13496                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
13497                | Token::Assign
13498                | Token::PlusAssign
13499                | Token::MinusAssign
13500                | Token::MulAssign
13501                | Token::DivAssign
13502                | Token::ModAssign
13503                | Token::PowAssign
13504                | Token::DotAssign
13505                | Token::AndAssign
13506                | Token::OrAssign
13507                | Token::XorAssign
13508                | Token::DefinedOrAssign
13509                | Token::ShiftLeftAssign
13510                | Token::ShiftRightAssign
13511                | Token::BitAndAssign
13512                | Token::BitOrAssign
13513        )
13514    }
13515
13516    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
13517        let mut args = Vec::new();
13518        // Line of the last consumed token (the keyword / function name that
13519        // triggered this arg parse).  Used for implicit-semicolon: if no args
13520        // have been parsed yet and the next token is on a *different* line,
13521        // treat the newline as a statement boundary and stop.
13522        let call_line = self.prev_line();
13523        loop {
13524            if matches!(
13525                self.peek(),
13526                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13527            ) {
13528                break;
13529            }
13530            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
13531            if let Token::Ident(ref kw) = self.peek().clone() {
13532                if matches!(
13533                    kw.as_str(),
13534                    "if" | "unless" | "while" | "until" | "for" | "foreach"
13535                ) {
13536                    break;
13537                }
13538            }
13539            // Implicit semicolons: if no args have been collected yet and the
13540            // next token is on a different line from the call keyword, treat
13541            // the newline as a statement boundary.  This prevents paren-less
13542            // calls (`say`, `print`, user subs) from greedily swallowing the
13543            // *next* statement when the author omitted a semicolon.
13544            // After a comma continuation, multi-line arg lists still work.
13545            if args.is_empty() && self.peek_line() > call_line {
13546                break;
13547            }
13548            // Paren-less builtin args: `|>` terminates the whole call list, so
13549            // individual args must not absorb a following `|>`.
13550            args.push(self.parse_assign_expr_stop_at_pipe()?);
13551            if !self.eat(&Token::Comma) {
13552                break;
13553            }
13554        }
13555        Ok(args)
13556    }
13557
13558    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
13559        let mut pairs = Vec::new();
13560        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
13561            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
13562            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
13563            let line = self.peek_line();
13564            let key = if let Token::Ident(ref name) = self.peek().clone() {
13565                if matches!(self.peek_at(1), Token::FatArrow) {
13566                    self.advance();
13567                    Expr {
13568                        kind: ExprKind::String(name.clone()),
13569                        line,
13570                    }
13571                } else {
13572                    self.parse_assign_expr()?
13573                }
13574            } else {
13575                self.parse_assign_expr()?
13576            };
13577            // If the key expression is a hash/array variable and is followed by `}` or `,`
13578            // with no `=>`, treat the whole thing as a hash-from-expression construction.
13579            // This handles `{ %a }`, `{ %a, key => val }`, etc.
13580            if matches!(self.peek(), Token::RBrace | Token::Comma)
13581                && matches!(
13582                    key.kind,
13583                    ExprKind::HashVar(_)
13584                        | ExprKind::Deref {
13585                            kind: Sigil::Hash,
13586                            ..
13587                        }
13588                )
13589            {
13590                // Synthesize a pair whose key/value is spread from the hash expression.
13591                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
13592                // The evaluator will flatten this.
13593                let sentinel_key = Expr {
13594                    kind: ExprKind::String("__HASH_SPREAD__".into()),
13595                    line,
13596                };
13597                pairs.push((sentinel_key, key));
13598                self.eat(&Token::Comma);
13599                continue;
13600            }
13601            // Expect => or , after key
13602            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
13603                let val = self.parse_assign_expr()?;
13604                pairs.push((key, val));
13605                self.eat(&Token::Comma);
13606            } else {
13607                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
13608            }
13609        }
13610        self.expect(&Token::RBrace)?;
13611        Ok(pairs)
13612    }
13613
13614    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
13615    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
13616    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
13617    /// navigates. Caller expects and consumes `term` itself.
13618    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
13619        let mut pairs = Vec::new();
13620        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
13621            && !matches!(self.peek(), Token::Eof)
13622        {
13623            let line = self.peek_line();
13624            let key = if let Token::Ident(ref name) = self.peek().clone() {
13625                if matches!(self.peek_at(1), Token::FatArrow) {
13626                    self.advance();
13627                    Expr {
13628                        kind: ExprKind::String(name.clone()),
13629                        line,
13630                    }
13631                } else {
13632                    self.parse_assign_expr()?
13633                }
13634            } else {
13635                self.parse_assign_expr()?
13636            };
13637            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
13638                let val = self.parse_assign_expr()?;
13639                pairs.push((key, val));
13640                self.eat(&Token::Comma);
13641            } else {
13642                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
13643            }
13644        }
13645        Ok(pairs)
13646    }
13647
13648    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
13649    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
13650    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
13651    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
13652    /// Each step wraps the current expression in an `ArrowDeref`.
13653    fn interp_chain_subscripts(
13654        &self,
13655        chars: &[char],
13656        i: &mut usize,
13657        mut base: Expr,
13658        line: usize,
13659    ) -> Expr {
13660        loop {
13661            // Optional `->` connector
13662            let (after, requires_subscript) =
13663                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
13664                    (*i + 2, true)
13665                } else {
13666                    (*i, false)
13667                };
13668            if after >= chars.len() {
13669                break;
13670            }
13671            match chars[after] {
13672                '[' => {
13673                    *i = after + 1;
13674                    let mut idx_str = String::new();
13675                    while *i < chars.len() && chars[*i] != ']' {
13676                        idx_str.push(chars[*i]);
13677                        *i += 1;
13678                    }
13679                    if *i < chars.len() {
13680                        *i += 1;
13681                    }
13682                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
13683                        Expr {
13684                            kind: ExprKind::ScalarVar(rest.to_string()),
13685                            line,
13686                        }
13687                    } else if let Ok(n) = idx_str.parse::<i64>() {
13688                        Expr {
13689                            kind: ExprKind::Integer(n),
13690                            line,
13691                        }
13692                    } else {
13693                        Expr {
13694                            kind: ExprKind::String(idx_str),
13695                            line,
13696                        }
13697                    };
13698                    base = Expr {
13699                        kind: ExprKind::ArrowDeref {
13700                            expr: Box::new(base),
13701                            index: Box::new(idx_expr),
13702                            kind: DerefKind::Array,
13703                        },
13704                        line,
13705                    };
13706                }
13707                '{' => {
13708                    *i = after + 1;
13709                    let mut key = String::new();
13710                    let mut depth = 1usize;
13711                    while *i < chars.len() && depth > 0 {
13712                        if chars[*i] == '{' {
13713                            depth += 1;
13714                        } else if chars[*i] == '}' {
13715                            depth -= 1;
13716                            if depth == 0 {
13717                                break;
13718                            }
13719                        }
13720                        key.push(chars[*i]);
13721                        *i += 1;
13722                    }
13723                    if *i < chars.len() {
13724                        *i += 1;
13725                    }
13726                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
13727                        Expr {
13728                            kind: ExprKind::ScalarVar(rest.to_string()),
13729                            line,
13730                        }
13731                    } else {
13732                        Expr {
13733                            kind: ExprKind::String(key),
13734                            line,
13735                        }
13736                    };
13737                    base = Expr {
13738                        kind: ExprKind::ArrowDeref {
13739                            expr: Box::new(base),
13740                            index: Box::new(key_expr),
13741                            kind: DerefKind::Hash,
13742                        },
13743                        line,
13744                    };
13745                }
13746                _ => {
13747                    if requires_subscript {
13748                        // `->method()` etc — not interpolated, leave for literal output.
13749                    }
13750                    break;
13751                }
13752            }
13753        }
13754        base
13755    }
13756
13757    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
13758        // Parse $var and @var inside double-quoted strings
13759        let mut parts = Vec::new();
13760        let mut literal = String::new();
13761        let chars: Vec<char> = s.chars().collect();
13762        let mut i = 0;
13763
13764        'istr: while i < chars.len() {
13765            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
13766                literal.push('$');
13767                i += 1;
13768                continue;
13769            }
13770            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
13771            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
13772                literal.push('\\');
13773                i += 1;
13774                // i now points at '$' — fall through to $ handling below
13775            }
13776            if chars[i] == '$' && i + 1 < chars.len() {
13777                if !literal.is_empty() {
13778                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
13779                }
13780                i += 1; // past `$`
13781                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
13782                while i < chars.len() && chars[i].is_whitespace() {
13783                    i += 1;
13784                }
13785                if i >= chars.len() {
13786                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
13787                }
13788                // `$#name` — last index of `@name` (Perl `$#array`).
13789                if chars[i] == '#' {
13790                    i += 1;
13791                    let mut sname = String::from("#");
13792                    while i < chars.len()
13793                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
13794                    {
13795                        sname.push(chars[i]);
13796                        i += 1;
13797                    }
13798                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
13799                        sname.push_str("::");
13800                        i += 2;
13801                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13802                            sname.push(chars[i]);
13803                            i += 1;
13804                        }
13805                    }
13806                    parts.push(StringPart::ScalarVar(sname));
13807                    continue;
13808                }
13809                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
13810                // between) and the second `$` is not followed by a word character or digit (`$$x`
13811                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
13812                if chars[i] == '$' {
13813                    let next_c = chars.get(i + 1).copied();
13814                    let is_pid = match next_c {
13815                        None => true,
13816                        Some(c)
13817                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
13818                        {
13819                            true
13820                        }
13821                        _ => false,
13822                    };
13823                    if is_pid {
13824                        parts.push(StringPart::ScalarVar("$$".to_string()));
13825                        i += 1; // consume second `$`
13826                        continue;
13827                    }
13828                    i += 1; // skip second `$` — same as a single `$` before the identifier
13829                }
13830                if chars[i] == '{' {
13831                    // `${…}` — braced variable OR expression interpolation.
13832                    //   `${name}`              → ScalarVar(name)        (Perl standard)
13833                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
13834                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
13835                    // stryke's prior `#{expr}` form remains supported elsewhere.
13836                    i += 1;
13837                    let mut inner = String::new();
13838                    let mut depth = 1usize;
13839                    while i < chars.len() && depth > 0 {
13840                        match chars[i] {
13841                            '{' => depth += 1,
13842                            '}' => {
13843                                depth -= 1;
13844                                if depth == 0 {
13845                                    break;
13846                                }
13847                            }
13848                            _ => {}
13849                        }
13850                        inner.push(chars[i]);
13851                        i += 1;
13852                    }
13853                    if i < chars.len() {
13854                        i += 1; // skip closing }
13855                    }
13856
13857                    // Distinguish "name" from "expression". If trimmed inner starts with
13858                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
13859                    // expression and emit a scalar deref. Otherwise, plain variable name.
13860                    let trimmed = inner.trim();
13861                    let is_expr = trimmed.starts_with('$')
13862                        || trimmed.starts_with('\\')
13863                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
13864                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
13865                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
13866                    let mut base: Expr = if is_expr {
13867                        // Re-parse the inner content as a Perl expression. Wrap in
13868                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
13869                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
13870                        match parse_expression_from_str(trimmed, "<interp>") {
13871                            Ok(e) => Expr {
13872                                kind: ExprKind::Deref {
13873                                    expr: Box::new(e),
13874                                    kind: Sigil::Scalar,
13875                                },
13876                                line,
13877                            },
13878                            Err(_) => Expr {
13879                                kind: ExprKind::ScalarVar(inner.clone()),
13880                                line,
13881                            },
13882                        }
13883                    } else {
13884                        // Treat as a plain (possibly qualified) variable name.
13885                        Expr {
13886                            kind: ExprKind::ScalarVar(inner),
13887                            line,
13888                        }
13889                    };
13890
13891                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
13892                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
13893                    // chains thereafter.
13894                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
13895                    parts.push(StringPart::Expr(base));
13896                } else if chars[i] == '^' {
13897                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
13898                    let mut name = String::from("^");
13899                    i += 1;
13900                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13901                        name.push(chars[i]);
13902                        i += 1;
13903                    }
13904                    if i < chars.len() && chars[i] == '{' {
13905                        i += 1; // skip {
13906                        let mut key = String::new();
13907                        let mut depth = 1;
13908                        while i < chars.len() && depth > 0 {
13909                            if chars[i] == '{' {
13910                                depth += 1;
13911                            } else if chars[i] == '}' {
13912                                depth -= 1;
13913                                if depth == 0 {
13914                                    break;
13915                                }
13916                            }
13917                            key.push(chars[i]);
13918                            i += 1;
13919                        }
13920                        if i < chars.len() {
13921                            i += 1;
13922                        }
13923                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
13924                            Expr {
13925                                kind: ExprKind::ScalarVar(rest.to_string()),
13926                                line,
13927                            }
13928                        } else {
13929                            Expr {
13930                                kind: ExprKind::String(key),
13931                                line,
13932                            }
13933                        };
13934                        parts.push(StringPart::Expr(Expr {
13935                            kind: ExprKind::HashElement {
13936                                hash: name,
13937                                key: Box::new(key_expr),
13938                            },
13939                            line,
13940                        }));
13941                    } else if i < chars.len() && chars[i] == '[' {
13942                        i += 1;
13943                        let mut idx_str = String::new();
13944                        while i < chars.len() && chars[i] != ']' {
13945                            idx_str.push(chars[i]);
13946                            i += 1;
13947                        }
13948                        if i < chars.len() {
13949                            i += 1;
13950                        }
13951                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
13952                            Expr {
13953                                kind: ExprKind::ScalarVar(rest.to_string()),
13954                                line,
13955                            }
13956                        } else if let Ok(n) = idx_str.parse::<i64>() {
13957                            Expr {
13958                                kind: ExprKind::Integer(n),
13959                                line,
13960                            }
13961                        } else {
13962                            Expr {
13963                                kind: ExprKind::String(idx_str),
13964                                line,
13965                            }
13966                        };
13967                        parts.push(StringPart::Expr(Expr {
13968                            kind: ExprKind::ArrayElement {
13969                                array: name,
13970                                index: Box::new(idx_expr),
13971                            },
13972                            line,
13973                        }));
13974                    } else {
13975                        parts.push(StringPart::ScalarVar(name));
13976                    }
13977                } else if chars[i].is_alphabetic() || chars[i] == '_' {
13978                    let mut name = String::new();
13979                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
13980                        name.push(chars[i]);
13981                        i += 1;
13982                    }
13983                    // `$_<`, `$_<<`, … — outer topic (stryke extension); only for bare `_`.
13984                    if name == "_" {
13985                        while i < chars.len() && chars[i] == '<' {
13986                            name.push('<');
13987                            i += 1;
13988                        }
13989                    }
13990                    // Build the base expression, then thread arrow-deref chains
13991                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
13992                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
13993                    // correctly inside double-quoted strings (Perl convention).
13994                    let mut base = if i < chars.len() && chars[i] == '{' {
13995                        // $hash{key}
13996                        i += 1; // skip {
13997                        let mut key = String::new();
13998                        let mut depth = 1;
13999                        while i < chars.len() && depth > 0 {
14000                            if chars[i] == '{' {
14001                                depth += 1;
14002                            } else if chars[i] == '}' {
14003                                depth -= 1;
14004                                if depth == 0 {
14005                                    break;
14006                                }
14007                            }
14008                            key.push(chars[i]);
14009                            i += 1;
14010                        }
14011                        if i < chars.len() {
14012                            i += 1;
14013                        } // skip }
14014                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
14015                            Expr {
14016                                kind: ExprKind::ScalarVar(rest.to_string()),
14017                                line,
14018                            }
14019                        } else {
14020                            Expr {
14021                                kind: ExprKind::String(key),
14022                                line,
14023                            }
14024                        };
14025                        Expr {
14026                            kind: ExprKind::HashElement {
14027                                hash: name,
14028                                key: Box::new(key_expr),
14029                            },
14030                            line,
14031                        }
14032                    } else if i < chars.len() && chars[i] == '[' {
14033                        // $array[idx]
14034                        i += 1;
14035                        let mut idx_str = String::new();
14036                        while i < chars.len() && chars[i] != ']' {
14037                            idx_str.push(chars[i]);
14038                            i += 1;
14039                        }
14040                        if i < chars.len() {
14041                            i += 1;
14042                        }
14043                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
14044                            Expr {
14045                                kind: ExprKind::ScalarVar(rest.to_string()),
14046                                line,
14047                            }
14048                        } else if let Ok(n) = idx_str.parse::<i64>() {
14049                            Expr {
14050                                kind: ExprKind::Integer(n),
14051                                line,
14052                            }
14053                        } else {
14054                            Expr {
14055                                kind: ExprKind::String(idx_str),
14056                                line,
14057                            }
14058                        };
14059                        Expr {
14060                            kind: ExprKind::ArrayElement {
14061                                array: name,
14062                                index: Box::new(idx_expr),
14063                            },
14064                            line,
14065                        }
14066                    } else {
14067                        // Bare $name — defer to the chain-extension loop below.
14068                        Expr {
14069                            kind: ExprKind::ScalarVar(name),
14070                            line,
14071                        }
14072                    };
14073
14074                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
14075                    // implies `->` between consecutive subscripts (`$m[1][2]`
14076                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
14077                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14078                    parts.push(StringPart::Expr(base));
14079                } else if chars[i].is_ascii_digit() {
14080                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
14081                    if chars[i] == '0' {
14082                        i += 1;
14083                        if i < chars.len() && chars[i].is_ascii_digit() {
14084                            return Err(self.syntax_err(
14085                                "Numeric variables with more than one digit may not start with '0'",
14086                                line,
14087                            ));
14088                        }
14089                        parts.push(StringPart::ScalarVar("0".into()));
14090                    } else {
14091                        let start = i;
14092                        while i < chars.len() && chars[i].is_ascii_digit() {
14093                            i += 1;
14094                        }
14095                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
14096                    }
14097                } else {
14098                    let c = chars[i];
14099                    let probe = c.to_string();
14100                    if Interpreter::is_special_scalar_name_for_get(&probe)
14101                        || matches!(c, '\'' | '`')
14102                    {
14103                        i += 1;
14104                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
14105                        if i < chars.len() && chars[i] == '{' {
14106                            i += 1; // skip {
14107                            let mut key = String::new();
14108                            let mut depth = 1;
14109                            while i < chars.len() && depth > 0 {
14110                                if chars[i] == '{' {
14111                                    depth += 1;
14112                                } else if chars[i] == '}' {
14113                                    depth -= 1;
14114                                    if depth == 0 {
14115                                        break;
14116                                    }
14117                                }
14118                                key.push(chars[i]);
14119                                i += 1;
14120                            }
14121                            if i < chars.len() {
14122                                i += 1;
14123                            } // skip }
14124                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
14125                                Expr {
14126                                    kind: ExprKind::ScalarVar(rest.to_string()),
14127                                    line,
14128                                }
14129                            } else {
14130                                Expr {
14131                                    kind: ExprKind::String(key),
14132                                    line,
14133                                }
14134                            };
14135                            let mut base = Expr {
14136                                kind: ExprKind::HashElement {
14137                                    hash: probe,
14138                                    key: Box::new(key_expr),
14139                                },
14140                                line,
14141                            };
14142                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14143                            parts.push(StringPart::Expr(base));
14144                        } else {
14145                            // Check for arrow deref chain: `$@->{key}`, etc.
14146                            let mut base = Expr {
14147                                kind: ExprKind::ScalarVar(probe),
14148                                line,
14149                            };
14150                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14151                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
14152                                // No chain extension — use the simpler ScalarVar part
14153                                if let ExprKind::ScalarVar(name) = base.kind {
14154                                    parts.push(StringPart::ScalarVar(name));
14155                                }
14156                            } else {
14157                                parts.push(StringPart::Expr(base));
14158                            }
14159                        }
14160                    } else {
14161                        literal.push('$');
14162                        literal.push(c);
14163                        i += 1;
14164                    }
14165                }
14166            } else if chars[i] == '@' && i + 1 < chars.len() {
14167                let next = chars[i + 1];
14168                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
14169                if next == '$' {
14170                    if !literal.is_empty() {
14171                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14172                    }
14173                    i += 1; // past `@`
14174                    debug_assert_eq!(chars[i], '$');
14175                    i += 1; // past `$`
14176                    while i < chars.len() && chars[i].is_whitespace() {
14177                        i += 1;
14178                    }
14179                    if i >= chars.len() {
14180                        return Err(self.syntax_err(
14181                            "Expected variable or block after `@$` in double-quoted string",
14182                            line,
14183                        ));
14184                    }
14185                    let inner_expr = if chars[i] == '{' {
14186                        i += 1;
14187                        let start = i;
14188                        let mut depth = 1usize;
14189                        while i < chars.len() && depth > 0 {
14190                            match chars[i] {
14191                                '{' => depth += 1,
14192                                '}' => {
14193                                    depth -= 1;
14194                                    if depth == 0 {
14195                                        break;
14196                                    }
14197                                }
14198                                _ => {}
14199                            }
14200                            i += 1;
14201                        }
14202                        if depth != 0 {
14203                            return Err(self.syntax_err(
14204                                "Unterminated `${ ... }` after `@` in double-quoted string",
14205                                line,
14206                            ));
14207                        }
14208                        let inner: String = chars[start..i].iter().collect();
14209                        i += 1; // closing `}`
14210                        parse_expression_from_str(inner.trim(), "-e")?
14211                    } else {
14212                        let mut name = String::new();
14213                        if chars[i] == '^' {
14214                            name.push('^');
14215                            i += 1;
14216                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
14217                            {
14218                                name.push(chars[i]);
14219                                i += 1;
14220                            }
14221                        } else {
14222                            while i < chars.len()
14223                                && (chars[i].is_alphanumeric()
14224                                    || chars[i] == '_'
14225                                    || chars[i] == ':')
14226                            {
14227                                name.push(chars[i]);
14228                                i += 1;
14229                            }
14230                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14231                                name.push_str("::");
14232                                i += 2;
14233                                while i < chars.len()
14234                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
14235                                {
14236                                    name.push(chars[i]);
14237                                    i += 1;
14238                                }
14239                            }
14240                        }
14241                        if name.is_empty() {
14242                            return Err(self.syntax_err(
14243                                "Expected identifier after `@$` in double-quoted string",
14244                                line,
14245                            ));
14246                        }
14247                        Expr {
14248                            kind: ExprKind::ScalarVar(name),
14249                            line,
14250                        }
14251                    };
14252                    parts.push(StringPart::Expr(Expr {
14253                        kind: ExprKind::Deref {
14254                            expr: Box::new(inner_expr),
14255                            kind: Sigil::Array,
14256                        },
14257                        line,
14258                    }));
14259                    continue 'istr;
14260                }
14261                if next == '{' {
14262                    if !literal.is_empty() {
14263                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14264                    }
14265                    i += 2; // `@{`
14266                    let start = i;
14267                    let mut depth = 1usize;
14268                    while i < chars.len() && depth > 0 {
14269                        match chars[i] {
14270                            '{' => depth += 1,
14271                            '}' => {
14272                                depth -= 1;
14273                                if depth == 0 {
14274                                    break;
14275                                }
14276                            }
14277                            _ => {}
14278                        }
14279                        i += 1;
14280                    }
14281                    if depth != 0 {
14282                        return Err(
14283                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
14284                        );
14285                    }
14286                    let inner: String = chars[start..i].iter().collect();
14287                    i += 1; // closing `}`
14288                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
14289                    parts.push(StringPart::Expr(Expr {
14290                        kind: ExprKind::Deref {
14291                            expr: Box::new(inner_expr),
14292                            kind: Sigil::Array,
14293                        },
14294                        line,
14295                    }));
14296                    continue 'istr;
14297                }
14298                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
14299                    literal.push(chars[i]);
14300                    i += 1;
14301                } else {
14302                    if !literal.is_empty() {
14303                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14304                    }
14305                    i += 1;
14306                    let mut name = String::new();
14307                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
14308                        name.push(chars[i]);
14309                        i += 1;
14310                    } else {
14311                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
14312                            name.push(chars[i]);
14313                            i += 1;
14314                        }
14315                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14316                            name.push_str("::");
14317                            i += 2;
14318                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
14319                            {
14320                                name.push(chars[i]);
14321                                i += 1;
14322                            }
14323                        }
14324                    }
14325                    if i < chars.len() && chars[i] == '[' {
14326                        i += 1;
14327                        let start_inner = i;
14328                        let mut depth = 1usize;
14329                        while i < chars.len() && depth > 0 {
14330                            match chars[i] {
14331                                '[' => depth += 1,
14332                                ']' => depth -= 1,
14333                                _ => {}
14334                            }
14335                            if depth == 0 {
14336                                let inner: String = chars[start_inner..i].iter().collect();
14337                                i += 1; // closing ]
14338                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
14339                                parts.push(StringPart::Expr(Expr {
14340                                    kind: ExprKind::ArraySlice {
14341                                        array: name.clone(),
14342                                        indices,
14343                                    },
14344                                    line,
14345                                }));
14346                                continue 'istr;
14347                            }
14348                            i += 1;
14349                        }
14350                        return Err(self.syntax_err(
14351                            "Unterminated [ in array slice inside quoted string",
14352                            line,
14353                        ));
14354                    }
14355                    parts.push(StringPart::ArrayVar(name));
14356                }
14357            } else if chars[i] == '#'
14358                && i + 1 < chars.len()
14359                && chars[i + 1] == '{'
14360                && !crate::compat_mode()
14361            {
14362                // #{expr} — Ruby-style expression interpolation (stryke extension).
14363                if !literal.is_empty() {
14364                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14365                }
14366                i += 2; // skip `#{`
14367                let mut inner = String::new();
14368                let mut depth = 1usize;
14369                while i < chars.len() && depth > 0 {
14370                    match chars[i] {
14371                        '{' => depth += 1,
14372                        '}' => {
14373                            depth -= 1;
14374                            if depth == 0 {
14375                                break;
14376                            }
14377                        }
14378                        _ => {}
14379                    }
14380                    inner.push(chars[i]);
14381                    i += 1;
14382                }
14383                if i < chars.len() {
14384                    i += 1; // skip closing `}`
14385                }
14386                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
14387                parts.push(StringPart::Expr(expr));
14388            } else {
14389                literal.push(chars[i]);
14390                i += 1;
14391            }
14392        }
14393        if !literal.is_empty() {
14394            parts.push(StringPart::Literal(literal));
14395        }
14396
14397        if parts.len() == 1 {
14398            if let StringPart::Literal(s) = &parts[0] {
14399                return Ok(Expr {
14400                    kind: ExprKind::String(s.clone()),
14401                    line,
14402                });
14403            }
14404        }
14405        if parts.is_empty() {
14406            return Ok(Expr {
14407                kind: ExprKind::String(String::new()),
14408                line,
14409            });
14410        }
14411
14412        Ok(Expr {
14413            kind: ExprKind::InterpolatedString(parts),
14414            line,
14415        })
14416    }
14417
14418    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
14419        match &e.kind {
14420            ExprKind::String(s) => Ok(s.clone()),
14421            _ => Err(self.syntax_err(
14422                "overload key must be a string literal (e.g. '\"\"' or '+')",
14423                e.line,
14424            )),
14425        }
14426    }
14427
14428    fn expr_to_overload_sub(&self, e: &Expr) -> PerlResult<String> {
14429        match &e.kind {
14430            ExprKind::String(s) => Ok(s.clone()),
14431            ExprKind::Integer(n) => Ok(n.to_string()),
14432            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
14433            _ => Err(self.syntax_err(
14434                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
14435                e.line,
14436            )),
14437        }
14438    }
14439}
14440
14441fn merge_expr_list(parts: Vec<Expr>) -> Expr {
14442    if parts.len() == 1 {
14443        parts.into_iter().next().unwrap()
14444    } else {
14445        let line = parts.first().map(|e| e.line).unwrap_or(0);
14446        Expr {
14447            kind: ExprKind::List(parts),
14448            line,
14449        }
14450    }
14451}
14452
14453/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
14454pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
14455    let mut lexer = Lexer::new_with_file(s, file);
14456    let tokens = lexer.tokenize()?;
14457    let mut parser = Parser::new_with_file(tokens, file);
14458    let e = parser.parse_expression()?;
14459    if !parser.at_eof() {
14460        return Err(parser.syntax_err(
14461            "Extra tokens in embedded string expression",
14462            parser.peek_line(),
14463        ));
14464    }
14465    Ok(e)
14466}
14467
14468/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
14469pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
14470    let mut lexer = Lexer::new_with_file(s, file);
14471    let tokens = lexer.tokenize()?;
14472    let mut parser = Parser::new_with_file(tokens, file);
14473    let stmts = parser.parse_statements()?;
14474    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
14475    let inner = Expr {
14476        kind: ExprKind::CodeRef {
14477            params: vec![],
14478            body: stmts,
14479        },
14480        line: inner_line,
14481    };
14482    Ok(Expr {
14483        kind: ExprKind::Do(Box::new(inner)),
14484        line,
14485    })
14486}
14487
14488/// Comma-separated expressions on a `format` value line (below a picture line).
14489/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
14490pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
14491    let mut lexer = Lexer::new_with_file(s, file);
14492    let tokens = lexer.tokenize()?;
14493    let mut parser = Parser::new_with_file(tokens, file);
14494    parser.parse_arg_list()
14495}
14496
14497pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
14498    let trimmed = line.trim();
14499    if trimmed.is_empty() {
14500        return Ok(vec![]);
14501    }
14502    let mut lexer = Lexer::new(trimmed);
14503    let tokens = lexer.tokenize()?;
14504    let mut parser = Parser::new(tokens);
14505    let mut exprs = Vec::new();
14506    loop {
14507        if parser.at_eof() {
14508            break;
14509        }
14510        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
14511        exprs.push(parser.parse_assign_expr()?);
14512        if parser.eat(&Token::Comma) {
14513            continue;
14514        }
14515        if !parser.at_eof() {
14516            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
14517        }
14518        break;
14519    }
14520    Ok(exprs)
14521}
14522
14523#[cfg(test)]
14524mod tests {
14525    use super::*;
14526
14527    fn parse_ok(code: &str) -> Program {
14528        let mut lexer = Lexer::new(code);
14529        let tokens = lexer.tokenize().expect("tokenize");
14530        let mut parser = Parser::new(tokens);
14531        parser.parse_program().expect("parse")
14532    }
14533
14534    fn parse_err(code: &str) -> String {
14535        let mut lexer = Lexer::new(code);
14536        let tokens = match lexer.tokenize() {
14537            Ok(t) => t,
14538            Err(e) => return e.message,
14539        };
14540        let mut parser = Parser::new(tokens);
14541        parser.parse_program().unwrap_err().message
14542    }
14543
14544    #[test]
14545    fn parse_empty_program() {
14546        let p = parse_ok("");
14547        assert!(p.statements.is_empty());
14548    }
14549
14550    #[test]
14551    fn parse_semicolons_only() {
14552        let p = parse_ok(";;");
14553        assert!(p.statements.len() <= 3);
14554    }
14555
14556    #[test]
14557    fn parse_simple_scalar_assignment() {
14558        let p = parse_ok("$x = 1");
14559        assert_eq!(p.statements.len(), 1);
14560    }
14561
14562    #[test]
14563    fn parse_simple_array_assignment() {
14564        let p = parse_ok("@arr = (1, 2, 3)");
14565        assert_eq!(p.statements.len(), 1);
14566    }
14567
14568    #[test]
14569    fn parse_simple_hash_assignment() {
14570        let p = parse_ok("%h = (a => 1, b => 2)");
14571        assert_eq!(p.statements.len(), 1);
14572    }
14573
14574    #[test]
14575    fn parse_subroutine_decl() {
14576        let p = parse_ok("fn foo { 1 }");
14577        assert_eq!(p.statements.len(), 1);
14578        match &p.statements[0].kind {
14579            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
14580            _ => panic!("expected SubDecl"),
14581        }
14582    }
14583
14584    #[test]
14585    fn parse_subroutine_with_prototype() {
14586        let p = parse_ok("fn foo ($$) { 1 }");
14587        assert_eq!(p.statements.len(), 1);
14588        match &p.statements[0].kind {
14589            StmtKind::SubDecl { prototype, .. } => {
14590                assert!(prototype.is_some());
14591            }
14592            _ => panic!("expected SubDecl"),
14593        }
14594    }
14595
14596    #[test]
14597    fn parse_anonymous_fn() {
14598        let p = parse_ok("my $f = fn { 1 }");
14599        assert_eq!(p.statements.len(), 1);
14600    }
14601
14602    #[test]
14603    fn parse_if_statement() {
14604        let p = parse_ok("if (1) { 2 }");
14605        assert_eq!(p.statements.len(), 1);
14606        matches!(&p.statements[0].kind, StmtKind::If { .. });
14607    }
14608
14609    #[test]
14610    fn parse_if_elsif_else() {
14611        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
14612        assert_eq!(p.statements.len(), 1);
14613    }
14614
14615    #[test]
14616    fn parse_unless_statement() {
14617        let p = parse_ok("unless (0) { 1 }");
14618        assert_eq!(p.statements.len(), 1);
14619    }
14620
14621    #[test]
14622    fn parse_while_loop() {
14623        let p = parse_ok("while ($x) { $x-- }");
14624        assert_eq!(p.statements.len(), 1);
14625    }
14626
14627    #[test]
14628    fn parse_until_loop() {
14629        let p = parse_ok("until ($x) { $x++ }");
14630        assert_eq!(p.statements.len(), 1);
14631    }
14632
14633    #[test]
14634    fn parse_for_c_style() {
14635        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
14636        assert_eq!(p.statements.len(), 1);
14637    }
14638
14639    #[test]
14640    fn parse_foreach_loop() {
14641        let p = parse_ok("foreach my $x (@arr) { 1 }");
14642        assert_eq!(p.statements.len(), 1);
14643    }
14644
14645    #[test]
14646    fn parse_loop_with_label() {
14647        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
14648        assert_eq!(p.statements.len(), 1);
14649        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
14650    }
14651
14652    #[test]
14653    fn parse_begin_block() {
14654        let p = parse_ok("BEGIN { 1 }");
14655        assert_eq!(p.statements.len(), 1);
14656        matches!(&p.statements[0].kind, StmtKind::Begin(_));
14657    }
14658
14659    #[test]
14660    fn parse_end_block() {
14661        let p = parse_ok("END { 1 }");
14662        assert_eq!(p.statements.len(), 1);
14663        matches!(&p.statements[0].kind, StmtKind::End(_));
14664    }
14665
14666    #[test]
14667    fn parse_package_statement() {
14668        let p = parse_ok("package Foo::Bar");
14669        assert_eq!(p.statements.len(), 1);
14670        match &p.statements[0].kind {
14671            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
14672            _ => panic!("expected Package"),
14673        }
14674    }
14675
14676    #[test]
14677    fn parse_use_statement() {
14678        let p = parse_ok("use strict");
14679        assert_eq!(p.statements.len(), 1);
14680    }
14681
14682    #[test]
14683    fn parse_no_statement() {
14684        let p = parse_ok("no warnings");
14685        assert_eq!(p.statements.len(), 1);
14686    }
14687
14688    #[test]
14689    fn parse_require_bareword() {
14690        let p = parse_ok("require Foo::Bar");
14691        assert_eq!(p.statements.len(), 1);
14692    }
14693
14694    #[test]
14695    fn parse_require_string() {
14696        let p = parse_ok(r#"require "foo.pl""#);
14697        assert_eq!(p.statements.len(), 1);
14698    }
14699
14700    #[test]
14701    fn parse_eval_block() {
14702        let p = parse_ok("eval { 1 }");
14703        assert_eq!(p.statements.len(), 1);
14704    }
14705
14706    #[test]
14707    fn parse_eval_string() {
14708        let p = parse_ok(r#"eval "1 + 2""#);
14709        assert_eq!(p.statements.len(), 1);
14710    }
14711
14712    #[test]
14713    fn parse_qw_word_list() {
14714        let p = parse_ok("my @a = qw(foo bar baz)");
14715        assert_eq!(p.statements.len(), 1);
14716    }
14717
14718    #[test]
14719    fn parse_q_string() {
14720        let p = parse_ok("my $s = q{hello}");
14721        assert_eq!(p.statements.len(), 1);
14722    }
14723
14724    #[test]
14725    fn parse_qq_string() {
14726        let p = parse_ok(r#"my $s = qq(hello $x)"#);
14727        assert_eq!(p.statements.len(), 1);
14728    }
14729
14730    #[test]
14731    fn parse_regex_match() {
14732        let p = parse_ok(r#"$x =~ /foo/"#);
14733        assert_eq!(p.statements.len(), 1);
14734    }
14735
14736    #[test]
14737    fn parse_regex_substitution() {
14738        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
14739        assert_eq!(p.statements.len(), 1);
14740    }
14741
14742    #[test]
14743    fn parse_transliterate() {
14744        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
14745        assert_eq!(p.statements.len(), 1);
14746    }
14747
14748    #[test]
14749    fn parse_ternary_operator() {
14750        let p = parse_ok("my $x = $a ? 1 : 2");
14751        assert_eq!(p.statements.len(), 1);
14752    }
14753
14754    #[test]
14755    fn parse_arrow_method_call() {
14756        let p = parse_ok("$obj->method()");
14757        assert_eq!(p.statements.len(), 1);
14758    }
14759
14760    #[test]
14761    fn parse_arrow_deref_hash() {
14762        let p = parse_ok("$r->{key}");
14763        assert_eq!(p.statements.len(), 1);
14764    }
14765
14766    #[test]
14767    fn parse_arrow_deref_array() {
14768        let p = parse_ok("$r->[0]");
14769        assert_eq!(p.statements.len(), 1);
14770    }
14771
14772    #[test]
14773    fn parse_chained_arrow_deref() {
14774        let p = parse_ok("$r->{a}[0]{b}");
14775        assert_eq!(p.statements.len(), 1);
14776    }
14777
14778    #[test]
14779    fn parse_my_multiple_vars() {
14780        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
14781        assert_eq!(p.statements.len(), 1);
14782    }
14783
14784    #[test]
14785    fn parse_our_scalar() {
14786        let p = parse_ok("our $VERSION = '1.0'");
14787        assert_eq!(p.statements.len(), 1);
14788    }
14789
14790    #[test]
14791    fn parse_local_scalar() {
14792        let p = parse_ok("local $/ = undef");
14793        assert_eq!(p.statements.len(), 1);
14794    }
14795
14796    #[test]
14797    fn parse_state_variable() {
14798        let p = parse_ok("fn my_counter { state $n = 0; $n++ }");
14799        assert_eq!(p.statements.len(), 1);
14800    }
14801
14802    #[test]
14803    fn parse_postfix_if() {
14804        let p = parse_ok("print 1 if $x");
14805        assert_eq!(p.statements.len(), 1);
14806    }
14807
14808    #[test]
14809    fn parse_postfix_unless() {
14810        let p = parse_ok("die 'error' unless $ok");
14811        assert_eq!(p.statements.len(), 1);
14812    }
14813
14814    #[test]
14815    fn parse_postfix_while() {
14816        let p = parse_ok("$x++ while $x < 10");
14817        assert_eq!(p.statements.len(), 1);
14818    }
14819
14820    #[test]
14821    fn parse_postfix_for() {
14822        let p = parse_ok("print for @arr");
14823        assert_eq!(p.statements.len(), 1);
14824    }
14825
14826    #[test]
14827    fn parse_last_next_redo() {
14828        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
14829        assert_eq!(p.statements.len(), 1);
14830    }
14831
14832    #[test]
14833    fn parse_return_statement() {
14834        let p = parse_ok("fn foo { return 42 }");
14835        assert_eq!(p.statements.len(), 1);
14836    }
14837
14838    #[test]
14839    fn parse_wantarray() {
14840        let p = parse_ok("fn foo { wantarray ? @a : $a }");
14841        assert_eq!(p.statements.len(), 1);
14842    }
14843
14844    #[test]
14845    fn parse_caller_builtin() {
14846        let p = parse_ok("my @c = caller");
14847        assert_eq!(p.statements.len(), 1);
14848    }
14849
14850    #[test]
14851    fn parse_ref_to_array() {
14852        let p = parse_ok("my $r = \\@arr");
14853        assert_eq!(p.statements.len(), 1);
14854    }
14855
14856    #[test]
14857    fn parse_ref_to_hash() {
14858        let p = parse_ok("my $r = \\%hash");
14859        assert_eq!(p.statements.len(), 1);
14860    }
14861
14862    #[test]
14863    fn parse_ref_to_scalar() {
14864        let p = parse_ok("my $r = \\$x");
14865        assert_eq!(p.statements.len(), 1);
14866    }
14867
14868    #[test]
14869    fn parse_deref_scalar() {
14870        let p = parse_ok("my $v = $$r");
14871        assert_eq!(p.statements.len(), 1);
14872    }
14873
14874    #[test]
14875    fn parse_deref_array() {
14876        let p = parse_ok("my @a = @$r");
14877        assert_eq!(p.statements.len(), 1);
14878    }
14879
14880    #[test]
14881    fn parse_deref_hash() {
14882        let p = parse_ok("my %h = %$r");
14883        assert_eq!(p.statements.len(), 1);
14884    }
14885
14886    #[test]
14887    fn parse_blessed_ref() {
14888        let p = parse_ok("bless $r, 'Foo'");
14889        assert_eq!(p.statements.len(), 1);
14890    }
14891
14892    #[test]
14893    fn parse_heredoc_basic() {
14894        let p = parse_ok("my $s = <<END;\nfoo\nEND");
14895        assert_eq!(p.statements.len(), 1);
14896    }
14897
14898    #[test]
14899    fn parse_heredoc_quoted() {
14900        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
14901        assert_eq!(p.statements.len(), 1);
14902    }
14903
14904    #[test]
14905    fn parse_do_block() {
14906        let p = parse_ok("my $x = do { 1 + 2 }");
14907        assert_eq!(p.statements.len(), 1);
14908    }
14909
14910    #[test]
14911    fn parse_do_file() {
14912        let p = parse_ok(r#"do "foo.pl""#);
14913        assert_eq!(p.statements.len(), 1);
14914    }
14915
14916    #[test]
14917    fn parse_map_expression() {
14918        let p = parse_ok("my @b = map { $_ * 2 } @a");
14919        assert_eq!(p.statements.len(), 1);
14920    }
14921
14922    #[test]
14923    fn parse_grep_expression() {
14924        let p = parse_ok("my @b = grep { $_ > 0 } @a");
14925        assert_eq!(p.statements.len(), 1);
14926    }
14927
14928    #[test]
14929    fn parse_sort_expression() {
14930        let p = parse_ok("my @b = sort { $a <=> $b } @a");
14931        assert_eq!(p.statements.len(), 1);
14932    }
14933
14934    #[test]
14935    fn parse_pipe_forward() {
14936        let p = parse_ok("@a |> map { $_ * 2 }");
14937        assert_eq!(p.statements.len(), 1);
14938    }
14939
14940    #[test]
14941    fn parse_expression_from_str_simple() {
14942        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
14943        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
14944    }
14945
14946    #[test]
14947    fn parse_expression_from_str_extra_tokens_error() {
14948        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
14949        assert!(err.message.contains("Extra tokens"));
14950    }
14951
14952    #[test]
14953    fn parse_slice_indices_from_str_basic() {
14954        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
14955        assert_eq!(indices.len(), 3);
14956    }
14957
14958    #[test]
14959    fn parse_format_value_line_empty() {
14960        let exprs = parse_format_value_line("").unwrap();
14961        assert!(exprs.is_empty());
14962    }
14963
14964    #[test]
14965    fn parse_format_value_line_single() {
14966        let exprs = parse_format_value_line("$x").unwrap();
14967        assert_eq!(exprs.len(), 1);
14968    }
14969
14970    #[test]
14971    fn parse_format_value_line_multiple() {
14972        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
14973        assert_eq!(exprs.len(), 3);
14974    }
14975
14976    #[test]
14977    fn parse_unclosed_brace_error() {
14978        let err = parse_err("fn foo {");
14979        assert!(!err.is_empty());
14980    }
14981
14982    #[test]
14983    fn parse_unclosed_paren_error() {
14984        let err = parse_err("print (1, 2");
14985        assert!(!err.is_empty());
14986    }
14987
14988    #[test]
14989    fn parse_invalid_statement_error() {
14990        let err = parse_err("???");
14991        assert!(!err.is_empty());
14992    }
14993
14994    #[test]
14995    fn merge_expr_list_single() {
14996        let e = Expr {
14997            kind: ExprKind::Integer(1),
14998            line: 1,
14999        };
15000        let merged = merge_expr_list(vec![e.clone()]);
15001        matches!(merged.kind, ExprKind::Integer(1));
15002    }
15003
15004    #[test]
15005    fn merge_expr_list_multiple() {
15006        let e1 = Expr {
15007            kind: ExprKind::Integer(1),
15008            line: 1,
15009        };
15010        let e2 = Expr {
15011            kind: ExprKind::Integer(2),
15012            line: 1,
15013        };
15014        let merged = merge_expr_list(vec![e1, e2]);
15015        matches!(merged.kind, ExprKind::List(_));
15016    }
15017}