Skip to main content

stryke/
parser.rs

1use crate::ast::*;
2use crate::error::{ErrorKind, PerlError, PerlResult};
3use crate::vm_helper::VMHelper;
4use crate::lexer::{Lexer, LITERAL_AT_IN_DQUOTE, 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    /// Counter (depth-tracked like [`Self::suppress_colon_range`]) that
127    /// disables `~` as a range separator. Used inside paired `~...~` char-
128    /// index/slice subscripts so the closing `~` doesn't get eaten as a
129    /// range op. `:` range is still allowed inside (e.g. `$_~1:3~` is a
130    /// slice with a `:` range as the index).
131    suppress_tilde_range: u32,
132    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
133    /// instead of thread-first (prepend). Set by `->>` thread macro.
134    thread_last_mode: bool,
135    /// When true, we're parsing a module (via `use`/`require`), not user code.
136    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
137    pub parsing_module: bool,
138    /// `self.pos` immediately after consuming a paren-list close (`(EXPR)`,
139    /// `(EXPR, …)`, `()`) or `qw(…)` in `parse_primary`. The `x` operator
140    /// reads this at parse time to distinguish `(LIST) x N` (list repetition)
141    /// from `EXPR x N` (scalar string repetition). The compare is exact: any
142    /// postfix consumption (`->method()`, `[idx]`, …) advances `self.pos`
143    /// past this checkpoint, so list-repeat fires only when `x` is the very
144    /// next token after the closing paren.
145    list_construct_close_pos: Option<usize>,
146    /// Synthetic SubDecl statements queued by anonymous-sub overload handlers
147    /// (`use overload "+" => sub { ... }`) — drained at the end of
148    /// [`Self::parse_program`] and prepended to the top-level statements so
149    /// the package-qualified synthetic name resolves at runtime. (PARITY-012)
150    pending_synthetic_subs: Vec<Statement>,
151    /// Counter for unique anonymous-overload-handler names.
152    next_overload_anon_id: u32,
153}
154
155impl Parser {
156    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
157        Self::new_with_file(tokens, "-e")
158    }
159
160    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
161        Self {
162            tokens,
163            pos: 0,
164            next_rate_limit_slot: 0,
165            suppress_indirect_paren_call: 0,
166            pipe_rhs_depth: 0,
167            no_pipe_forward_depth: 0,
168            suppress_scalar_hash_brace: 0,
169            next_desugar_tmp: 0,
170            error_file: file.into(),
171            declared_subs: std::collections::HashSet::new(),
172            suppress_parenless_call: 0,
173            suppress_slash_as_div: 0,
174            suppress_m_regex: 0,
175            suppress_colon_range: 0,
176            suppress_tilde_range: 0,
177            thread_last_mode: false,
178            pending_synthetic_subs: Vec::new(),
179            next_overload_anon_id: 0,
180            parsing_module: false,
181            list_construct_close_pos: None,
182        }
183    }
184
185    fn alloc_desugar_tmp(&mut self) -> u32 {
186        let n = self.next_desugar_tmp;
187        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
188        n
189    }
190
191    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
192    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
193    /// placeholder list instead of erroring on a missing operand.
194    #[inline]
195    fn in_pipe_rhs(&self) -> bool {
196        self.pipe_rhs_depth > 0
197    }
198
199    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
200    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
201    fn pipe_supplies_slurped_list_operand(&self) -> bool {
202        self.in_pipe_rhs()
203            && (matches!(
204                self.peek(),
205                Token::Semicolon
206                    | Token::RBrace
207                    | Token::RParen
208                    | Token::Eof
209                    | Token::Comma
210                    | Token::PipeForward
211            ) || self.peek_line() > self.prev_line())
212    }
213
214    /// Empty placeholder list used as a stand-in for the list operand of
215    /// list-taking builtins when they appear on the RHS of `|>`.
216    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
217    /// value at desugar time, so the placeholder is never evaluated.
218    #[inline]
219    fn pipe_placeholder_list(&self, line: usize) -> Expr {
220        Expr {
221            kind: ExprKind::List(vec![]),
222            line,
223        }
224    }
225
226    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
227    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
228    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
229    /// `@a |> NAME { ... }` route through the same substitution.
230    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
231        matches!(
232            name,
233            "pfirst"
234                | "pany"
235                | "any"
236                | "all"
237                | "none"
238                | "first"
239                | "take_while"
240                | "drop_while"
241                | "skip_while"
242                | "reject"
243                | "tap"
244                | "peek"
245                | "group_by"
246                | "chunk_by"
247                | "partition"
248                | "min_by"
249                | "max_by"
250                | "zip_with"
251                | "count_by"
252        )
253    }
254
255    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
256    ///
257    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
258    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
259    /// element instead of stringifying the bareword.  Non-bareword expressions
260    /// pass through unchanged.
261    ///
262    /// Also injects `$_` into known builtins that were parsed with zero
263    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
264    /// topic variable instead of being no-ops.
265    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
266        let line = expr.line;
267        let topic = || Expr {
268            kind: ExprKind::ScalarVar("_".into()),
269            line,
270        };
271        match expr.kind {
272            ExprKind::Bareword(ref name) => Expr {
273                kind: ExprKind::FuncCall {
274                    name: name.clone(),
275                    args: vec![topic()],
276                },
277                line,
278            },
279            // Builtins that take Vec<Expr> args — inject $_ when empty.
280            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
281                kind: ExprKind::Unlink(vec![topic()]),
282                line,
283            },
284            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
285                kind: ExprKind::Chmod(vec![topic()]),
286                line,
287            },
288            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
289            ExprKind::Stat(_) => expr,
290            ExprKind::Lstat(_) => expr,
291            ExprKind::Readlink(_) => expr,
292            // rev with empty list should use $_
293            ExprKind::Rev(ref inner) => {
294                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
295                    Expr {
296                        kind: ExprKind::Rev(Box::new(topic())),
297                        line,
298                    }
299                } else {
300                    expr
301                }
302            }
303            _ => expr,
304        }
305    }
306
307    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
308    /// duration, so any trailing `|>` is left to the enclosing parser instead
309    /// of being absorbed into this sub-expression. Used by paren-less arg
310    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
311    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
312    /// left-associatively instead of letting `head`'s first arg swallow the
313    /// outer `|>`. The counter is restored on both success and error paths.
314    fn parse_assign_expr_stop_at_pipe(&mut self) -> PerlResult<Expr> {
315        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
316        let r = self.parse_assign_expr();
317        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
318        r
319    }
320
321    fn syntax_err(&self, message: impl Into<String>, line: usize) -> PerlError {
322        PerlError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
323    }
324
325    fn alloc_rate_limit_slot(&mut self) -> u32 {
326        let s = self.next_rate_limit_slot;
327        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
328        s
329    }
330
331    fn peek(&self) -> &Token {
332        self.tokens
333            .get(self.pos)
334            .map(|(t, _)| t)
335            .unwrap_or(&Token::Eof)
336    }
337
338    fn peek_line(&self) -> usize {
339        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
340    }
341
342    fn peek_at(&self, offset: usize) -> &Token {
343        self.tokens
344            .get(self.pos + offset)
345            .map(|(t, _)| t)
346            .unwrap_or(&Token::Eof)
347    }
348
349    fn advance(&mut self) -> (Token, usize) {
350        let tok = self
351            .tokens
352            .get(self.pos)
353            .cloned()
354            .unwrap_or((Token::Eof, 0));
355        self.pos += 1;
356        tok
357    }
358
359    /// Line number of the most recently consumed token (the token at `pos - 1`).
360    fn prev_line(&self) -> usize {
361        if self.pos > 0 {
362            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
363        } else {
364            0
365        }
366    }
367
368    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
369    /// Heuristics (assuming current token is `{`):
370    /// - `{ bareword =>` → hashref
371    /// - `{ "string" =>` → hashref
372    /// - `{ $var =>` → hashref
373    /// - `{ 0 =>` → hashref (numeric key)
374    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
375    /// - `{ }` (empty) → hashref
376    fn looks_like_hashref(&self) -> bool {
377        debug_assert!(matches!(self.peek(), Token::LBrace));
378        let tok1 = self.peek_at(1);
379        let tok2 = self.peek_at(2);
380        match tok1 {
381            Token::RBrace => true,
382            Token::Ident(_)
383            | Token::SingleString(_)
384            | Token::DoubleString(_)
385            | Token::ScalarVar(_)
386            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
387            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
388            _ => false,
389        }
390    }
391
392    fn expect(&mut self, expected: &Token) -> PerlResult<usize> {
393        let (tok, line) = self.advance();
394        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
395            Ok(line)
396        } else {
397            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
398        }
399    }
400
401    fn eat(&mut self, expected: &Token) -> bool {
402        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
403            self.advance();
404            true
405        } else {
406            false
407        }
408    }
409
410    fn at_eof(&self) -> bool {
411        matches!(self.peek(), Token::Eof)
412    }
413
414    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
415    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
416        matches!(
417            tok,
418            Token::RParen
419                | Token::Semicolon
420                | Token::Comma
421                | Token::RBrace
422                | Token::Eof
423                | Token::LogAnd
424                | Token::LogOr
425                | Token::LogAndWord
426                | Token::LogOrWord
427                | Token::PipeForward
428        )
429    }
430
431    /// True when the next token is a statement-starting keyword on a *different*
432    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
433    /// import lists when semicolons are omitted (stryke extension).
434    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
435        // Semicolons-optional is a stryke extension; in compat mode, require them.
436        if crate::compat_mode() {
437            return false;
438        }
439        if self.peek_line() == stmt_line {
440            return false;
441        }
442        matches!(
443            self.peek(),
444            Token::Ident(ref kw) if matches!(kw.as_str(),
445                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
446                | "if" | "unless" | "while" | "until" | "for" | "foreach"
447                | "return" | "last" | "next" | "redo" | "package" | "require"
448                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
449            )
450        )
451    }
452
453    /// True when the next token is on a different line from `stmt_line` and could
454    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
455    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
456    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
457        if crate::compat_mode() {
458            return false;
459        }
460        if self.peek_line() == stmt_line {
461            return false;
462        }
463        matches!(
464            self.peek(),
465            Token::ScalarVar(_)
466                | Token::DerefScalarVar(_)
467                | Token::ArrayVar(_)
468                | Token::HashVar(_)
469                | Token::LBrace
470        ) || self.next_is_new_stmt_keyword(stmt_line)
471    }
472
473    // ── Top level ──
474
475    pub fn parse_program(&mut self) -> PerlResult<Program> {
476        let mut statements = self.parse_statements()?;
477        // Prepend any synthetic SubDecl stubs queued by anonymous overload
478        // handlers so the package-qualified synthetic names resolve when the
479        // overload table is consulted at runtime. (PARITY-012)
480        if !self.pending_synthetic_subs.is_empty() {
481            let synthetics = std::mem::take(&mut self.pending_synthetic_subs);
482            let mut combined = Vec::with_capacity(synthetics.len() + statements.len());
483            combined.extend(synthetics);
484            combined.extend(statements.drain(..));
485            statements = combined;
486        }
487        Ok(Program { statements })
488    }
489
490    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
491    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
492        let mut statements = Vec::new();
493        while !self.at_eof() {
494            if matches!(self.peek(), Token::Semicolon) {
495                let line = self.peek_line();
496                self.advance();
497                statements.push(Statement {
498                    label: None,
499                    kind: StmtKind::Empty,
500                    line,
501                });
502                continue;
503            }
504            statements.push(self.parse_statement()?);
505        }
506        Ok(statements)
507    }
508
509    // ── Statements ──
510
511    fn parse_statement(&mut self) -> PerlResult<Statement> {
512        let line = self.peek_line();
513
514        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
515        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
516        let label = match self.peek().clone() {
517            Token::Ident(_) => {
518                if matches!(self.peek_at(1), Token::Colon)
519                    && !matches!(self.peek_at(2), Token::Colon)
520                {
521                    let (tok, _) = self.advance();
522                    let l = match tok {
523                        Token::Ident(l) => l,
524                        _ => unreachable!(),
525                    };
526                    self.advance(); // ':'
527                    Some(l)
528                } else {
529                    None
530                }
531            }
532            _ => None,
533        };
534
535        let mut stmt = match self.peek().clone() {
536            Token::FormatDecl { .. } => {
537                let tok_line = self.peek_line();
538                let (tok, _) = self.advance();
539                match tok {
540                    Token::FormatDecl { name, lines } => Statement {
541                        label: label.clone(),
542                        kind: StmtKind::FormatDecl { name, lines },
543                        line: tok_line,
544                    },
545                    _ => unreachable!(),
546                }
547            }
548            Token::Ident(ref kw) => match kw.as_str() {
549                "if" => self.parse_if()?,
550                "unless" => self.parse_unless()?,
551                "while" => {
552                    let mut s = self.parse_while()?;
553                    if let StmtKind::While {
554                        label: ref mut lbl, ..
555                    } = s.kind
556                    {
557                        *lbl = label.clone();
558                    }
559                    s
560                }
561                "until" => {
562                    let mut s = self.parse_until()?;
563                    if let StmtKind::Until {
564                        label: ref mut lbl, ..
565                    } = s.kind
566                    {
567                        *lbl = label.clone();
568                    }
569                    s
570                }
571                "for" => {
572                    let mut s = self.parse_for_or_foreach()?;
573                    match s.kind {
574                        StmtKind::For {
575                            label: ref mut lbl, ..
576                        }
577                        | StmtKind::Foreach {
578                            label: ref mut lbl, ..
579                        } => *lbl = label.clone(),
580                        _ => {}
581                    }
582                    s
583                }
584                "foreach" => {
585                    let mut s = self.parse_foreach()?;
586                    if let StmtKind::Foreach {
587                        label: ref mut lbl, ..
588                    } = s.kind
589                    {
590                        *lbl = label.clone();
591                    }
592                    s
593                }
594                "sub" => {
595                    if crate::no_interop_mode() {
596                        return Err(self.syntax_err(
597                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
598                            self.peek_line(),
599                        ));
600                    }
601                    self.parse_sub_decl(true)?
602                }
603                "fn" => self.parse_sub_decl(false)?,
604                "struct" => {
605                    if crate::compat_mode() {
606                        return Err(self.syntax_err(
607                            "`struct` is a stryke extension (disabled by --compat)",
608                            self.peek_line(),
609                        ));
610                    }
611                    self.parse_struct_decl()?
612                }
613                "enum" => {
614                    if crate::compat_mode() {
615                        return Err(self.syntax_err(
616                            "`enum` is a stryke extension (disabled by --compat)",
617                            self.peek_line(),
618                        ));
619                    }
620                    self.parse_enum_decl()?
621                }
622                "class" => {
623                    if crate::compat_mode() {
624                        // TODO: parse Perl 5.38 class syntax with :isa()
625                        return Err(self.syntax_err(
626                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
627                            self.peek_line(),
628                        ));
629                    }
630                    self.parse_class_decl(false, false)?
631                }
632                "abstract" => {
633                    self.advance(); // abstract
634                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
635                        return Err(self.syntax_err(
636                            "`abstract` must be followed by `class`",
637                            self.peek_line(),
638                        ));
639                    }
640                    self.parse_class_decl(true, false)?
641                }
642                "final" => {
643                    self.advance(); // final
644                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
645                        return Err(self
646                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
647                    }
648                    self.parse_class_decl(false, true)?
649                }
650                "trait" => {
651                    if crate::compat_mode() {
652                        return Err(self.syntax_err(
653                            "`trait` is a stryke extension (disabled by --compat)",
654                            self.peek_line(),
655                        ));
656                    }
657                    self.parse_trait_decl()?
658                }
659                "my" => self.parse_my_our_local("my", false)?,
660                "state" => self.parse_my_our_local("state", false)?,
661                "mysync" => {
662                    if crate::compat_mode() {
663                        return Err(self.syntax_err(
664                            "`mysync` is a stryke extension (disabled by --compat)",
665                            self.peek_line(),
666                        ));
667                    }
668                    self.parse_my_our_local("mysync", false)?
669                }
670                "frozen" | "const" => {
671                    let leading = kw.as_str().to_string();
672                    if crate::compat_mode() {
673                        return Err(self.syntax_err(
674                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
675                            self.peek_line(),
676                        ));
677                    }
678                    // `frozen my $x = val;` / `const my $x = val;` — the
679                    // two spellings are interchangeable (`const` is the
680                    // more-familiar name for new users). Expects `my`
681                    // to follow.
682                    self.advance(); // consume "frozen"/"const"
683                    if let Token::Ident(ref kw) = self.peek().clone() {
684                        if kw == "my" {
685                            let mut stmt = self.parse_my_our_local("my", false)?;
686                            if let StmtKind::My(ref mut decls) = stmt.kind {
687                                for decl in decls.iter_mut() {
688                                    decl.frozen = true;
689                                }
690                            }
691                            stmt
692                        } else {
693                            return Err(self.syntax_err(
694                                format!("Expected 'my' after '{leading}'"),
695                                self.peek_line(),
696                            ));
697                        }
698                    } else {
699                        return Err(self.syntax_err(
700                            format!("Expected 'my' after '{leading}'"),
701                            self.peek_line(),
702                        ));
703                    }
704                }
705                "typed" => {
706                    if crate::compat_mode() {
707                        return Err(self.syntax_err(
708                            "`typed` is a stryke extension (disabled by --compat)",
709                            self.peek_line(),
710                        ));
711                    }
712                    self.advance();
713                    if let Token::Ident(ref kw) = self.peek().clone() {
714                        if kw == "my" {
715                            self.parse_my_our_local("my", true)?
716                        } else {
717                            return Err(
718                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
719                            );
720                        }
721                    } else {
722                        return Err(
723                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
724                        );
725                    }
726                }
727                "our" => self.parse_my_our_local("our", false)?,
728                "local" => self.parse_my_our_local("local", false)?,
729                "package" => self.parse_package()?,
730                "use" => self.parse_use()?,
731                "no" => self.parse_no()?,
732                "return" => self.parse_return()?,
733                "last" => {
734                    self.advance();
735                    let lbl = if let Token::Ident(ref s) = self.peek() {
736                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
737                            let (Token::Ident(l), _) = self.advance() else {
738                                unreachable!()
739                            };
740                            Some(l)
741                        } else {
742                            None
743                        }
744                    } else {
745                        None
746                    };
747                    let stmt = Statement {
748                        label: None,
749                        kind: StmtKind::Last(lbl.or(label.clone())),
750                        line,
751                    };
752                    self.parse_stmt_postfix_modifier(stmt)?
753                }
754                "next" => {
755                    self.advance();
756                    let lbl = if let Token::Ident(ref s) = self.peek() {
757                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
758                            let (Token::Ident(l), _) = self.advance() else {
759                                unreachable!()
760                            };
761                            Some(l)
762                        } else {
763                            None
764                        }
765                    } else {
766                        None
767                    };
768                    let stmt = Statement {
769                        label: None,
770                        kind: StmtKind::Next(lbl.or(label.clone())),
771                        line,
772                    };
773                    self.parse_stmt_postfix_modifier(stmt)?
774                }
775                "redo" => {
776                    self.advance();
777                    self.eat(&Token::Semicolon);
778                    Statement {
779                        label: None,
780                        kind: StmtKind::Redo(label.clone()),
781                        line,
782                    }
783                }
784                "BEGIN" => {
785                    self.advance();
786                    let block = self.parse_block()?;
787                    Statement {
788                        label: None,
789                        kind: StmtKind::Begin(block),
790                        line,
791                    }
792                }
793                "END" => {
794                    self.advance();
795                    let block = self.parse_block()?;
796                    Statement {
797                        label: None,
798                        kind: StmtKind::End(block),
799                        line,
800                    }
801                }
802                "UNITCHECK" => {
803                    self.advance();
804                    let block = self.parse_block()?;
805                    Statement {
806                        label: None,
807                        kind: StmtKind::UnitCheck(block),
808                        line,
809                    }
810                }
811                "CHECK" => {
812                    self.advance();
813                    let block = self.parse_block()?;
814                    Statement {
815                        label: None,
816                        kind: StmtKind::Check(block),
817                        line,
818                    }
819                }
820                "INIT" => {
821                    self.advance();
822                    let block = self.parse_block()?;
823                    Statement {
824                        label: None,
825                        kind: StmtKind::Init(block),
826                        line,
827                    }
828                }
829                "goto" => {
830                    self.advance();
831                    let target = self.parse_expression()?;
832                    let stmt = Statement {
833                        label: None,
834                        kind: StmtKind::Goto {
835                            target: Box::new(target),
836                        },
837                        line,
838                    };
839                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
840                    self.parse_stmt_postfix_modifier(stmt)?
841                }
842                "continue" => {
843                    self.advance();
844                    let block = self.parse_block()?;
845                    Statement {
846                        label: None,
847                        kind: StmtKind::Continue(block),
848                        line,
849                    }
850                }
851                "before"
852                    if matches!(
853                        self.peek_at(1),
854                        Token::SingleString(_) | Token::DoubleString(_)
855                    ) =>
856                {
857                    self.parse_advice_decl(crate::ast::AdviceKind::Before)?
858                }
859                "after"
860                    if matches!(
861                        self.peek_at(1),
862                        Token::SingleString(_) | Token::DoubleString(_)
863                    ) =>
864                {
865                    self.parse_advice_decl(crate::ast::AdviceKind::After)?
866                }
867                "around"
868                    if matches!(
869                        self.peek_at(1),
870                        Token::SingleString(_) | Token::DoubleString(_)
871                    ) =>
872                {
873                    self.parse_advice_decl(crate::ast::AdviceKind::Around)?
874                }
875                "try" => self.parse_try_catch()?,
876                "defer" => self.parse_defer_stmt()?,
877                "tie" => self.parse_tie_stmt()?,
878                "given" => self.parse_given()?,
879                "when" => self.parse_when_stmt()?,
880                "default" => self.parse_default_stmt()?,
881                "eval_timeout" => self.parse_eval_timeout()?,
882                "do" => {
883                    if matches!(self.peek_at(1), Token::LBrace) {
884                        self.advance();
885                        let body = self.parse_block()?;
886                        if let Token::Ident(ref w) = self.peek().clone() {
887                            if w == "while" {
888                                self.advance();
889                                self.expect(&Token::LParen)?;
890                                let mut condition = self.parse_expression()?;
891                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
892                                self.expect(&Token::RParen)?;
893                                self.eat(&Token::Semicolon);
894                                Statement {
895                                    label: label.clone(),
896                                    kind: StmtKind::DoWhile { body, condition },
897                                    line,
898                                }
899                            } else {
900                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
901                                let inner = Expr {
902                                    kind: ExprKind::CodeRef {
903                                        params: vec![],
904                                        body,
905                                    },
906                                    line: inner_line,
907                                };
908                                let expr = Expr {
909                                    kind: ExprKind::Do(Box::new(inner)),
910                                    line,
911                                };
912                                let stmt = Statement {
913                                    label: label.clone(),
914                                    kind: StmtKind::Expression(expr),
915                                    line,
916                                };
917                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
918                                self.parse_stmt_postfix_modifier(stmt)?
919                            }
920                        } else {
921                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
922                            let inner = Expr {
923                                kind: ExprKind::CodeRef {
924                                    params: vec![],
925                                    body,
926                                },
927                                line: inner_line,
928                            };
929                            let expr = Expr {
930                                kind: ExprKind::Do(Box::new(inner)),
931                                line,
932                            };
933                            let stmt = Statement {
934                                label: label.clone(),
935                                kind: StmtKind::Expression(expr),
936                                line,
937                            };
938                            self.parse_stmt_postfix_modifier(stmt)?
939                        }
940                    } else {
941                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
942                            let stmt = self.maybe_postfix_modifier(expr)?;
943                            self.parse_stmt_postfix_modifier(stmt)?
944                        } else {
945                            let expr = self.parse_expression()?;
946                            let stmt = self.maybe_postfix_modifier(expr)?;
947                            self.parse_stmt_postfix_modifier(stmt)?
948                        }
949                    }
950                }
951                _ => {
952                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
953                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
954                        let stmt = self.maybe_postfix_modifier(expr)?;
955                        self.parse_stmt_postfix_modifier(stmt)?
956                    } else {
957                        let expr = self.parse_expression()?;
958                        let stmt = self.maybe_postfix_modifier(expr)?;
959                        self.parse_stmt_postfix_modifier(stmt)?
960                    }
961                }
962            },
963            Token::LBrace => {
964                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
965                // If it looks like a hashref, parse as expression; otherwise parse as block.
966                if self.looks_like_hashref() {
967                    let expr = self.parse_expression()?;
968                    let stmt = self.maybe_postfix_modifier(expr)?;
969                    self.parse_stmt_postfix_modifier(stmt)?
970                } else {
971                    let block = self.parse_block()?;
972                    let stmt = Statement {
973                        label: None,
974                        kind: StmtKind::Block(block),
975                        line,
976                    };
977                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
978                    self.parse_stmt_postfix_modifier(stmt)?
979                }
980            }
981            _ => {
982                let expr = self.parse_expression()?;
983                let stmt = self.maybe_postfix_modifier(expr)?;
984                self.parse_stmt_postfix_modifier(stmt)?
985            }
986        };
987
988        stmt.label = label;
989        Ok(stmt)
990    }
991
992    /// Handle postfix if/unless on statement-level keywords like last/next.
993    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
994        let line = stmt.line;
995        // Implicit semicolon: a modifier keyword on a new line is a new
996        // statement, not a postfix modifier.  This prevents semicolon-less
997        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
998        // as `my $x = "val" if ($x) { ... }`.
999        if self.peek_line() > self.prev_line() {
1000            self.eat(&Token::Semicolon);
1001            return Ok(stmt);
1002        }
1003        if let Token::Ident(ref kw) = self.peek().clone() {
1004            match kw.as_str() {
1005                "if" => {
1006                    self.advance();
1007                    let mut cond = self.parse_expression()?;
1008                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1009                    self.eat(&Token::Semicolon);
1010                    return Ok(Statement {
1011                        label: None,
1012                        kind: StmtKind::If {
1013                            condition: cond,
1014                            body: vec![stmt],
1015                            elsifs: vec![],
1016                            else_block: None,
1017                        },
1018                        line,
1019                    });
1020                }
1021                "unless" => {
1022                    self.advance();
1023                    let mut cond = self.parse_expression()?;
1024                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1025                    self.eat(&Token::Semicolon);
1026                    return Ok(Statement {
1027                        label: None,
1028                        kind: StmtKind::Unless {
1029                            condition: cond,
1030                            body: vec![stmt],
1031                            else_block: None,
1032                        },
1033                        line,
1034                    });
1035                }
1036                "while" | "until" | "for" | "foreach" => {
1037                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
1038                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
1039                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
1040                        let out = self.maybe_postfix_modifier(expr)?;
1041                        self.eat(&Token::Semicolon);
1042                        return Ok(out);
1043                    }
1044                    return Err(self.syntax_err(
1045                        format!("postfix `{}` is not supported on this statement form", kw),
1046                        self.peek_line(),
1047                    ));
1048                }
1049                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
1050                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
1051                    let line = stmt.line;
1052                    let block = self.stmt_into_parallel_block(stmt)?;
1053                    let which = kw.as_str();
1054                    self.advance();
1055                    self.eat(&Token::Comma);
1056                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
1057                    self.eat(&Token::Semicolon);
1058                    let list = Box::new(list);
1059                    let progress = progress.map(Box::new);
1060                    let kind = match which {
1061                        "pmap" => ExprKind::PMapExpr {
1062                            block,
1063                            list,
1064                            progress,
1065                            flat_outputs: false,
1066                            on_cluster: None,
1067                            stream: false,
1068                        },
1069                        "pflat_map" => ExprKind::PMapExpr {
1070                            block,
1071                            list,
1072                            progress,
1073                            flat_outputs: true,
1074                            on_cluster: None,
1075                            stream: false,
1076                        },
1077                        "pgrep" => ExprKind::PGrepExpr {
1078                            block,
1079                            list,
1080                            progress,
1081                            stream: false,
1082                        },
1083                        "pfor" => ExprKind::PForExpr {
1084                            block,
1085                            list,
1086                            progress,
1087                        },
1088                        "preduce" => ExprKind::PReduceExpr {
1089                            block,
1090                            list,
1091                            progress,
1092                        },
1093                        "pcache" => ExprKind::PcacheExpr {
1094                            block,
1095                            list,
1096                            progress,
1097                        },
1098                        _ => unreachable!(),
1099                    };
1100                    return Ok(Statement {
1101                        label: None,
1102                        kind: StmtKind::Expression(Expr { kind, line }),
1103                        line,
1104                    });
1105                }
1106                _ => {}
1107            }
1108        }
1109        self.eat(&Token::Semicolon);
1110        Ok(stmt)
1111    }
1112
1113    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1114    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1115    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
1116        let line = stmt.line;
1117        match stmt.kind {
1118            StmtKind::Block(block) => Ok(block),
1119            StmtKind::Expression(expr) => {
1120                if let ExprKind::Do(ref inner) = expr.kind {
1121                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1122                        return Ok(body.clone());
1123                    }
1124                }
1125                Ok(vec![Statement {
1126                    label: None,
1127                    kind: StmtKind::Expression(expr),
1128                    line,
1129                }])
1130            }
1131            _ => Err(self.syntax_err(
1132                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1133                line,
1134            )),
1135        }
1136    }
1137
1138    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1139    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`](ExprKind::Do)([`CodeRef`](ExprKind::CodeRef))).
1140    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1141        match stmt.kind {
1142            StmtKind::Expression(expr) => Some(expr),
1143            StmtKind::Block(block) => {
1144                let line = stmt.line;
1145                let inner = Expr {
1146                    kind: ExprKind::CodeRef {
1147                        params: vec![],
1148                        body: block,
1149                    },
1150                    line,
1151                };
1152                Some(Expr {
1153                    kind: ExprKind::Do(Box::new(inner)),
1154                    line,
1155                })
1156            }
1157            _ => None,
1158        }
1159    }
1160
1161    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1162    /// (same set as [`parse_list_until_terminator`]).
1163    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1164        matches!(
1165            self.peek(),
1166            Token::Ident(ref kw)
1167                if matches!(
1168                    kw.as_str(),
1169                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1170                )
1171        )
1172    }
1173
1174    /// Token classes whose precedence sits below a Perl-style named unary
1175    /// operator. When one of these is the next token after a unary keyword
1176    /// (`length`, `len`, `cnt`, …), the keyword takes no explicit argument
1177    /// and the surrounding expression continues. Mirrors the `parse_one_arg_or_default`
1178    /// boundary set; kept as a separate predicate so other parse paths can
1179    /// reuse it without committing to default-to-`$_` semantics.
1180    fn peek_is_named_unary_terminator(&self) -> bool {
1181        matches!(
1182            self.peek(),
1183            Token::Semicolon
1184                | Token::RBrace
1185                | Token::RParen
1186                | Token::RBracket
1187                | Token::Eof
1188                | Token::Comma
1189                | Token::FatArrow
1190                | Token::PipeForward
1191                | Token::Question
1192                | Token::Colon
1193                | Token::NumEq
1194                | Token::NumNe
1195                | Token::NumLt
1196                | Token::NumGt
1197                | Token::NumLe
1198                | Token::NumGe
1199                | Token::Spaceship
1200                | Token::StrEq
1201                | Token::StrNe
1202                | Token::StrLt
1203                | Token::StrGt
1204                | Token::StrLe
1205                | Token::StrGe
1206                | Token::StrCmp
1207                | Token::LogAnd
1208                | Token::LogOr
1209                | Token::LogAndWord
1210                | Token::LogOrWord
1211                | Token::DefinedOr
1212                | Token::Range
1213                | Token::RangeExclusive
1214                | Token::Assign
1215                | Token::PlusAssign
1216                | Token::MinusAssign
1217                | Token::MulAssign
1218                | Token::DivAssign
1219                | Token::ModAssign
1220                | Token::PowAssign
1221                | Token::DotAssign
1222                | Token::AndAssign
1223                | Token::OrAssign
1224                | Token::XorAssign
1225                | Token::DefinedOrAssign
1226                | Token::ShiftLeftAssign
1227                | Token::ShiftRightAssign
1228                | Token::BitAndAssign
1229                | Token::BitOrAssign
1230        )
1231    }
1232
1233    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1234        let line = expr.line;
1235        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1236        if self.peek_line() > self.prev_line() {
1237            return Ok(Statement {
1238                label: None,
1239                kind: StmtKind::Expression(expr),
1240                line,
1241            });
1242        }
1243        match self.peek() {
1244            Token::Ident(ref kw) => match kw.as_str() {
1245                "if" => {
1246                    self.advance();
1247                    let cond = self.parse_expression()?;
1248                    Ok(Statement {
1249                        label: None,
1250                        kind: StmtKind::Expression(Expr {
1251                            kind: ExprKind::PostfixIf {
1252                                expr: Box::new(expr),
1253                                condition: Box::new(cond),
1254                            },
1255                            line,
1256                        }),
1257                        line,
1258                    })
1259                }
1260                "unless" => {
1261                    self.advance();
1262                    let cond = self.parse_expression()?;
1263                    Ok(Statement {
1264                        label: None,
1265                        kind: StmtKind::Expression(Expr {
1266                            kind: ExprKind::PostfixUnless {
1267                                expr: Box::new(expr),
1268                                condition: Box::new(cond),
1269                            },
1270                            line,
1271                        }),
1272                        line,
1273                    })
1274                }
1275                "while" => {
1276                    self.advance();
1277                    let cond = self.parse_expression()?;
1278                    Ok(Statement {
1279                        label: None,
1280                        kind: StmtKind::Expression(Expr {
1281                            kind: ExprKind::PostfixWhile {
1282                                expr: Box::new(expr),
1283                                condition: Box::new(cond),
1284                            },
1285                            line,
1286                        }),
1287                        line,
1288                    })
1289                }
1290                "until" => {
1291                    self.advance();
1292                    let cond = self.parse_expression()?;
1293                    Ok(Statement {
1294                        label: None,
1295                        kind: StmtKind::Expression(Expr {
1296                            kind: ExprKind::PostfixUntil {
1297                                expr: Box::new(expr),
1298                                condition: Box::new(cond),
1299                            },
1300                            line,
1301                        }),
1302                        line,
1303                    })
1304                }
1305                "for" | "foreach" => {
1306                    self.advance();
1307                    let list = self.parse_expression()?;
1308                    Ok(Statement {
1309                        label: None,
1310                        kind: StmtKind::Expression(Expr {
1311                            kind: ExprKind::PostfixForeach {
1312                                expr: Box::new(expr),
1313                                list: Box::new(list),
1314                            },
1315                            line,
1316                        }),
1317                        line,
1318                    })
1319                }
1320                _ => Ok(Statement {
1321                    label: None,
1322                    kind: StmtKind::Expression(expr),
1323                    line,
1324                }),
1325            },
1326            _ => Ok(Statement {
1327                label: None,
1328                kind: StmtKind::Expression(expr),
1329                line,
1330            }),
1331        }
1332    }
1333
1334    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1335    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1336        let saved = self.pos;
1337        let line = self.peek_line();
1338        let mut name = match self.peek() {
1339            Token::Ident(n) => n.clone(),
1340            _ => return None,
1341        };
1342        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1343        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1344            return None;
1345        }
1346        self.advance();
1347        while self.eat(&Token::PackageSep) {
1348            match self.advance() {
1349                (Token::Ident(part), _) => {
1350                    name = format!("{}::{}", name, part);
1351                }
1352                _ => {
1353                    self.pos = saved;
1354                    return None;
1355                }
1356            }
1357        }
1358        match self.peek() {
1359            Token::Semicolon | Token::RBrace => Some(Expr {
1360                kind: ExprKind::FuncCall { name, args: vec![] },
1361                line,
1362            }),
1363            _ => {
1364                self.pos = saved;
1365                None
1366            }
1367        }
1368    }
1369
1370    /// Map an operator-keyword token (the lexer converts `eq`, `ne`, …, `and`,
1371    /// `or`, `not`, `x` to dedicated tokens) back to its identifier spelling.
1372    /// Used in hash-key contexts where the bareword form is the user's intent.
1373    pub(crate) fn operator_keyword_to_ident_str(tok: &Token) -> Option<&'static str> {
1374        Some(match tok {
1375            Token::StrEq => "eq",
1376            Token::StrNe => "ne",
1377            Token::StrLt => "lt",
1378            Token::StrGt => "gt",
1379            Token::StrLe => "le",
1380            Token::StrGe => "ge",
1381            Token::StrCmp => "cmp",
1382            Token::LogAndWord => "and",
1383            Token::LogOrWord => "or",
1384            Token::LogNotWord => "not",
1385            Token::X => "x",
1386            _ => return None,
1387        })
1388    }
1389
1390    /// Bare names that resolve to the topic-slot scalar matrix:
1391    /// `_`, `_0`, `_1`, …, `_N`, plus `_<+`, `_N<+` for the 4-deep outer chain.
1392    /// These must NOT be treated as zero-arg sub calls — they're scalar var refs.
1393    pub(crate) fn is_underscore_topic_slot(name: &str) -> bool {
1394        if name == "_" {
1395            return true;
1396        }
1397        if !name.starts_with('_') || name.len() < 2 {
1398            return false;
1399        }
1400        let bytes = name.as_bytes();
1401        let mut i = 1;
1402        // Optional digit run (positional slot index).
1403        while i < bytes.len() && bytes[i].is_ascii_digit() {
1404            i += 1;
1405        }
1406        // Then any number of `<` chevrons (1..4 in practice, lexer caps at 4).
1407        let chevrons_start = i;
1408        while i < bytes.len() && bytes[i] == b'<' {
1409            i += 1;
1410        }
1411        // Must be one of: `_`, `_N`, `_<+`, `_N<+`. No other trailing chars.
1412        i == bytes.len() && (i > 1 || chevrons_start > 1)
1413    }
1414
1415    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1416    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1417        // Topic-slot scalar names (`_`, `_N`, `_<+`, `_N<+`) are scalar
1418        // variables, not zero-arg sub calls. Without this guard, the
1419        // statement-position parser would emit `Op::Call("_0", 0)` and fail
1420        // at runtime with "Undefined subroutine &_0".
1421        if Self::is_underscore_topic_slot(name) {
1422            return false;
1423        }
1424        !matches!(
1425            name,
1426            "__FILE__"
1427                | "__LINE__"
1428                | "abs"
1429                | "async"
1430                | "spawn"
1431                | "atan2"
1432                | "await"
1433                | "barrier"
1434                | "bless"
1435                | "caller"
1436                | "capture"
1437                | "cat"
1438                | "chdir"
1439                | "chmod"
1440                | "chomp"
1441                | "chop"
1442                | "chr"
1443                | "chown"
1444                | "closedir"
1445                | "close"
1446                | "collect"
1447                | "cos"
1448                | "crypt"
1449                | "defined"
1450                | "dec"
1451                | "delete"
1452                | "die"
1453                | "deque"
1454                | "do"
1455                | "each"
1456                | "eof"
1457                | "fore"
1458                | "eval"
1459                | "exec"
1460                | "exists"
1461                | "exit"
1462                | "exp"
1463                | "fan"
1464                | "fan_cap"
1465                | "fc"
1466                | "fetch_url"
1467                | "d"
1468                | "dirs"
1469                | "dr"
1470                | "f"
1471                | "fi"
1472                | "files"
1473                | "filesf"
1474                | "filter"
1475                | "fr"
1476                | "getcwd"
1477                | "glob_par"
1478                | "par_sed"
1479                | "glob"
1480                | "grep"
1481                | "greps"
1482                | "heap"
1483                | "hex"
1484                | "inc"
1485                | "index"
1486                | "int"
1487                | "join"
1488                | "keys"
1489                | "lcfirst"
1490                | "lc"
1491                | "length"
1492                | "link"
1493                | "log"
1494                | "lstat"
1495                | "map"
1496                | "flat_map"
1497                | "maps"
1498                | "flat_maps"
1499                | "flatten"
1500                | "frequencies"
1501                | "freq"
1502                | "interleave"
1503                | "ddump"
1504                | "stringify"
1505                | "str"
1506                | "s"
1507                | "input"
1508                | "lines"
1509                | "words"
1510                | "chars"
1511                | "digits"
1512                | "letters"
1513                | "letters_uc"
1514                | "letters_lc"
1515                | "punctuation"
1516                | "sentences"
1517                | "paragraphs"
1518                | "sections"
1519                | "numbers"
1520                | "graphemes"
1521                | "columns"
1522                | "trim"
1523                | "avg"
1524                | "top"
1525                | "pager"
1526                | "pg"
1527                | "less"
1528                | "count_by"
1529                | "to_file"
1530                | "to_json"
1531                | "to_csv"
1532                | "grep_v"
1533                | "select_keys"
1534                | "pluck"
1535                | "clamp"
1536                | "normalize"
1537                | "stddev"
1538                | "squared"
1539                | "square"
1540                | "cubed"
1541                | "cube"
1542                | "expt"
1543                | "pow"
1544                | "pw"
1545                | "snake_case"
1546                | "camel_case"
1547                | "kebab_case"
1548                | "to_toml"
1549                | "to_yaml"
1550                | "to_xml"
1551                | "to_html"
1552                | "to_markdown"
1553                | "xopen"
1554                | "clip"
1555                | "paste"
1556                | "to_table"
1557                | "sparkline"
1558                | "bar_chart"
1559                | "flame"
1560                | "set"
1561                | "list_count"
1562                | "list_size"
1563                | "count"
1564                | "size"
1565                | "cnt"
1566                | "len"
1567                | "all"
1568                | "any"
1569                | "none"
1570                | "take_while"
1571                | "drop_while"
1572                | "skip_while"
1573                | "skip"
1574                | "first_or"
1575                | "tap"
1576                | "peek"
1577                | "partition"
1578                | "min_by"
1579                | "max_by"
1580                | "zip_with"
1581                | "group_by"
1582                | "chunk_by"
1583                | "with_index"
1584                | "puniq"
1585                | "pfirst"
1586                | "pany"
1587                | "uniq"
1588                | "distinct"
1589                | "shuffle"
1590                | "shuffled"
1591                | "chunked"
1592                | "windowed"
1593                | "match"
1594                | "mkdir"
1595                | "every"
1596                | "gen"
1597                | "oct"
1598                | "open"
1599                | "p"
1600                | "opendir"
1601                | "ord"
1602                | "par_lines"
1603                | "par_walk"
1604                | "pipe"
1605                | "pipes"
1606                | "block_devices"
1607                | "char_devices"
1608                | "exe"
1609                | "executables"
1610                | "rate_limit"
1611                | "retry"
1612                | "pcache"
1613                | "pchannel"
1614                | "pfor"
1615                | "pgrep"
1616                | "pgreps"
1617                | "pipeline"
1618                | "pmap_chunked"
1619                | "pmap_reduce"
1620                | "pmap_on"
1621                | "pflat_map_on"
1622                | "pmap"
1623                | "pmaps"
1624                | "pflat_map"
1625                | "pflat_maps"
1626                | "pop"
1627                | "pos"
1628                | "ppool"
1629                | "preduce_init"
1630                | "preduce"
1631                | "pselect"
1632                | "printf"
1633                | "print"
1634                | "pr"
1635                | "psort"
1636                | "push"
1637                | "pwatch"
1638                | "rand"
1639                | "readdir"
1640                | "readlink"
1641                | "reduce"
1642                | "fold"
1643                | "inject"
1644                | "first"
1645                | "detect"
1646                | "find"
1647                | "find_all"
1648                | "ref"
1649                | "rename"
1650                | "require"
1651                | "rev"
1652                | "reverse"
1653                | "reversed"
1654                | "rewinddir"
1655                | "rindex"
1656                | "rmdir"
1657                | "rm"
1658                | "say"
1659                | "scalar"
1660                | "seekdir"
1661                | "shift"
1662                | "sin"
1663                | "slurp"
1664                | "sockets"
1665                | "sort"
1666                | "splice"
1667                | "splice_last"
1668                | "splice1"
1669                | "spl_last"
1670                | "split"
1671                | "sprintf"
1672                | "sqrt"
1673                | "srand"
1674                | "stat"
1675                | "study"
1676                | "substr"
1677                | "symlink"
1678                | "sym_links"
1679                | "system"
1680                | "telldir"
1681                | "timer"
1682                | "trace"
1683                | "ucfirst"
1684                | "uc"
1685                | "undef"
1686                | "umask"
1687                | "unlink"
1688                | "unshift"
1689                | "utime"
1690                | "values"
1691                | "wantarray"
1692                | "warn"
1693                | "watch"
1694                | "yield"
1695                | "sub"
1696        )
1697    }
1698
1699    fn parse_block(&mut self) -> PerlResult<Block> {
1700        self.expect(&Token::LBrace)?;
1701        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1702        // parses its own input instead of using `$_[0]` placeholder.
1703        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1704        self.pipe_rhs_depth = 0;
1705        let mut stmts = Vec::new();
1706        // `{ |$a, $b| body }` — Ruby-style block params.
1707        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1708        // or `my $p = $_N` for positional N≥3.
1709        if let Some(param_stmts) = self.try_parse_block_params()? {
1710            stmts.extend(param_stmts);
1711        }
1712        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1713            if self.eat(&Token::Semicolon) {
1714                continue;
1715            }
1716            stmts.push(self.parse_statement()?);
1717        }
1718        self.expect(&Token::RBrace)?;
1719        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1720        Self::default_topic_for_sole_bareword(&mut stmts);
1721        Ok(stmts)
1722    }
1723
1724    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1725    /// Returns `None` if the leading `|` is not block-param syntax.
1726    /// When successful, returns `my $var = <implicit>` assignment statements
1727    /// that alias the block's positional arguments.
1728    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1729        if !matches!(self.peek(), Token::BitOr) {
1730            return Ok(None);
1731        }
1732        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1733        let mut i = 1; // skip the opening `|`
1734        loop {
1735            match self.peek_at(i) {
1736                Token::ScalarVar(_) => i += 1,
1737                _ => return Ok(None), // not `|$var...|`
1738            }
1739            match self.peek_at(i) {
1740                Token::BitOr => break,  // closing `|`
1741                Token::Comma => i += 1, // more params
1742                _ => return Ok(None),   // not block params
1743            }
1744        }
1745        // Confirmed — consume and build assignments.
1746        let line = self.peek_line();
1747        self.advance(); // eat opening `|`
1748        let mut names = Vec::new();
1749        loop {
1750            if let Token::ScalarVar(ref name) = self.peek().clone() {
1751                names.push(name.clone());
1752                self.advance();
1753            }
1754            if self.eat(&Token::BitOr) {
1755                break;
1756            }
1757            self.expect(&Token::Comma)?;
1758        }
1759        // Generate `my $name = <source>` for each param.
1760        // 1 param  → source is `$_` (map/grep/each/for topic)
1761        // 2 params → sources are `$a`, `$b` (sort/reduce)
1762        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1763        let sources: Vec<&str> = match names.len() {
1764            1 => vec!["_"],
1765            2 => vec!["a", "b"],
1766            n => {
1767                // Can't return borrowed from a generated vec, handle below.
1768                let _ = n;
1769                vec![] // sentinel — handled in the else branch
1770            }
1771        };
1772        let mut stmts = Vec::with_capacity(names.len());
1773        if !sources.is_empty() {
1774            for (name, src) in names.iter().zip(sources.iter()) {
1775                stmts.push(Statement {
1776                    label: None,
1777                    kind: StmtKind::My(vec![VarDecl {
1778                        sigil: Sigil::Scalar,
1779                        name: name.clone(),
1780                        initializer: Some(Expr {
1781                            kind: ExprKind::ScalarVar(src.to_string()),
1782                            line,
1783                        }),
1784                        frozen: false,
1785                        type_annotation: None,
1786                    }]),
1787                    line,
1788                });
1789            }
1790        } else {
1791            // N≥3: positional `$_`, `$_1`, `$_2`, …
1792            for (idx, name) in names.iter().enumerate() {
1793                let src = if idx == 0 {
1794                    "_".to_string()
1795                } else {
1796                    format!("_{idx}")
1797                };
1798                stmts.push(Statement {
1799                    label: None,
1800                    kind: StmtKind::My(vec![VarDecl {
1801                        sigil: Sigil::Scalar,
1802                        name: name.clone(),
1803                        initializer: Some(Expr {
1804                            kind: ExprKind::ScalarVar(src),
1805                            line,
1806                        }),
1807                        frozen: false,
1808                        type_annotation: None,
1809                    }]),
1810                    line,
1811                });
1812            }
1813        }
1814        Ok(Some(stmts))
1815    }
1816
1817    /// Block shorthand: when the body is literally one bare builtin call
1818    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1819    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1820    ///
1821    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1822    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1823    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1824    /// empty args and return the wrong value. This rewrite levels the
1825    /// playing field at parse time — no per-builtin handling needed.
1826    ///
1827    /// Narrow by design: fires only when the block has *exactly one*
1828    /// expression statement whose sole content is a known-bareword call
1829    /// with zero args. Multi-statement blocks and blocks with any other
1830    /// content are untouched.
1831    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1832        let [only] = stmts else { return };
1833        let StmtKind::Expression(ref mut expr) = only.kind else {
1834            return;
1835        };
1836        let topic_line = expr.line;
1837        let topic_arg = || Expr {
1838            kind: ExprKind::ScalarVar("_".to_string()),
1839            line: topic_line,
1840        };
1841        match expr.kind {
1842            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1843            ExprKind::FuncCall {
1844                ref name,
1845                ref mut args,
1846            } if args.is_empty()
1847                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1848            {
1849                args.push(topic_arg());
1850            }
1851            // Lone bareword (the parser sometimes keeps a bareword as a
1852            // `Bareword` node instead of a zero-arg `FuncCall` —
1853            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1854            ExprKind::Bareword(ref name)
1855                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1856            {
1857                let n = name.clone();
1858                expr.kind = ExprKind::FuncCall {
1859                    name: n,
1860                    args: vec![topic_arg()],
1861                };
1862            }
1863            _ => {}
1864        }
1865    }
1866
1867    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1868    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
1869    /// handles specially by emitting Op::DeferBlock.
1870    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1871        let line = self.peek_line();
1872        self.advance(); // defer
1873        let body = self.parse_block()?;
1874        self.eat(&Token::Semicolon);
1875        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
1876        let coderef = Expr {
1877            kind: ExprKind::CodeRef {
1878                params: vec![],
1879                body,
1880            },
1881            line,
1882        };
1883        Ok(Statement {
1884            label: None,
1885            kind: StmtKind::Expression(Expr {
1886                kind: ExprKind::FuncCall {
1887                    name: "defer__internal".to_string(),
1888                    args: vec![coderef],
1889                },
1890                line,
1891            }),
1892            line,
1893        })
1894    }
1895
1896    /// `try { } catch ($err) { }` with optional `finally { }`
1897    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
1898        let line = self.peek_line();
1899        self.advance(); // try
1900        let try_block = self.parse_block()?;
1901        match self.peek() {
1902            Token::Ident(ref k) if k == "catch" => {
1903                self.advance();
1904            }
1905            _ => {
1906                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
1907            }
1908        }
1909        self.expect(&Token::LParen)?;
1910        let catch_var = self.parse_scalar_var_name()?;
1911        self.expect(&Token::RParen)?;
1912        let catch_block = self.parse_block()?;
1913        let finally_block = match self.peek() {
1914            Token::Ident(ref k) if k == "finally" => {
1915                self.advance();
1916                Some(self.parse_block()?)
1917            }
1918            _ => None,
1919        };
1920        self.eat(&Token::Semicolon);
1921        Ok(Statement {
1922            label: None,
1923            kind: StmtKind::TryCatch {
1924                try_block,
1925                catch_var,
1926                catch_block,
1927                finally_block,
1928            },
1929            line,
1930        })
1931    }
1932
1933    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
1934    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
1935    ///
1936    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
1937    ///
1938    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
1939    /// is not parsed from tokens — using `parse_unary()` there lets the first
1940    /// bareword greedily consume the next token as its arg, which misparses
1941    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
1942    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
1943    /// token through the stage loop, and wrap the resulting chain in a
1944    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
1945    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
1946    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> PerlResult<Expr> {
1947        // Set thread-last mode for pipe_forward_apply calls within this macro
1948        let saved_thread_last = self.thread_last_mode;
1949        self.thread_last_mode = thread_last;
1950
1951        let pipe_rhs_wrap = self.in_pipe_rhs();
1952        let mut result = if pipe_rhs_wrap {
1953            Expr {
1954                kind: ExprKind::ArrayElement {
1955                    array: "_".to_string(),
1956                    index: Box::new(Expr {
1957                        kind: ExprKind::Integer(0),
1958                        line: _line,
1959                    }),
1960                },
1961                line: _line,
1962            }
1963        } else {
1964            // Suppress paren-less function calls so `t Color::Red p` parses
1965            // the enum variant without consuming `p` as an argument.
1966            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
1967            let expr = self.parse_thread_input();
1968            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
1969            expr?
1970        };
1971
1972        // Track line where the last stage ended (initially the input expression's line).
1973        let mut last_stage_end_line = self.prev_line();
1974
1975        // Parse stages until we hit a statement terminator
1976        loop {
1977            // Newline termination: if the next token is on a different line than where
1978            // the previous stage ended, the thread macro terminates. This allows
1979            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
1980            // without requiring a semicolon.
1981            if self.peek_line() > last_stage_end_line {
1982                break;
1983            }
1984
1985            // Check for terminators - |> ends thread and allows piping the result.
1986            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
1987            // cannot be stages, so they implicitly terminate the thread macro.
1988            match self.peek() {
1989                Token::Semicolon
1990                | Token::RBrace
1991                | Token::RParen
1992                | Token::RBracket
1993                | Token::PipeForward
1994                | Token::Eof
1995                | Token::ScalarVar(_)
1996                | Token::ArrayVar(_)
1997                | Token::HashVar(_)
1998                | Token::Comma => break,
1999                Token::Ident(ref kw)
2000                    if matches!(
2001                        kw.as_str(),
2002                        "my" | "our"
2003                            | "local"
2004                            | "state"
2005                            | "if"
2006                            | "unless"
2007                            | "while"
2008                            | "until"
2009                            | "for"
2010                            | "foreach"
2011                            | "return"
2012                            | "last"
2013                            | "next"
2014                            | "redo"
2015                    ) =>
2016                {
2017                    break
2018                }
2019                _ => {}
2020            }
2021
2022            let stage_line = self.peek_line();
2023
2024            // Parse a stage and apply it to result via pipe
2025            match self.peek().clone() {
2026                // `>{ block }` — standalone anonymous block (sugar for fn { })
2027                Token::ArrowBrace => {
2028                    self.advance(); // consume `>{`
2029                    let mut stmts = Vec::new();
2030                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2031                        if self.eat(&Token::Semicolon) {
2032                            continue;
2033                        }
2034                        stmts.push(self.parse_statement()?);
2035                    }
2036                    self.expect(&Token::RBrace)?;
2037                    let code_ref = Expr {
2038                        kind: ExprKind::CodeRef {
2039                            params: vec![],
2040                            body: stmts,
2041                        },
2042                        line: stage_line,
2043                    };
2044                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2045                }
2046                // `sub { block }` — blocked in no-interop mode
2047                Token::Ident(ref name) if name == "sub" => {
2048                    if crate::no_interop_mode() {
2049                        return Err(self.syntax_err(
2050                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
2051                            stage_line,
2052                        ));
2053                    }
2054                    self.advance(); // consume `sub`
2055                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2056                    let body = self.parse_block()?;
2057                    let code_ref = Expr {
2058                        kind: ExprKind::CodeRef { params, body },
2059                        line: stage_line,
2060                    };
2061                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2062                }
2063                // `fn { block }` — stryke anonymous function
2064                Token::Ident(ref name) if name == "fn" => {
2065                    self.advance(); // consume `fn`
2066                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2067                    self.parse_sub_attributes()?;
2068                    let body = self.parse_fn_eq_body_or_block(false)?;
2069                    let code_ref = Expr {
2070                        kind: ExprKind::CodeRef { params, body },
2071                        line: stage_line,
2072                    };
2073                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2074                }
2075                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
2076                Token::Ident(ref name) => {
2077                    let mut func_name = name.clone();
2078                    self.advance();
2079
2080                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
2081                    while matches!(self.peek(), Token::PackageSep) {
2082                        self.advance(); // consume `::`
2083                        if let Token::Ident(ref part) = self.peek().clone() {
2084                            func_name.push_str("::");
2085                            func_name.push_str(part);
2086                            self.advance();
2087                        } else {
2088                            return Err(self.syntax_err(
2089                                format!(
2090                                    "Expected identifier after `::` in thread stage, got {:?}",
2091                                    self.peek()
2092                                ),
2093                                stage_line,
2094                            ));
2095                        }
2096                    }
2097
2098                    // Handle s/// and tr/// encoded tokens
2099                    if func_name.starts_with('\x00') {
2100                        let parts: Vec<&str> = func_name.split('\x00').collect();
2101                        if parts.len() >= 4 && parts[1] == "s" {
2102                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2103                            let stage = Expr {
2104                                kind: ExprKind::Substitution {
2105                                    expr: Box::new(result.clone()),
2106                                    pattern: parts[2].to_string(),
2107                                    replacement: parts[3].to_string(),
2108                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2109                                    delim,
2110                                },
2111                                line: stage_line,
2112                            };
2113                            result = stage;
2114                            last_stage_end_line = self.prev_line();
2115                            continue;
2116                        }
2117                        if parts.len() >= 4 && parts[1] == "tr" {
2118                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2119                            let stage = Expr {
2120                                kind: ExprKind::Transliterate {
2121                                    expr: Box::new(result.clone()),
2122                                    from: parts[2].to_string(),
2123                                    to: parts[3].to_string(),
2124                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2125                                    delim,
2126                                },
2127                                line: stage_line,
2128                            };
2129                            result = stage;
2130                            last_stage_end_line = self.prev_line();
2131                            continue;
2132                        }
2133                        return Err(
2134                            self.syntax_err("Unexpected encoded token in thread", stage_line)
2135                        );
2136                    }
2137
2138                    // `map +{ ... }` — hashref expression form (not a code block).
2139                    // The `+` disambiguates: `+{` is always a hashref constructor.
2140                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
2141                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
2142                    if matches!(self.peek(), Token::Plus)
2143                        && matches!(self.peek_at(1), Token::LBrace)
2144                    {
2145                        self.advance(); // consume `+`
2146                        self.expect(&Token::LBrace)?;
2147                        // try_parse_hash_ref consumes the closing `}`
2148                        let pairs = self.try_parse_hash_ref()?;
2149                        let hashref_expr = Expr {
2150                            kind: ExprKind::HashRef(pairs),
2151                            line: stage_line,
2152                        };
2153                        let flatten_array_refs =
2154                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
2155                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
2156                        // Placeholder list — pipe_forward_apply replaces it with `result`.
2157                        let placeholder = Expr {
2158                            kind: ExprKind::Undef,
2159                            line: stage_line,
2160                        };
2161                        let map_node = Expr {
2162                            kind: ExprKind::MapExprComma {
2163                                expr: Box::new(hashref_expr),
2164                                list: Box::new(placeholder),
2165                                flatten_array_refs,
2166                                stream,
2167                            },
2168                            line: stage_line,
2169                        };
2170                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
2171                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
2172                    } else if func_name == "pmap_chunked" {
2173                        let chunk_size = self.parse_assign_expr()?;
2174                        let block = self.parse_block_or_bareword_block()?;
2175                        let placeholder = self.pipe_placeholder_list(stage_line);
2176                        let stage = Expr {
2177                            kind: ExprKind::PMapChunkedExpr {
2178                                chunk_size: Box::new(chunk_size),
2179                                block,
2180                                list: Box::new(placeholder),
2181                                progress: None,
2182                            },
2183                            line: stage_line,
2184                        };
2185                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2186                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2187                    } else if func_name == "preduce_init" {
2188                        let init = self.parse_assign_expr()?;
2189                        let block = self.parse_block_or_bareword_block()?;
2190                        let placeholder = self.pipe_placeholder_list(stage_line);
2191                        let stage = Expr {
2192                            kind: ExprKind::PReduceInitExpr {
2193                                init: Box::new(init),
2194                                block,
2195                                list: Box::new(placeholder),
2196                                progress: None,
2197                            },
2198                            line: stage_line,
2199                        };
2200                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2201                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2202                    } else if func_name == "pmap_reduce" {
2203                        let map_block = self.parse_block_or_bareword_block()?;
2204                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2205                            self.parse_block()?
2206                        } else {
2207                            self.expect(&Token::Comma)?;
2208                            self.parse_block_or_bareword_cmp_block()?
2209                        };
2210                        let placeholder = self.pipe_placeholder_list(stage_line);
2211                        let stage = Expr {
2212                            kind: ExprKind::PMapReduceExpr {
2213                                map_block,
2214                                reduce_block,
2215                                list: Box::new(placeholder),
2216                                progress: None,
2217                            },
2218                            line: stage_line,
2219                        };
2220                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2221                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2222                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2223                    // expression is parsed before the block, the threaded list slots in
2224                    // as the placeholder.
2225                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2226                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2227                        // brace opens the block, not a hash subscript.
2228                        self.suppress_scalar_hash_brace =
2229                            self.suppress_scalar_hash_brace.saturating_add(1);
2230                        let cluster = self.parse_assign_expr();
2231                        self.suppress_scalar_hash_brace =
2232                            self.suppress_scalar_hash_brace.saturating_sub(1);
2233                        let cluster = cluster?;
2234                        // Optional comma between cluster and block (matches the
2235                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2236                        self.eat(&Token::Comma);
2237                        let block = self.parse_block_or_bareword_block()?;
2238                        let placeholder = self.pipe_placeholder_list(stage_line);
2239                        let stage = Expr {
2240                            kind: ExprKind::PMapExpr {
2241                                block,
2242                                list: Box::new(placeholder),
2243                                progress: None,
2244                                flat_outputs: func_name == "pflat_map_on",
2245                                on_cluster: Some(Box::new(cluster)),
2246                                stream: false,
2247                            },
2248                            line: stage_line,
2249                        };
2250                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2251                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2252                    } else if matches!(self.peek(), Token::LBrace) {
2253                        // Parse as a block-taking builtin
2254                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2255                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2256                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2257                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2258                    } else if matches!(self.peek(), Token::LParen) {
2259                        // Special handling for join(sep) and split(pattern) in thread context.
2260                        // These take the threaded list/string as their data argument, not as $_.
2261                        if func_name == "join" {
2262                            self.advance(); // consume `(`
2263                            let separator = self.parse_assign_expr()?;
2264                            self.expect(&Token::RParen)?;
2265                            let placeholder = self.pipe_placeholder_list(stage_line);
2266                            let stage = Expr {
2267                                kind: ExprKind::JoinExpr {
2268                                    separator: Box::new(separator),
2269                                    list: Box::new(placeholder),
2270                                },
2271                                line: stage_line,
2272                            };
2273                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2274                        } else if func_name == "split" {
2275                            self.advance(); // consume `(`
2276                            let pattern = self.parse_assign_expr()?;
2277                            let limit = if self.eat(&Token::Comma) {
2278                                Some(Box::new(self.parse_assign_expr()?))
2279                            } else {
2280                                None
2281                            };
2282                            self.expect(&Token::RParen)?;
2283                            let placeholder = Expr {
2284                                kind: ExprKind::ScalarVar("_".to_string()),
2285                                line: stage_line,
2286                            };
2287                            let stage = Expr {
2288                                kind: ExprKind::SplitExpr {
2289                                    pattern: Box::new(pattern),
2290                                    string: Box::new(placeholder),
2291                                    limit,
2292                                },
2293                                line: stage_line,
2294                            };
2295                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2296                        } else {
2297                            // `name($_-bearing-args)` — parse explicit args, require at
2298                            // least one `$_` placeholder, then wrap as a `>{...}` block
2299                            // so the threaded value binds to `$_` at any position.
2300                            // Examples:
2301                            //   t 10 add2($_, 5) p      → add2(10, 5)
2302                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2303                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2304                            // To pass the threaded value as a sole arg, use bare form:
2305                            //   t 10 add2 p   (not `add2()`)
2306                            self.advance(); // consume `(`
2307                            let mut call_args = Vec::new();
2308                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2309                                call_args.push(self.parse_assign_expr()?);
2310                                if !self.eat(&Token::Comma) {
2311                                    break;
2312                                }
2313                            }
2314                            self.expect(&Token::RParen)?;
2315                            // If no `$_` placeholder, auto-inject threaded value.
2316                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2317                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2318                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2319                                let topic = Expr {
2320                                    kind: ExprKind::ScalarVar("_".to_string()),
2321                                    line: stage_line,
2322                                };
2323                                if self.thread_last_mode {
2324                                    call_args.push(topic);
2325                                } else {
2326                                    call_args.insert(0, topic);
2327                                }
2328                            }
2329                            let call_expr = Expr {
2330                                kind: ExprKind::FuncCall {
2331                                    name: func_name.clone(),
2332                                    args: call_args,
2333                                },
2334                                line: stage_line,
2335                            };
2336                            let code_ref = Expr {
2337                                kind: ExprKind::CodeRef {
2338                                    params: vec![],
2339                                    body: vec![Statement {
2340                                        label: None,
2341                                        kind: StmtKind::Expression(call_expr),
2342                                        line: stage_line,
2343                                    }],
2344                                },
2345                                line: stage_line,
2346                            };
2347                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2348                        }
2349                    } else {
2350                        // Bare function name — handle unary builtins specially
2351                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2352                    }
2353                }
2354                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2355                Token::Regex(ref pattern, ref flags, delim) => {
2356                    let pattern = pattern.clone();
2357                    let flags = flags.clone();
2358                    self.advance();
2359                    result =
2360                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2361                }
2362                // Handle `/` that was lexed as Slash (division) because it followed a term.
2363                // In thread stage context, `/pattern/` should be a regex filter.
2364                Token::Slash => {
2365                    self.advance(); // consume opening /
2366
2367                    // Special case: if next token is Ident("m") or similar followed by Regex,
2368                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2369                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2370                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2371                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2372                            && matches!(self.peek_at(1), Token::Regex(..))
2373                        {
2374                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2375                            self.advance(); // consume the ident
2376                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2377                                            // extract what would have been the closing `/` situation.
2378                                            // Actually, the lexer consumed everything. Let's just use the ident
2379                                            // as the pattern and expect a closing slash.
2380                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2381                                self.peek().clone()
2382                            {
2383                                // The misparsed regex ate our closing `/`.
2384                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2385                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2386                                // interprets as m// regex start, reads until next `/` (none) -> error.
2387                                // So we shouldn't reach here if there was an error.
2388                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2389                                // This is getting complicated. Let me try a different approach.
2390                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2391                                // Skip for now and fall through to manual parsing.
2392                                let _ = (misparsed_pattern, misparsed_flags);
2393                            }
2394                        }
2395                    }
2396
2397                    // Manually parse the regex pattern from tokens until we hit another Slash
2398                    let mut pattern = String::new();
2399                    loop {
2400                        match self.peek().clone() {
2401                            Token::Slash => {
2402                                self.advance(); // consume closing /
2403                                break;
2404                            }
2405                            Token::Eof | Token::Semicolon | Token::Newline => {
2406                                return Err(self
2407                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2408                            }
2409                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2410                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2411                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2412                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2413                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2414                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2415                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2416                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2417                                // This is a lexer bug workaround.
2418                                if pattern.is_empty()
2419                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2420                                {
2421                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2422                                    // and lexer misparsed. The Regex token is garbage.
2423                                    // Just use the ident as pattern and ignore this Regex.
2424                                    // But we already advanced past the ident...
2425                                    // This is messy. Let me try a cleaner approach.
2426                                    let _ = (inner_pattern, inner_flags, delim);
2427                                }
2428                                // For now, error out - this case is too complex
2429                                return Err(self.syntax_err(
2430                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2431                                    stage_line,
2432                                ));
2433                            }
2434                            Token::Ident(ref s) => {
2435                                pattern.push_str(s);
2436                                self.advance();
2437                            }
2438                            Token::Integer(n) => {
2439                                pattern.push_str(&n.to_string());
2440                                self.advance();
2441                            }
2442                            Token::ScalarVar(ref v) => {
2443                                pattern.push('$');
2444                                pattern.push_str(v);
2445                                self.advance();
2446                            }
2447                            Token::Dot => {
2448                                pattern.push('.');
2449                                self.advance();
2450                            }
2451                            Token::Star => {
2452                                pattern.push('*');
2453                                self.advance();
2454                            }
2455                            Token::Plus => {
2456                                pattern.push('+');
2457                                self.advance();
2458                            }
2459                            Token::Question => {
2460                                pattern.push('?');
2461                                self.advance();
2462                            }
2463                            Token::LParen => {
2464                                pattern.push('(');
2465                                self.advance();
2466                            }
2467                            Token::RParen => {
2468                                pattern.push(')');
2469                                self.advance();
2470                            }
2471                            Token::LBracket => {
2472                                pattern.push('[');
2473                                self.advance();
2474                            }
2475                            Token::RBracket => {
2476                                pattern.push(']');
2477                                self.advance();
2478                            }
2479                            Token::Backslash => {
2480                                pattern.push('\\');
2481                                self.advance();
2482                            }
2483                            Token::BitOr => {
2484                                pattern.push('|');
2485                                self.advance();
2486                            }
2487                            Token::Power => {
2488                                pattern.push_str("**");
2489                                self.advance();
2490                            }
2491                            Token::BitXor => {
2492                                pattern.push('^');
2493                                self.advance();
2494                            }
2495                            Token::Minus => {
2496                                pattern.push('-');
2497                                self.advance();
2498                            }
2499                            _ => {
2500                                return Err(self.syntax_err(
2501                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2502                                    stage_line,
2503                                ));
2504                            }
2505                        }
2506                    }
2507                    // Parse optional flags (sequence of letters after closing /)
2508                    // Be careful: single letters like 'e' could be regex flags OR thread
2509                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2510                    let mut flags = String::new();
2511                    if let Token::Ident(ref s) = self.peek().clone() {
2512                        let is_flag_only =
2513                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2514                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2515                        if is_flag_only && !followed_by_brace {
2516                            flags.push_str(s);
2517                            self.advance();
2518                        }
2519                    }
2520                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2521                }
2522                tok => {
2523                    return Err(self.syntax_err(
2524                        format!(
2525                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2526                            tok
2527                        ),
2528                        stage_line,
2529                    ));
2530                }
2531            };
2532            last_stage_end_line = self.prev_line();
2533        }
2534
2535        // Restore thread-last mode
2536        self.thread_last_mode = saved_thread_last;
2537
2538        if pipe_rhs_wrap {
2539            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2540            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2541            let body_line = result.line;
2542            return Ok(Expr {
2543                kind: ExprKind::CodeRef {
2544                    params: vec![],
2545                    body: vec![Statement {
2546                        label: None,
2547                        kind: StmtKind::Expression(result),
2548                        line: body_line,
2549                    }],
2550                },
2551                line: _line,
2552            });
2553        }
2554        Ok(result)
2555    }
2556
2557    /// Build a grep filter stage from a regex pattern for the thread macro.
2558    fn thread_regex_grep_stage(
2559        &self,
2560        list: Expr,
2561        pattern: String,
2562        flags: String,
2563        delim: char,
2564        line: usize,
2565    ) -> Expr {
2566        let topic = Expr {
2567            kind: ExprKind::ScalarVar("_".to_string()),
2568            line,
2569        };
2570        let match_expr = Expr {
2571            kind: ExprKind::Match {
2572                expr: Box::new(topic),
2573                pattern,
2574                flags,
2575                scalar_g: false,
2576                delim,
2577            },
2578            line,
2579        };
2580        let block = vec![Statement {
2581            label: None,
2582            kind: StmtKind::Expression(match_expr),
2583            line,
2584        }];
2585        Expr {
2586            kind: ExprKind::GrepExpr {
2587                block,
2588                list: Box::new(list),
2589                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2590            },
2591            line,
2592        }
2593    }
2594
2595    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2596    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2597    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2598    /// must appear in the args, otherwise the threaded value is silently dropped.
2599    ///
2600    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2601    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2602    /// per-variant walker that would need to be updated whenever new `ExprKind`
2603    /// variants are added (and would silently miss any it forgot to handle).
2604    /// Parse-time perf is non-critical and the AST is small at this scope.
2605    fn expr_contains_topic_var(e: &Expr) -> bool {
2606        format!("{:?}", e).contains("ScalarVar(\"_\")")
2607    }
2608
2609    /// Apply a bare function name in thread context, handling unary builtins specially.
2610    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2611        let kind = match name {
2612            // String functions
2613            "uc" => ExprKind::Uc(Box::new(arg)),
2614            "lc" => ExprKind::Lc(Box::new(arg)),
2615            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2616            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2617            "fc" => ExprKind::Fc(Box::new(arg)),
2618            "chomp" => ExprKind::Chomp(Box::new(arg)),
2619            "chop" => ExprKind::Chop(Box::new(arg)),
2620            "length" => ExprKind::Length(Box::new(arg)),
2621            "len" | "cnt" => ExprKind::FuncCall {
2622                name: "count".to_string(),
2623                args: vec![arg],
2624            },
2625            "quotemeta" | "qm" => ExprKind::FuncCall {
2626                name: "quotemeta".to_string(),
2627                args: vec![arg],
2628            },
2629            // Numeric functions
2630            "abs" => ExprKind::Abs(Box::new(arg)),
2631            "int" => ExprKind::Int(Box::new(arg)),
2632            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2633            "sin" => ExprKind::Sin(Box::new(arg)),
2634            "cos" => ExprKind::Cos(Box::new(arg)),
2635            "exp" => ExprKind::Exp(Box::new(arg)),
2636            "log" => ExprKind::Log(Box::new(arg)),
2637            "hex" => ExprKind::Hex(Box::new(arg)),
2638            "oct" => ExprKind::Oct(Box::new(arg)),
2639            "chr" => ExprKind::Chr(Box::new(arg)),
2640            "ord" => ExprKind::Ord(Box::new(arg)),
2641            // Type/ref functions
2642            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2643            "ref" => ExprKind::Ref(Box::new(arg)),
2644            "scalar" => {
2645                if crate::no_interop_mode() {
2646                    return Err(self.syntax_err(
2647                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
2648                        line,
2649                    ));
2650                }
2651                ExprKind::ScalarContext(Box::new(arg))
2652            }
2653            // Array/hash functions
2654            "keys" => ExprKind::Keys(Box::new(arg)),
2655            "values" => ExprKind::Values(Box::new(arg)),
2656            "each" => ExprKind::Each(Box::new(arg)),
2657            "pop" => ExprKind::Pop(Box::new(arg)),
2658            "shift" => ExprKind::Shift(Box::new(arg)),
2659            "reverse" => {
2660                if crate::no_interop_mode() {
2661                    return Err(self.syntax_err(
2662                        "stryke uses `rev` instead of `reverse` (--no-interop)",
2663                        line,
2664                    ));
2665                }
2666                ExprKind::ReverseExpr(Box::new(arg))
2667            }
2668            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2669            "sort" | "so" => ExprKind::SortExpr {
2670                cmp: None,
2671                list: Box::new(arg),
2672            },
2673            "psort" => ExprKind::PSortExpr {
2674                cmp: None,
2675                list: Box::new(arg),
2676                progress: None,
2677            },
2678            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2679                name: "uniq".to_string(),
2680                args: vec![arg],
2681            },
2682            "trim" | "tm" => ExprKind::FuncCall {
2683                name: "trim".to_string(),
2684                args: vec![arg],
2685            },
2686            "flatten" | "fl" => ExprKind::FuncCall {
2687                name: "flatten".to_string(),
2688                args: vec![arg],
2689            },
2690            "compact" | "cpt" => ExprKind::FuncCall {
2691                name: "compact".to_string(),
2692                args: vec![arg],
2693            },
2694            "shuffle" | "shuf" => ExprKind::FuncCall {
2695                name: "shuffle".to_string(),
2696                args: vec![arg],
2697            },
2698            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2699                name: "frequencies".to_string(),
2700                args: vec![arg],
2701            },
2702            "dedup" | "dup" => ExprKind::FuncCall {
2703                name: "dedup".to_string(),
2704                args: vec![arg],
2705            },
2706            "enumerate" | "en" => ExprKind::FuncCall {
2707                name: "enumerate".to_string(),
2708                args: vec![arg],
2709            },
2710            "lines" | "ln" => ExprKind::FuncCall {
2711                name: "lines".to_string(),
2712                args: vec![arg],
2713            },
2714            "words" | "wd" => ExprKind::FuncCall {
2715                name: "words".to_string(),
2716                args: vec![arg],
2717            },
2718            "chars" | "ch" => ExprKind::FuncCall {
2719                name: "chars".to_string(),
2720                args: vec![arg],
2721            },
2722            "digits" | "dg" => ExprKind::FuncCall {
2723                name: "digits".to_string(),
2724                args: vec![arg],
2725            },
2726            "letters" | "lts" => ExprKind::FuncCall {
2727                name: "letters".to_string(),
2728                args: vec![arg],
2729            },
2730            "letters_uc" => ExprKind::FuncCall {
2731                name: "letters_uc".to_string(),
2732                args: vec![arg],
2733            },
2734            "letters_lc" => ExprKind::FuncCall {
2735                name: "letters_lc".to_string(),
2736                args: vec![arg],
2737            },
2738            "punctuation" | "punct" => ExprKind::FuncCall {
2739                name: "punctuation".to_string(),
2740                args: vec![arg],
2741            },
2742            "sentences" | "sents" => ExprKind::FuncCall {
2743                name: "sentences".to_string(),
2744                args: vec![arg],
2745            },
2746            "paragraphs" | "paras" => ExprKind::FuncCall {
2747                name: "paragraphs".to_string(),
2748                args: vec![arg],
2749            },
2750            "sections" | "sects" => ExprKind::FuncCall {
2751                name: "sections".to_string(),
2752                args: vec![arg],
2753            },
2754            "numbers" | "nums" => ExprKind::FuncCall {
2755                name: "numbers".to_string(),
2756                args: vec![arg],
2757            },
2758            "graphemes" | "grs" => ExprKind::FuncCall {
2759                name: "graphemes".to_string(),
2760                args: vec![arg],
2761            },
2762            "columns" | "cols" => ExprKind::FuncCall {
2763                name: "columns".to_string(),
2764                args: vec![arg],
2765            },
2766            // File functions
2767            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2768            "chdir" => ExprKind::Chdir(Box::new(arg)),
2769            "stat" => ExprKind::Stat(Box::new(arg)),
2770            "lstat" => ExprKind::Lstat(Box::new(arg)),
2771            "readlink" => ExprKind::Readlink(Box::new(arg)),
2772            "readdir" => ExprKind::Readdir(Box::new(arg)),
2773            "close" => ExprKind::Close(Box::new(arg)),
2774            "basename" | "bn" => ExprKind::FuncCall {
2775                name: "basename".to_string(),
2776                args: vec![arg],
2777            },
2778            "dirname" | "dn" => ExprKind::FuncCall {
2779                name: "dirname".to_string(),
2780                args: vec![arg],
2781            },
2782            "realpath" | "rp" => ExprKind::FuncCall {
2783                name: "realpath".to_string(),
2784                args: vec![arg],
2785            },
2786            "which" | "wh" => ExprKind::FuncCall {
2787                name: "which".to_string(),
2788                args: vec![arg],
2789            },
2790            // Other
2791            "eval" => ExprKind::Eval(Box::new(arg)),
2792            "require" => ExprKind::Require(Box::new(arg)),
2793            "study" => ExprKind::Study(Box::new(arg)),
2794            // Case conversion
2795            "snake_case" | "sc" => ExprKind::FuncCall {
2796                name: "snake_case".to_string(),
2797                args: vec![arg],
2798            },
2799            "camel_case" | "cc" => ExprKind::FuncCall {
2800                name: "camel_case".to_string(),
2801                args: vec![arg],
2802            },
2803            "kebab_case" | "kc" => ExprKind::FuncCall {
2804                name: "kebab_case".to_string(),
2805                args: vec![arg],
2806            },
2807            // Serialization
2808            "to_json" | "tj" => ExprKind::FuncCall {
2809                name: "to_json".to_string(),
2810                args: vec![arg],
2811            },
2812            "to_yaml" | "ty" => ExprKind::FuncCall {
2813                name: "to_yaml".to_string(),
2814                args: vec![arg],
2815            },
2816            "to_toml" | "tt" => ExprKind::FuncCall {
2817                name: "to_toml".to_string(),
2818                args: vec![arg],
2819            },
2820            "to_csv" | "tc" => ExprKind::FuncCall {
2821                name: "to_csv".to_string(),
2822                args: vec![arg],
2823            },
2824            "to_xml" | "tx" => ExprKind::FuncCall {
2825                name: "to_xml".to_string(),
2826                args: vec![arg],
2827            },
2828            "to_html" | "th" => ExprKind::FuncCall {
2829                name: "to_html".to_string(),
2830                args: vec![arg],
2831            },
2832            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2833                name: "to_markdown".to_string(),
2834                args: vec![arg],
2835            },
2836            "xopen" | "xo" => ExprKind::FuncCall {
2837                name: "xopen".to_string(),
2838                args: vec![arg],
2839            },
2840            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2841                name: "clip".to_string(),
2842                args: vec![arg],
2843            },
2844            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2845                name: "to_table".to_string(),
2846                args: vec![arg],
2847            },
2848            "sparkline" | "spark" => ExprKind::FuncCall {
2849                name: "sparkline".to_string(),
2850                args: vec![arg],
2851            },
2852            "bar_chart" | "bars" => ExprKind::FuncCall {
2853                name: "bar_chart".to_string(),
2854                args: vec![arg],
2855            },
2856            "flame" | "flamechart" => ExprKind::FuncCall {
2857                name: "flame".to_string(),
2858                args: vec![arg],
2859            },
2860            "ddump" | "dd" => ExprKind::FuncCall {
2861                name: "ddump".to_string(),
2862                args: vec![arg],
2863            },
2864            "say" => {
2865                if crate::no_interop_mode() {
2866                    return Err(
2867                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
2868                    );
2869                }
2870                ExprKind::Say {
2871                    handle: None,
2872                    args: vec![arg],
2873                }
2874            }
2875            "p" => ExprKind::Say {
2876                handle: None,
2877                args: vec![arg],
2878            },
2879            "print" => ExprKind::Print {
2880                handle: None,
2881                args: vec![arg],
2882            },
2883            "warn" => ExprKind::Warn(vec![arg]),
2884            "die" => ExprKind::Die(vec![arg]),
2885            "stringify" | "str" => ExprKind::FuncCall {
2886                name: "stringify".to_string(),
2887                args: vec![arg],
2888            },
2889            "json_decode" | "jd" => ExprKind::FuncCall {
2890                name: "json_decode".to_string(),
2891                args: vec![arg],
2892            },
2893            "yaml_decode" | "yd" => ExprKind::FuncCall {
2894                name: "yaml_decode".to_string(),
2895                args: vec![arg],
2896            },
2897            "toml_decode" | "td" => ExprKind::FuncCall {
2898                name: "toml_decode".to_string(),
2899                args: vec![arg],
2900            },
2901            "xml_decode" | "xd" => ExprKind::FuncCall {
2902                name: "xml_decode".to_string(),
2903                args: vec![arg],
2904            },
2905            "json_encode" | "je" => ExprKind::FuncCall {
2906                name: "json_encode".to_string(),
2907                args: vec![arg],
2908            },
2909            "yaml_encode" | "ye" => ExprKind::FuncCall {
2910                name: "yaml_encode".to_string(),
2911                args: vec![arg],
2912            },
2913            "toml_encode" | "te" => ExprKind::FuncCall {
2914                name: "toml_encode".to_string(),
2915                args: vec![arg],
2916            },
2917            "xml_encode" | "xe" => ExprKind::FuncCall {
2918                name: "xml_encode".to_string(),
2919                args: vec![arg],
2920            },
2921            // Encoding
2922            "base64_encode" | "b64e" => ExprKind::FuncCall {
2923                name: "base64_encode".to_string(),
2924                args: vec![arg],
2925            },
2926            "base64_decode" | "b64d" => ExprKind::FuncCall {
2927                name: "base64_decode".to_string(),
2928                args: vec![arg],
2929            },
2930            "hex_encode" | "hxe" => ExprKind::FuncCall {
2931                name: "hex_encode".to_string(),
2932                args: vec![arg],
2933            },
2934            "hex_decode" | "hxd" => ExprKind::FuncCall {
2935                name: "hex_decode".to_string(),
2936                args: vec![arg],
2937            },
2938            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
2939                name: "url_encode".to_string(),
2940                args: vec![arg],
2941            },
2942            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
2943                name: "url_decode".to_string(),
2944                args: vec![arg],
2945            },
2946            "gzip" | "gz" => ExprKind::FuncCall {
2947                name: "gzip".to_string(),
2948                args: vec![arg],
2949            },
2950            "gunzip" | "ugz" => ExprKind::FuncCall {
2951                name: "gunzip".to_string(),
2952                args: vec![arg],
2953            },
2954            "zstd" | "zst" => ExprKind::FuncCall {
2955                name: "zstd".to_string(),
2956                args: vec![arg],
2957            },
2958            "zstd_decode" | "uzst" => ExprKind::FuncCall {
2959                name: "zstd_decode".to_string(),
2960                args: vec![arg],
2961            },
2962            // Crypto
2963            "sha256" | "s256" => ExprKind::FuncCall {
2964                name: "sha256".to_string(),
2965                args: vec![arg],
2966            },
2967            "sha1" | "s1" => ExprKind::FuncCall {
2968                name: "sha1".to_string(),
2969                args: vec![arg],
2970            },
2971            "md5" | "m5" => ExprKind::FuncCall {
2972                name: "md5".to_string(),
2973                args: vec![arg],
2974            },
2975            "uuid" | "uid" => ExprKind::FuncCall {
2976                name: "uuid".to_string(),
2977                args: vec![arg],
2978            },
2979            // Datetime
2980            "datetime_utc" | "utc" => ExprKind::FuncCall {
2981                name: "datetime_utc".to_string(),
2982                args: vec![arg],
2983            },
2984            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
2985            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
2986            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
2987                block: vec![Statement {
2988                    label: None,
2989                    kind: StmtKind::Expression(Expr {
2990                        kind: ExprKind::Say {
2991                            handle: None,
2992                            args: vec![Expr {
2993                                kind: ExprKind::ScalarVar("_".into()),
2994                                line,
2995                            }],
2996                        },
2997                        line,
2998                    }),
2999                    line,
3000                }],
3001                list: Box::new(arg),
3002            },
3003            // Default: generic function call
3004            _ => ExprKind::FuncCall {
3005                name: name.to_string(),
3006                args: vec![arg],
3007            },
3008        };
3009        Ok(Expr { kind, line })
3010    }
3011
3012    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
3013    /// In thread context, we only parse the block - the list comes from the piped result.
3014    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
3015        let block = self.parse_block()?;
3016        // Use a placeholder for the list - pipe_forward_apply will replace it
3017        let placeholder = self.pipe_placeholder_list(line);
3018
3019        match name {
3020            "map" | "flat_map" | "maps" | "flat_maps" => {
3021                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
3022                let stream = matches!(name, "maps" | "flat_maps");
3023                Ok(Expr {
3024                    kind: ExprKind::MapExpr {
3025                        block,
3026                        list: Box::new(placeholder),
3027                        flatten_array_refs,
3028                        stream,
3029                    },
3030                    line,
3031                })
3032            }
3033            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
3034                let keyword = match name {
3035                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
3036                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
3037                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
3038                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
3039                    _ => unreachable!(),
3040                };
3041                Ok(Expr {
3042                    kind: ExprKind::GrepExpr {
3043                        block,
3044                        list: Box::new(placeholder),
3045                        keyword,
3046                    },
3047                    line,
3048                })
3049            }
3050            "sort" | "so" => Ok(Expr {
3051                kind: ExprKind::SortExpr {
3052                    cmp: Some(SortComparator::Block(block)),
3053                    list: Box::new(placeholder),
3054                },
3055                line,
3056            }),
3057            "reduce" | "rd" => Ok(Expr {
3058                kind: ExprKind::ReduceExpr {
3059                    block,
3060                    list: Box::new(placeholder),
3061                },
3062                line,
3063            }),
3064            "fore" | "e" | "ep" => Ok(Expr {
3065                kind: ExprKind::ForEachExpr {
3066                    block,
3067                    list: Box::new(placeholder),
3068                },
3069                line,
3070            }),
3071            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
3072                kind: ExprKind::PMapExpr {
3073                    block,
3074                    list: Box::new(placeholder),
3075                    progress: None,
3076                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
3077                    on_cluster: None,
3078                    stream: name == "pmaps" || name == "pflat_maps",
3079                },
3080                line,
3081            }),
3082            "pgrep" | "pgreps" => Ok(Expr {
3083                kind: ExprKind::PGrepExpr {
3084                    block,
3085                    list: Box::new(placeholder),
3086                    progress: None,
3087                    stream: name == "pgreps",
3088                },
3089                line,
3090            }),
3091            "pfor" => Ok(Expr {
3092                kind: ExprKind::PForExpr {
3093                    block,
3094                    list: Box::new(placeholder),
3095                    progress: None,
3096                },
3097                line,
3098            }),
3099            "preduce" => Ok(Expr {
3100                kind: ExprKind::PReduceExpr {
3101                    block,
3102                    list: Box::new(placeholder),
3103                    progress: None,
3104                },
3105                line,
3106            }),
3107            "pcache" => Ok(Expr {
3108                kind: ExprKind::PcacheExpr {
3109                    block,
3110                    list: Box::new(placeholder),
3111                    progress: None,
3112                },
3113                line,
3114            }),
3115            "psort" => Ok(Expr {
3116                kind: ExprKind::PSortExpr {
3117                    cmp: Some(block),
3118                    list: Box::new(placeholder),
3119                    progress: None,
3120                },
3121                line,
3122            }),
3123            _ => {
3124                // Generic: parse block and treat as FuncCall with code ref arg.
3125                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
3126                // need the threaded list slot pre-allocated at args[1] so
3127                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
3128                // For everything else, the generic pipe-forward arm prepends or
3129                // appends the lhs based on `thread_last_mode`.
3130                let code_ref = Expr {
3131                    kind: ExprKind::CodeRef {
3132                        params: vec![],
3133                        body: block,
3134                    },
3135                    line,
3136                };
3137                let args = if Self::is_block_then_list_pipe_builtin(name) {
3138                    vec![code_ref, placeholder]
3139                } else {
3140                    vec![code_ref]
3141                };
3142                Ok(Expr {
3143                    kind: ExprKind::FuncCall {
3144                        name: name.to_string(),
3145                        args,
3146                    },
3147                    line,
3148                })
3149            }
3150        }
3151    }
3152
3153    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
3154    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
3155        let line = self.peek_line();
3156        self.advance(); // tie
3157        let target = match self.peek().clone() {
3158            Token::HashVar(h) => {
3159                self.advance();
3160                TieTarget::Hash(h)
3161            }
3162            Token::ArrayVar(a) => {
3163                self.advance();
3164                TieTarget::Array(a)
3165            }
3166            Token::ScalarVar(s) => {
3167                self.advance();
3168                TieTarget::Scalar(s)
3169            }
3170            tok => {
3171                return Err(self.syntax_err(
3172                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
3173                    self.peek_line(),
3174                ));
3175            }
3176        };
3177        self.expect(&Token::Comma)?;
3178        let class = self.parse_assign_expr()?;
3179        let mut args = Vec::new();
3180        while self.eat(&Token::Comma) {
3181            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3182                break;
3183            }
3184            args.push(self.parse_assign_expr()?);
3185        }
3186        self.eat(&Token::Semicolon);
3187        Ok(Statement {
3188            label: None,
3189            kind: StmtKind::Tie {
3190                target,
3191                class,
3192                args,
3193            },
3194            line,
3195        })
3196    }
3197
3198    /// `given (EXPR) { ... }`
3199    fn parse_given(&mut self) -> PerlResult<Statement> {
3200        let line = self.peek_line();
3201        self.advance();
3202        self.expect(&Token::LParen)?;
3203        let topic = self.parse_expression()?;
3204        self.expect(&Token::RParen)?;
3205        let body = self.parse_block()?;
3206        self.eat(&Token::Semicolon);
3207        Ok(Statement {
3208            label: None,
3209            kind: StmtKind::Given { topic, body },
3210            line,
3211        })
3212    }
3213
3214    /// `when (COND) { ... }` — only meaningful inside `given`
3215    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
3216        let line = self.peek_line();
3217        self.advance();
3218        self.expect(&Token::LParen)?;
3219        let cond = self.parse_expression()?;
3220        self.expect(&Token::RParen)?;
3221        let body = self.parse_block()?;
3222        self.eat(&Token::Semicolon);
3223        Ok(Statement {
3224            label: None,
3225            kind: StmtKind::When { cond, body },
3226            line,
3227        })
3228    }
3229
3230    /// `default { ... }` — only meaningful inside `given`
3231    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
3232        let line = self.peek_line();
3233        self.advance();
3234        let body = self.parse_block()?;
3235        self.eat(&Token::Semicolon);
3236        Ok(Statement {
3237            label: None,
3238            kind: StmtKind::DefaultCase { body },
3239            line,
3240        })
3241    }
3242
3243    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3244    ///
3245    /// Desugars to an if/elsif/else chain at parse time.
3246    /// Each arm is `condition => { body }` or `condition => expr`.
3247    /// `default => ...` becomes the else branch.
3248    fn parse_cond_expr(&mut self, line: usize) -> PerlResult<Expr> {
3249        self.expect(&Token::LBrace)?;
3250
3251        let mut arms: Vec<(Expr, Block)> = Vec::new();
3252        let mut else_block: Option<Block> = None;
3253
3254        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3255            let arm_line = self.peek_line();
3256
3257            // Check for `default =>`
3258            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3259                && matches!(self.peek_at(1), Token::FatArrow);
3260
3261            if is_default {
3262                self.advance(); // consume `default`
3263                self.advance(); // consume `=>`
3264                let body = if matches!(self.peek(), Token::LBrace) {
3265                    self.parse_block()?
3266                } else {
3267                    let expr = self.parse_assign_expr()?;
3268                    vec![Statement {
3269                        label: None,
3270                        kind: StmtKind::Expression(expr),
3271                        line: arm_line,
3272                    }]
3273                };
3274                else_block = Some(body);
3275                self.eat(&Token::Comma);
3276                break; // default must be last
3277            }
3278
3279            // Parse condition expression (stop before `=>`)
3280            let condition = self.parse_assign_expr()?;
3281            self.expect(&Token::FatArrow)?;
3282
3283            let body = if matches!(self.peek(), Token::LBrace) {
3284                self.parse_block()?
3285            } else {
3286                let expr = self.parse_assign_expr()?;
3287                vec![Statement {
3288                    label: None,
3289                    kind: StmtKind::Expression(expr),
3290                    line: arm_line,
3291                }]
3292            };
3293
3294            arms.push((condition, body));
3295            self.eat(&Token::Comma);
3296        }
3297
3298        self.expect(&Token::RBrace)?;
3299
3300        if arms.is_empty() {
3301            return Err(self.syntax_err("cond requires at least one condition arm", line));
3302        }
3303
3304        // Build if/elsif/else chain from the arms.
3305        let (first_cond, first_body) = arms.remove(0);
3306        let elsifs: Vec<(Expr, Block)> = arms;
3307
3308        // Wrap in a do-block so `cond { ... }` is an expression.
3309        let if_stmt = Statement {
3310            label: None,
3311            kind: StmtKind::If {
3312                condition: first_cond,
3313                body: first_body,
3314                elsifs,
3315                else_block,
3316            },
3317            line,
3318        };
3319        let inner = Expr {
3320            kind: ExprKind::CodeRef {
3321                params: vec![],
3322                body: vec![if_stmt],
3323            },
3324            line,
3325        };
3326        Ok(Expr {
3327            kind: ExprKind::Do(Box::new(inner)),
3328            line,
3329        })
3330    }
3331
3332    /// `match (EXPR) { PATTERN => EXPR, ... }`
3333    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
3334        self.expect(&Token::LParen)?;
3335        let subject = self.parse_expression()?;
3336        self.expect(&Token::RParen)?;
3337        self.expect(&Token::LBrace)?;
3338        let mut arms = Vec::new();
3339        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3340            if self.eat(&Token::Semicolon) {
3341                continue;
3342            }
3343            let pattern = self.parse_match_pattern()?;
3344            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3345                self.advance();
3346                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3347                // separator (see [`Self::parse_comma_expr`]).
3348                Some(Box::new(self.parse_assign_expr()?))
3349            } else {
3350                None
3351            };
3352            self.expect(&Token::FatArrow)?;
3353            // Use assign-level parsing so commas separate arms, not `List` elements.
3354            let body = self.parse_assign_expr()?;
3355            arms.push(MatchArm {
3356                pattern,
3357                guard,
3358                body,
3359            });
3360            self.eat(&Token::Comma);
3361        }
3362        self.expect(&Token::RBrace)?;
3363        Ok(Expr {
3364            kind: ExprKind::AlgebraicMatch {
3365                subject: Box::new(subject),
3366                arms,
3367            },
3368            line,
3369        })
3370    }
3371
3372    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
3373        match self.peek().clone() {
3374            Token::Regex(pattern, flags, _delim) => {
3375                self.advance();
3376                Ok(MatchPattern::Regex { pattern, flags })
3377            }
3378            Token::Ident(ref s) if s == "_" => {
3379                self.advance();
3380                Ok(MatchPattern::Any)
3381            }
3382            Token::Ident(ref s) if s == "Some" => {
3383                self.advance();
3384                self.expect(&Token::LParen)?;
3385                let name = self.parse_scalar_var_name()?;
3386                self.expect(&Token::RParen)?;
3387                Ok(MatchPattern::OptionSome(name))
3388            }
3389            Token::LBracket => self.parse_match_array_pattern(),
3390            Token::LBrace => self.parse_match_hash_pattern(),
3391            Token::LParen => {
3392                self.advance();
3393                let e = self.parse_expression()?;
3394                self.expect(&Token::RParen)?;
3395                Ok(MatchPattern::Value(Box::new(e)))
3396            }
3397            _ => {
3398                let e = self.parse_assign_expr()?;
3399                Ok(MatchPattern::Value(Box::new(e)))
3400            }
3401        }
3402    }
3403
3404    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3405    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
3406        let mut elems = Vec::new();
3407        if self.eat(&Token::RBracket) {
3408            return Ok(vec![]);
3409        }
3410        loop {
3411            if matches!(self.peek(), Token::Star) {
3412                self.advance();
3413                elems.push(MatchArrayElem::Rest);
3414                self.eat(&Token::Comma);
3415                if !matches!(self.peek(), Token::RBracket) {
3416                    return Err(self.syntax_err(
3417                        "`*` must be the last element in an array match pattern",
3418                        self.peek_line(),
3419                    ));
3420                }
3421                self.expect(&Token::RBracket)?;
3422                return Ok(elems);
3423            }
3424            if let Token::ArrayVar(name) = self.peek().clone() {
3425                self.advance();
3426                elems.push(MatchArrayElem::RestBind(name));
3427                self.eat(&Token::Comma);
3428                if !matches!(self.peek(), Token::RBracket) {
3429                    return Err(self.syntax_err(
3430                        "`@name` rest bind must be the last element in an array match pattern",
3431                        self.peek_line(),
3432                    ));
3433                }
3434                self.expect(&Token::RBracket)?;
3435                return Ok(elems);
3436            }
3437            if let Token::ScalarVar(name) = self.peek().clone() {
3438                self.advance();
3439                elems.push(MatchArrayElem::CaptureScalar(name));
3440                if self.eat(&Token::Comma) {
3441                    if matches!(self.peek(), Token::RBracket) {
3442                        break;
3443                    }
3444                    continue;
3445                }
3446                break;
3447            }
3448            let e = self.parse_assign_expr()?;
3449            elems.push(MatchArrayElem::Expr(e));
3450            if self.eat(&Token::Comma) {
3451                if matches!(self.peek(), Token::RBracket) {
3452                    break;
3453                }
3454                continue;
3455            }
3456            break;
3457        }
3458        self.expect(&Token::RBracket)?;
3459        Ok(elems)
3460    }
3461
3462    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
3463        self.expect(&Token::LBracket)?;
3464        let elems = self.parse_match_array_elems_until_rbracket()?;
3465        Ok(MatchPattern::Array(elems))
3466    }
3467
3468    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
3469        self.expect(&Token::LBrace)?;
3470        let mut pairs = Vec::new();
3471        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3472            if self.eat(&Token::Semicolon) {
3473                continue;
3474            }
3475            let key = self.parse_assign_expr()?;
3476            self.expect(&Token::FatArrow)?;
3477            match self.advance().0 {
3478                Token::Ident(ref s) if s == "_" => {
3479                    pairs.push(MatchHashPair::KeyOnly { key });
3480                }
3481                Token::ScalarVar(name) => {
3482                    pairs.push(MatchHashPair::Capture { key, name });
3483                }
3484                tok => {
3485                    return Err(self.syntax_err(
3486                        format!(
3487                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3488                            tok
3489                        ),
3490                        self.peek_line(),
3491                    ));
3492                }
3493            }
3494            self.eat(&Token::Comma);
3495        }
3496        self.expect(&Token::RBrace)?;
3497        Ok(MatchPattern::Hash(pairs))
3498    }
3499
3500    /// `eval_timeout SECS { ... }`
3501    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
3502        let line = self.peek_line();
3503        self.advance();
3504        let timeout = self.parse_postfix()?;
3505        let body = self.parse_block_or_bareword_block_no_args()?;
3506        self.eat(&Token::Semicolon);
3507        Ok(Statement {
3508            label: None,
3509            kind: StmtKind::EvalTimeout { timeout, body },
3510            line,
3511        })
3512    }
3513
3514    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3515        match &mut cond.kind {
3516            ExprKind::Match {
3517                flags, scalar_g, ..
3518            } if flags.contains('g') => {
3519                *scalar_g = true;
3520            }
3521            ExprKind::UnaryOp {
3522                op: UnaryOp::LogNot,
3523                expr,
3524            } => {
3525                if let ExprKind::Match {
3526                    flags, scalar_g, ..
3527                } = &mut expr.kind
3528                {
3529                    if flags.contains('g') {
3530                        *scalar_g = true;
3531                    }
3532                }
3533            }
3534            _ => {}
3535        }
3536    }
3537
3538    fn parse_if(&mut self) -> PerlResult<Statement> {
3539        let line = self.peek_line();
3540        self.advance(); // 'if'
3541        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3542            if crate::compat_mode() {
3543                return Err(self.syntax_err(
3544                    "`if let` is a stryke extension (disabled by --compat)",
3545                    line,
3546                ));
3547            }
3548            return self.parse_if_let(line);
3549        }
3550        self.expect(&Token::LParen)?;
3551        let mut cond = self.parse_expression()?;
3552        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3553        self.expect(&Token::RParen)?;
3554        let body = self.parse_block()?;
3555
3556        let mut elsifs = Vec::new();
3557        let mut else_block = None;
3558
3559        loop {
3560            if let Token::Ident(ref kw) = self.peek().clone() {
3561                if kw == "elsif" {
3562                    self.advance();
3563                    self.expect(&Token::LParen)?;
3564                    let mut c = self.parse_expression()?;
3565                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3566                    self.expect(&Token::RParen)?;
3567                    let b = self.parse_block()?;
3568                    elsifs.push((c, b));
3569                    continue;
3570                }
3571                if kw == "else" {
3572                    self.advance();
3573                    else_block = Some(self.parse_block()?);
3574                }
3575            }
3576            break;
3577        }
3578
3579        Ok(Statement {
3580            label: None,
3581            kind: StmtKind::If {
3582                condition: cond,
3583                body,
3584                elsifs,
3585                else_block,
3586            },
3587            line,
3588        })
3589    }
3590
3591    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3592    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3593        self.advance(); // `let`
3594        let pattern = self.parse_match_pattern()?;
3595        self.expect(&Token::Assign)?;
3596        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3597        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3598        let rhs = self.parse_assign_expr();
3599        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3600        let rhs = rhs?;
3601        let then_block = self.parse_block()?;
3602        let else_block_opt = match self.peek().clone() {
3603            Token::Ident(ref kw) if kw == "else" => {
3604                self.advance();
3605                Some(self.parse_block()?)
3606            }
3607            Token::Ident(ref kw) if kw == "elsif" => {
3608                return Err(self.syntax_err(
3609                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3610                    self.peek_line(),
3611                ));
3612            }
3613            _ => None,
3614        };
3615        let then_expr = Self::expr_do_anon_block(then_block, line);
3616        let else_expr = if let Some(eb) = else_block_opt {
3617            Self::expr_do_anon_block(eb, line)
3618        } else {
3619            Expr {
3620                kind: ExprKind::Undef,
3621                line,
3622            }
3623        };
3624        let arms = vec![
3625            MatchArm {
3626                pattern,
3627                guard: None,
3628                body: then_expr,
3629            },
3630            MatchArm {
3631                pattern: MatchPattern::Any,
3632                guard: None,
3633                body: else_expr,
3634            },
3635        ];
3636        Ok(Statement {
3637            label: None,
3638            kind: StmtKind::Expression(Expr {
3639                kind: ExprKind::AlgebraicMatch {
3640                    subject: Box::new(rhs),
3641                    arms,
3642                },
3643                line,
3644            }),
3645            line,
3646        })
3647    }
3648
3649    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3650        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3651        Expr {
3652            kind: ExprKind::Do(Box::new(Expr {
3653                kind: ExprKind::CodeRef {
3654                    params: vec![],
3655                    body: block,
3656                },
3657                line: inner_line,
3658            })),
3659            line: outer_line,
3660        }
3661    }
3662
3663    fn parse_unless(&mut self) -> PerlResult<Statement> {
3664        let line = self.peek_line();
3665        self.advance(); // 'unless'
3666        self.expect(&Token::LParen)?;
3667        let mut cond = self.parse_expression()?;
3668        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3669        self.expect(&Token::RParen)?;
3670        let body = self.parse_block()?;
3671        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3672            if kw == "else" {
3673                self.advance();
3674                Some(self.parse_block()?)
3675            } else {
3676                None
3677            }
3678        } else {
3679            None
3680        };
3681        Ok(Statement {
3682            label: None,
3683            kind: StmtKind::Unless {
3684                condition: cond,
3685                body,
3686                else_block,
3687            },
3688            line,
3689        })
3690    }
3691
3692    fn parse_while(&mut self) -> PerlResult<Statement> {
3693        let line = self.peek_line();
3694        self.advance(); // 'while'
3695        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3696            if crate::compat_mode() {
3697                return Err(self.syntax_err(
3698                    "`while let` is a stryke extension (disabled by --compat)",
3699                    line,
3700                ));
3701            }
3702            return self.parse_while_let(line);
3703        }
3704        self.expect(&Token::LParen)?;
3705        let mut cond = self.parse_expression()?;
3706        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3707        self.expect(&Token::RParen)?;
3708        let body = self.parse_block()?;
3709        let continue_block = self.parse_optional_continue_block()?;
3710        Ok(Statement {
3711            label: None,
3712            kind: StmtKind::While {
3713                condition: cond,
3714                body,
3715                label: None,
3716                continue_block,
3717            },
3718            line,
3719        })
3720    }
3721
3722    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3723    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3724    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3725        self.advance(); // `let`
3726        let pattern = self.parse_match_pattern()?;
3727        self.expect(&Token::Assign)?;
3728        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3729        let rhs = self.parse_assign_expr();
3730        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3731        let rhs = rhs?;
3732        let mut user_body = self.parse_block()?;
3733        let continue_block = self.parse_optional_continue_block()?;
3734        user_body.push(Statement::new(
3735            StmtKind::Expression(Expr {
3736                kind: ExprKind::Integer(1),
3737                line,
3738            }),
3739            line,
3740        ));
3741        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3742        let match_expr = Expr {
3743            kind: ExprKind::AlgebraicMatch {
3744                subject: Box::new(rhs),
3745                arms: vec![
3746                    MatchArm {
3747                        pattern,
3748                        guard: None,
3749                        body: Self::expr_do_anon_block(user_body, line),
3750                    },
3751                    MatchArm {
3752                        pattern: MatchPattern::Any,
3753                        guard: None,
3754                        body: Expr {
3755                            kind: ExprKind::Integer(0),
3756                            line,
3757                        },
3758                    },
3759                ],
3760            },
3761            line,
3762        };
3763        let my_stmt = Statement::new(
3764            StmtKind::My(vec![VarDecl {
3765                sigil: Sigil::Scalar,
3766                name: tmp.clone(),
3767                initializer: Some(match_expr),
3768                frozen: false,
3769                type_annotation: None,
3770            }]),
3771            line,
3772        );
3773        let unless_last = Statement::new(
3774            StmtKind::Unless {
3775                condition: Expr {
3776                    kind: ExprKind::ScalarVar(tmp),
3777                    line,
3778                },
3779                body: vec![Statement::new(StmtKind::Last(None), line)],
3780                else_block: None,
3781            },
3782            line,
3783        );
3784        Ok(Statement::new(
3785            StmtKind::While {
3786                condition: Expr {
3787                    kind: ExprKind::Integer(1),
3788                    line,
3789                },
3790                body: vec![my_stmt, unless_last],
3791                label: None,
3792                continue_block,
3793            },
3794            line,
3795        ))
3796    }
3797
3798    fn parse_until(&mut self) -> PerlResult<Statement> {
3799        let line = self.peek_line();
3800        self.advance(); // 'until'
3801        self.expect(&Token::LParen)?;
3802        let mut cond = self.parse_expression()?;
3803        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3804        self.expect(&Token::RParen)?;
3805        let body = self.parse_block()?;
3806        let continue_block = self.parse_optional_continue_block()?;
3807        Ok(Statement {
3808            label: None,
3809            kind: StmtKind::Until {
3810                condition: cond,
3811                body,
3812                label: None,
3813                continue_block,
3814            },
3815            line,
3816        })
3817    }
3818
3819    /// `continue { ... }` after a loop body (optional).
3820    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3821        if let Token::Ident(ref kw) = self.peek().clone() {
3822            if kw == "continue" {
3823                self.advance();
3824                return Ok(Some(self.parse_block()?));
3825            }
3826        }
3827        Ok(None)
3828    }
3829
3830    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3831        let line = self.peek_line();
3832        self.advance(); // 'for'
3833
3834        // Peek to determine if C-style for or foreach
3835        // C-style: for (init; cond; step)
3836        // foreach-style: for $var (list) or for (list)
3837        match self.peek() {
3838            Token::LParen => {
3839                // Check if next after ( is a semicolon or an assignment — C-style
3840                // Or if it's a list — foreach-style
3841                // Heuristic: if the token after ( is 'my' or '$' followed by
3842                // content that contains ';', it's C-style.
3843                let saved = self.pos;
3844                self.advance(); // consume (
3845                                // Look for semicolon at paren depth 0
3846                let mut depth = 1;
3847                let mut has_semi = false;
3848                let mut scan = self.pos;
3849                while scan < self.tokens.len() {
3850                    match &self.tokens[scan].0 {
3851                        Token::LParen => depth += 1,
3852                        Token::RParen => {
3853                            depth -= 1;
3854                            if depth == 0 {
3855                                break;
3856                            }
3857                        }
3858                        Token::Semicolon if depth == 1 => {
3859                            has_semi = true;
3860                            break;
3861                        }
3862                        _ => {}
3863                    }
3864                    scan += 1;
3865                }
3866                self.pos = saved;
3867
3868                if has_semi {
3869                    self.parse_c_style_for(line)
3870                } else {
3871                    // foreach without explicit var — uses $_
3872                    self.expect(&Token::LParen)?;
3873                    let list = self.parse_expression()?;
3874                    self.expect(&Token::RParen)?;
3875                    let body = self.parse_block()?;
3876                    let continue_block = self.parse_optional_continue_block()?;
3877                    Ok(Statement {
3878                        label: None,
3879                        kind: StmtKind::Foreach {
3880                            var: "_".to_string(),
3881                            list,
3882                            body,
3883                            label: None,
3884                            continue_block,
3885                        },
3886                        line,
3887                    })
3888                }
3889            }
3890            Token::Ident(ref kw) if kw == "my" => {
3891                self.advance(); // 'my'
3892                let var = self.parse_scalar_var_name()?;
3893                self.expect(&Token::LParen)?;
3894                let list = self.parse_expression()?;
3895                self.expect(&Token::RParen)?;
3896                let body = self.parse_block()?;
3897                let continue_block = self.parse_optional_continue_block()?;
3898                Ok(Statement {
3899                    label: None,
3900                    kind: StmtKind::Foreach {
3901                        var,
3902                        list,
3903                        body,
3904                        label: None,
3905                        continue_block,
3906                    },
3907                    line,
3908                })
3909            }
3910            Token::ScalarVar(_) => {
3911                let var = self.parse_scalar_var_name()?;
3912                self.expect(&Token::LParen)?;
3913                let list = self.parse_expression()?;
3914                self.expect(&Token::RParen)?;
3915                let body = self.parse_block()?;
3916                let continue_block = self.parse_optional_continue_block()?;
3917                Ok(Statement {
3918                    label: None,
3919                    kind: StmtKind::Foreach {
3920                        var,
3921                        list,
3922                        body,
3923                        label: None,
3924                        continue_block,
3925                    },
3926                    line,
3927                })
3928            }
3929            _ => self.parse_c_style_for(line),
3930        }
3931    }
3932
3933    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
3934        self.expect(&Token::LParen)?;
3935        let init = if self.eat(&Token::Semicolon) {
3936            None
3937        } else {
3938            let s = self.parse_statement()?;
3939            self.eat(&Token::Semicolon);
3940            Some(Box::new(s))
3941        };
3942        let mut condition = if matches!(self.peek(), Token::Semicolon) {
3943            None
3944        } else {
3945            Some(self.parse_expression()?)
3946        };
3947        if let Some(ref mut c) = condition {
3948            Self::mark_match_scalar_g_for_boolean_condition(c);
3949        }
3950        self.expect(&Token::Semicolon)?;
3951        let step = if matches!(self.peek(), Token::RParen) {
3952            None
3953        } else {
3954            Some(self.parse_expression()?)
3955        };
3956        self.expect(&Token::RParen)?;
3957        let body = self.parse_block()?;
3958        let continue_block = self.parse_optional_continue_block()?;
3959        Ok(Statement {
3960            label: None,
3961            kind: StmtKind::For {
3962                init,
3963                condition,
3964                step,
3965                body,
3966                label: None,
3967                continue_block,
3968            },
3969            line,
3970        })
3971    }
3972
3973    fn parse_foreach(&mut self) -> PerlResult<Statement> {
3974        let line = self.peek_line();
3975        self.advance(); // 'foreach'
3976        let var = match self.peek() {
3977            Token::Ident(ref kw) if kw == "my" => {
3978                self.advance();
3979                self.parse_scalar_var_name()?
3980            }
3981            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
3982            _ => "_".to_string(),
3983        };
3984        self.expect(&Token::LParen)?;
3985        let list = self.parse_expression()?;
3986        self.expect(&Token::RParen)?;
3987        let body = self.parse_block()?;
3988        let continue_block = self.parse_optional_continue_block()?;
3989        Ok(Statement {
3990            label: None,
3991            kind: StmtKind::Foreach {
3992                var,
3993                list,
3994                body,
3995                label: None,
3996                continue_block,
3997            },
3998            line,
3999        })
4000    }
4001
4002    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
4003        match self.advance() {
4004            (Token::ScalarVar(name), _) => Ok(name),
4005            (tok, line) => {
4006                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
4007            }
4008        }
4009    }
4010
4011    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
4012    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
4013        let mut s = String::new();
4014        loop {
4015            match self.peek().clone() {
4016                Token::RParen => {
4017                    self.advance();
4018                    break;
4019                }
4020                Token::Eof => {
4021                    return Err(self.syntax_err(
4022                        "Unterminated sub prototype (expected ')' before end of input)",
4023                        self.peek_line(),
4024                    ));
4025                }
4026                Token::ScalarVar(v) if v == ")" => {
4027                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
4028                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
4029                    self.advance();
4030                    s.push('$');
4031                    if matches!(self.peek(), Token::LBrace) {
4032                        break;
4033                    }
4034                }
4035                Token::Ident(i) => {
4036                    let i = i.clone();
4037                    self.advance();
4038                    s.push_str(&i);
4039                }
4040                Token::Semicolon => {
4041                    self.advance();
4042                    s.push(';');
4043                }
4044                Token::LParen => {
4045                    self.advance();
4046                    s.push('(');
4047                }
4048                Token::LBracket => {
4049                    self.advance();
4050                    s.push('[');
4051                }
4052                Token::RBracket => {
4053                    self.advance();
4054                    s.push(']');
4055                }
4056                Token::Backslash => {
4057                    self.advance();
4058                    s.push('\\');
4059                }
4060                Token::Comma => {
4061                    self.advance();
4062                    s.push(',');
4063                }
4064                Token::ScalarVar(v) => {
4065                    let v = v.clone();
4066                    self.advance();
4067                    s.push('$');
4068                    s.push_str(&v);
4069                }
4070                Token::ArrayVar(v) => {
4071                    let v = v.clone();
4072                    self.advance();
4073                    s.push('@');
4074                    s.push_str(&v);
4075                }
4076                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
4077                Token::ArrayAt => {
4078                    self.advance();
4079                    s.push('@');
4080                }
4081                Token::HashVar(v) => {
4082                    let v = v.clone();
4083                    self.advance();
4084                    s.push('%');
4085                    s.push_str(&v);
4086                }
4087                Token::HashPercent => {
4088                    self.advance();
4089                    s.push('%');
4090                }
4091                Token::Plus => {
4092                    self.advance();
4093                    s.push('+');
4094                }
4095                Token::Minus => {
4096                    self.advance();
4097                    s.push('-');
4098                }
4099                Token::BitAnd => {
4100                    self.advance();
4101                    s.push('&');
4102                }
4103                tok => {
4104                    return Err(self.syntax_err(
4105                        format!("Unexpected token in sub prototype: {:?}", tok),
4106                        self.peek_line(),
4107                    ));
4108                }
4109            }
4110        }
4111        Ok(s)
4112    }
4113
4114    fn sub_signature_list_starts_here(&self) -> bool {
4115        match self.peek() {
4116            Token::LBrace | Token::LBracket => true,
4117            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
4118            Token::ArrayVar(_) | Token::HashVar(_) => true,
4119            _ => false,
4120        }
4121    }
4122
4123    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
4124        let (tok, line) = self.advance();
4125        match tok {
4126            Token::Ident(i) => Ok(i),
4127            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
4128            tok => Err(self.syntax_err(
4129                format!(
4130                    "sub signature: expected hash key (identifier or string), got {:?}",
4131                    tok
4132                ),
4133                line,
4134            )),
4135        }
4136    }
4137
4138    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
4139        let mut params = Vec::new();
4140        loop {
4141            if matches!(self.peek(), Token::RParen) {
4142                break;
4143            }
4144            match self.peek().clone() {
4145                Token::ScalarVar(name) => {
4146                    if name == "$$" || name == ")" {
4147                        return Err(self.syntax_err(
4148                            format!(
4149                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
4150                            ),
4151                            self.peek_line(),
4152                        ));
4153                    }
4154                    self.advance();
4155                    let ty = if self.eat(&Token::Colon) {
4156                        match self.peek() {
4157                            Token::Ident(ref tname) => {
4158                                let tname = tname.clone();
4159                                self.advance();
4160                                Some(match tname.as_str() {
4161                                    "Int" => PerlTypeName::Int,
4162                                    "Str" => PerlTypeName::Str,
4163                                    "Float" => PerlTypeName::Float,
4164                                    "Bool" => PerlTypeName::Bool,
4165                                    "Array" => PerlTypeName::Array,
4166                                    "Hash" => PerlTypeName::Hash,
4167                                    "Ref" => PerlTypeName::Ref,
4168                                    "Any" => PerlTypeName::Any,
4169                                    _ => PerlTypeName::Struct(tname),
4170                                })
4171                            }
4172                            _ => {
4173                                return Err(self.syntax_err(
4174                                    "expected type name after `:` in sub signature",
4175                                    self.peek_line(),
4176                                ));
4177                            }
4178                        }
4179                    } else {
4180                        None
4181                    };
4182                    // Check for default value: `$x = expr`
4183                    let default = if self.eat(&Token::Assign) {
4184                        Some(Box::new(self.parse_ternary()?))
4185                    } else {
4186                        None
4187                    };
4188                    params.push(SubSigParam::Scalar(name, ty, default));
4189                }
4190                Token::ArrayVar(name) => {
4191                    self.advance();
4192                    let default = if self.eat(&Token::Assign) {
4193                        Some(Box::new(self.parse_ternary()?))
4194                    } else {
4195                        None
4196                    };
4197                    params.push(SubSigParam::Array(name, default));
4198                }
4199                Token::HashVar(name) => {
4200                    self.advance();
4201                    let default = if self.eat(&Token::Assign) {
4202                        Some(Box::new(self.parse_ternary()?))
4203                    } else {
4204                        None
4205                    };
4206                    params.push(SubSigParam::Hash(name, default));
4207                }
4208                Token::LBracket => {
4209                    self.advance();
4210                    let elems = self.parse_match_array_elems_until_rbracket()?;
4211                    params.push(SubSigParam::ArrayDestruct(elems));
4212                }
4213                Token::LBrace => {
4214                    self.advance();
4215                    let mut pairs = Vec::new();
4216                    loop {
4217                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4218                            break;
4219                        }
4220                        if self.eat(&Token::Comma) {
4221                            continue;
4222                        }
4223                        let key = self.parse_sub_signature_hash_key()?;
4224                        self.expect(&Token::FatArrow)?;
4225                        let bind = self.parse_scalar_var_name()?;
4226                        pairs.push((key, bind));
4227                        self.eat(&Token::Comma);
4228                    }
4229                    self.expect(&Token::RBrace)?;
4230                    params.push(SubSigParam::HashDestruct(pairs));
4231                }
4232                tok => {
4233                    return Err(self.syntax_err(
4234                        format!(
4235                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4236                            tok
4237                        ),
4238                        self.peek_line(),
4239                    ));
4240                }
4241            }
4242            match self.peek() {
4243                Token::Comma => {
4244                    self.advance();
4245                    if matches!(self.peek(), Token::RParen) {
4246                        return Err(self.syntax_err(
4247                            "trailing `,` before `)` in sub signature",
4248                            self.peek_line(),
4249                        ));
4250                    }
4251                }
4252                Token::RParen => break,
4253                _ => {
4254                    return Err(self.syntax_err(
4255                        format!(
4256                            "expected `,` or `)` after sub signature parameter, got {:?}",
4257                            self.peek()
4258                        ),
4259                        self.peek_line(),
4260                    ));
4261                }
4262            }
4263        }
4264        Ok(params)
4265    }
4266
4267    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4268    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
4269        if !matches!(self.peek(), Token::LParen) {
4270            return Ok((vec![], None));
4271        }
4272        self.advance();
4273        if matches!(self.peek(), Token::RParen) {
4274            self.advance();
4275            return Ok((vec![], Some(String::new())));
4276        }
4277        if self.sub_signature_list_starts_here() {
4278            let params = self.parse_sub_signature_param_list()?;
4279            self.expect(&Token::RParen)?;
4280            return Ok((params, None));
4281        }
4282        let proto = self.parse_legacy_sub_prototype_tail()?;
4283        Ok((vec![], Some(proto)))
4284    }
4285
4286    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4287    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
4288        while self.eat(&Token::Colon) {
4289            match self.advance() {
4290                (Token::Ident(_), _) => {}
4291                (tok, line) => {
4292                    return Err(self.syntax_err(
4293                        format!("Expected attribute name after `:`, got {:?}", tok),
4294                        line,
4295                    ));
4296                }
4297            }
4298            if self.eat(&Token::LParen) {
4299                let mut depth = 1usize;
4300                while depth > 0 {
4301                    match self.advance().0 {
4302                        Token::LParen => depth += 1,
4303                        Token::RParen => {
4304                            depth -= 1;
4305                        }
4306                        Token::Eof => {
4307                            return Err(self.syntax_err(
4308                                "Unterminated sub attribute argument list",
4309                                self.peek_line(),
4310                            ));
4311                        }
4312                        _ => {}
4313                    }
4314                }
4315            }
4316        }
4317        Ok(())
4318    }
4319
4320    /// After `fn` + optional `(SIG)` + attrs: `{ ... }` or stryke-only `= EXPR` (one assign-level
4321    /// expression; no top-level `,`). `sub` always requires `{ ... }`.
4322    fn parse_fn_eq_body_or_block(&mut self, is_sub_keyword: bool) -> PerlResult<Block> {
4323        if !is_sub_keyword && self.eat(&Token::Assign) {
4324            let expr = self.parse_assign_expr()?;
4325            if matches!(self.peek(), Token::Comma) {
4326                return Err(self.syntax_err(
4327                    "`fn ... =` allows only a single expression; use `fn ... { ... }` for multiple statements",
4328                    self.peek_line(),
4329                ));
4330            }
4331            let eline = expr.line;
4332            self.eat(&Token::Semicolon);
4333            let mut body = vec![Statement {
4334                label: None,
4335                kind: StmtKind::Expression(expr),
4336                line: eline,
4337            }];
4338            Self::default_topic_for_sole_bareword(&mut body);
4339            Ok(body)
4340        } else {
4341            self.parse_block()
4342        }
4343    }
4344
4345    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> PerlResult<Statement> {
4346        let line = self.peek_line();
4347        self.advance(); // 'sub' or 'fn'
4348        match self.peek().clone() {
4349            Token::Ident(_) => {
4350                let name = self.parse_package_qualified_identifier()?;
4351                // Topic-slot barewords (`_`, `_<`, `_<<`, `_<<<`, `_<<<<`,
4352                // `_0`, `_1`, …, `_N`, plus `_N<+` chain forms) are scalar
4353                // refs to the current/positional/outer topic. A user-defined
4354                // sub with any of these names — bare or package-qualified —
4355                // would shadow the topic in expression position and silently
4356                // break every `_`-aware builtin (`map { _ }`, `say _`,
4357                // `lc _`, …). Reject ALL forms at parse time, including
4358                // `Foo::_`, `Pkg::_0`, `My::Module::_<<<<`.
4359                let bare = name.rsplit("::").next().unwrap_or(&name);
4360                if Self::is_underscore_topic_slot(bare) {
4361                    return Err(self.syntax_err(
4362                        format!(
4363                            "`fn {}` would shadow the topic-slot scalar; pick a different name",
4364                            name
4365                        ),
4366                        line,
4367                    ));
4368                }
4369                // Allow shadowing builtins:
4370                // - In compat mode (full Perl 5)
4371                // - When parsing a module (imports should work)
4372                // Block shadowing:
4373                // - In user code (default mode, not parsing module)
4374                // - Always in no-interop mode
4375                let allow_shadow =
4376                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
4377                if !allow_shadow {
4378                    self.check_udf_shadows_builtin(&name, line)?;
4379                }
4380                self.declared_subs.insert(name.clone());
4381                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4382                self.parse_sub_attributes()?;
4383                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4384                Ok(Statement {
4385                    label: None,
4386                    kind: StmtKind::SubDecl {
4387                        name,
4388                        params,
4389                        body,
4390                        prototype,
4391                    },
4392                    line,
4393                })
4394            }
4395            Token::LParen | Token::LBrace | Token::Colon => {
4396                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
4397                if is_sub_keyword && crate::no_interop_mode() {
4398                    return Err(self.syntax_err(
4399                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
4400                        line,
4401                    ));
4402                }
4403                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4404                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4405                self.parse_sub_attributes()?;
4406                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4407                Ok(Statement {
4408                    label: None,
4409                    kind: StmtKind::Expression(Expr {
4410                        kind: ExprKind::CodeRef { params, body },
4411                        line,
4412                    }),
4413                    line,
4414                })
4415            }
4416            tok => {
4417                // Sigil-form topic-slot names (`fn $_`, `fn $_<`, `fn $_0`,
4418                // `fn @_`, `fn %_`, …) are also rejected with the same
4419                // foot-gun message as the bareword form. Without this branch
4420                // the user gets a confusing generic "Expected sub name" error.
4421                let topic_name = match &tok {
4422                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n)
4423                        if Self::is_underscore_topic_slot(n) =>
4424                    {
4425                        Some((
4426                            match &tok {
4427                                Token::ScalarVar(_) => '$',
4428                                Token::ArrayVar(_) => '@',
4429                                Token::HashVar(_) => '%',
4430                                _ => unreachable!(),
4431                            },
4432                            n.clone(),
4433                        ))
4434                    }
4435                    _ => None,
4436                };
4437                if let Some((sigil, n)) = topic_name {
4438                    return Err(self.syntax_err(
4439                        format!(
4440                            "`fn {}{}` would shadow the topic-slot scalar; pick a different name",
4441                            sigil, n
4442                        ),
4443                        self.peek_line(),
4444                    ));
4445                }
4446                Err(self.syntax_err(
4447                    format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4448                    self.peek_line(),
4449                ))
4450            }
4451        }
4452    }
4453
4454    /// `before|after|around "<glob>" { ... }` — register AOP advice.
4455    /// The pattern is a glob (`*`, `?`) matched against the called sub's bare name.
4456    fn parse_advice_decl(&mut self, kind: crate::ast::AdviceKind) -> PerlResult<Statement> {
4457        let line = self.peek_line();
4458        self.advance(); // before/after/around
4459        let pattern = match self.advance() {
4460            (Token::SingleString(s), _) | (Token::DoubleString(s), _) => s,
4461            (tok, err_line) => {
4462                return Err(self.syntax_err(
4463                    format!(
4464                        "Expected string-literal pattern after `{}`, got {:?}",
4465                        match kind {
4466                            crate::ast::AdviceKind::Before => "before",
4467                            crate::ast::AdviceKind::After => "after",
4468                            crate::ast::AdviceKind::Around => "around",
4469                        },
4470                        tok
4471                    ),
4472                    err_line,
4473                ));
4474            }
4475        };
4476        let body = self.parse_block()?;
4477        Ok(Statement {
4478            label: None,
4479            kind: StmtKind::AdviceDecl {
4480                kind,
4481                pattern,
4482                body,
4483            },
4484            line,
4485        })
4486    }
4487
4488    /// `struct Name { field => Type, ... ; fn method { } }`
4489    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
4490        let line = self.peek_line();
4491        self.advance(); // struct
4492        let name = self.parse_package_qualified_identifier().map_err(|_| {
4493            self.syntax_err(
4494                format!("Expected struct name, got {:?}", self.peek()),
4495                self.peek_line(),
4496            )
4497        })?;
4498        self.expect(&Token::LBrace)?;
4499        let mut fields = Vec::new();
4500        let mut methods = Vec::new();
4501        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4502            // Check for method definition: `fn name { }` or `fn name { }`
4503            let is_method = match self.peek() {
4504                Token::Ident(s) => s == "fn" || s == "sub",
4505                _ => false,
4506            };
4507            if is_method {
4508                self.advance(); // fn/sub
4509                let method_name = match self.advance() {
4510                    (Token::Ident(n), _) => n,
4511                    (tok, err_line) => {
4512                        return Err(self
4513                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4514                    }
4515                };
4516                // Parse optional signature: `($self, $arg: Type, ...)`
4517                let params = if self.eat(&Token::LParen) {
4518                    let p = self.parse_sub_signature_param_list()?;
4519                    self.expect(&Token::RParen)?;
4520                    p
4521                } else {
4522                    Vec::new()
4523                };
4524                // parse_block handles its own { } delimiters
4525                let body = self.parse_block()?;
4526                methods.push(crate::ast::StructMethod {
4527                    name: method_name,
4528                    params,
4529                    body,
4530                });
4531                // Optional trailing comma/semicolon after method
4532                self.eat(&Token::Comma);
4533                self.eat(&Token::Semicolon);
4534                continue;
4535            }
4536
4537            let field_name = match self.advance() {
4538                (Token::Ident(n), _) => n,
4539                (tok, err_line) => {
4540                    return Err(
4541                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4542                    )
4543                }
4544            };
4545            // Support both `field => Type` and bare `field` (implies Any type)
4546            let ty = if self.eat(&Token::FatArrow) {
4547                self.parse_type_name()?
4548            } else {
4549                crate::ast::PerlTypeName::Any
4550            };
4551            let default = if self.eat(&Token::Assign) {
4552                // Use parse_ternary to avoid consuming commas (next field separator)
4553                Some(self.parse_ternary()?)
4554            } else {
4555                None
4556            };
4557            fields.push(StructField {
4558                name: field_name,
4559                ty,
4560                default,
4561            });
4562            if !self.eat(&Token::Comma) {
4563                // Also allow semicolons as field separators
4564                self.eat(&Token::Semicolon);
4565            }
4566        }
4567        self.expect(&Token::RBrace)?;
4568        self.eat(&Token::Semicolon);
4569        Ok(Statement {
4570            label: None,
4571            kind: StmtKind::StructDecl {
4572                def: StructDef {
4573                    name,
4574                    fields,
4575                    methods,
4576                },
4577            },
4578            line,
4579        })
4580    }
4581
4582    /// `enum Name { Variant1, Variant2 => Type, ... }`
4583    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
4584        let line = self.peek_line();
4585        self.advance(); // enum
4586        let name = self.parse_package_qualified_identifier().map_err(|_| {
4587            self.syntax_err(
4588                format!("Expected enum name, got {:?}", self.peek()),
4589                self.peek_line(),
4590            )
4591        })?;
4592        self.expect(&Token::LBrace)?;
4593        let mut variants = Vec::new();
4594        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4595            let variant_name = match self.advance() {
4596                (Token::Ident(n), _) => n,
4597                (tok, err_line) => {
4598                    return Err(
4599                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
4600                    )
4601                }
4602            };
4603            let ty = if self.eat(&Token::FatArrow) {
4604                Some(self.parse_type_name()?)
4605            } else {
4606                None
4607            };
4608            variants.push(EnumVariant {
4609                name: variant_name,
4610                ty,
4611            });
4612            if !self.eat(&Token::Comma) {
4613                self.eat(&Token::Semicolon);
4614            }
4615        }
4616        self.expect(&Token::RBrace)?;
4617        self.eat(&Token::Semicolon);
4618        Ok(Statement {
4619            label: None,
4620            kind: StmtKind::EnumDecl {
4621                def: EnumDef { name, variants },
4622            },
4623            line,
4624        })
4625    }
4626
4627    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
4628    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
4629        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
4630        let line = self.peek_line();
4631        self.advance(); // class
4632        let name = self.parse_package_qualified_identifier().map_err(|_| {
4633            self.syntax_err(
4634                format!("Expected class name, got {:?}", self.peek()),
4635                self.peek_line(),
4636            )
4637        })?;
4638
4639        // Parse `extends Parent1, Parent2` (each may be namespaced: `Foo::Base`)
4640        let mut extends = Vec::new();
4641        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
4642            self.advance(); // extends
4643            loop {
4644                let parent = self.parse_package_qualified_identifier().map_err(|_| {
4645                    self.syntax_err(
4646                        format!(
4647                            "Expected parent class name after `extends`, got {:?}",
4648                            self.peek()
4649                        ),
4650                        self.peek_line(),
4651                    )
4652                })?;
4653                extends.push(parent);
4654                if !self.eat(&Token::Comma) {
4655                    break;
4656                }
4657            }
4658        }
4659
4660        // Parse `impl Trait1, Trait2` (each may be namespaced: `Foo::Trait`)
4661        let mut implements = Vec::new();
4662        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
4663            self.advance(); // impl
4664            loop {
4665                let trait_name = self.parse_package_qualified_identifier().map_err(|_| {
4666                    self.syntax_err(
4667                        format!("Expected trait name after `impl`, got {:?}", self.peek()),
4668                        self.peek_line(),
4669                    )
4670                })?;
4671                implements.push(trait_name);
4672                if !self.eat(&Token::Comma) {
4673                    break;
4674                }
4675            }
4676        }
4677
4678        self.expect(&Token::LBrace)?;
4679        let mut fields = Vec::new();
4680        let mut methods = Vec::new();
4681        let mut static_fields = Vec::new();
4682
4683        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4684            // Check for visibility modifier
4685            let visibility = match self.peek() {
4686                Token::Ident(ref s) if s == "pub" => {
4687                    self.advance();
4688                    Visibility::Public
4689                }
4690                Token::Ident(ref s) if s == "priv" => {
4691                    self.advance();
4692                    Visibility::Private
4693                }
4694                Token::Ident(ref s) if s == "prot" => {
4695                    self.advance();
4696                    Visibility::Protected
4697                }
4698                _ => Visibility::Public, // default public
4699            };
4700
4701            // Check for static field: `static name: Type = default`
4702            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
4703                self.advance(); // static
4704
4705                // Could be a static method (`static fn`) or static field
4706                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4707                    // static fn is same as fn Self.name — handled below but not here
4708                    return Err(self.syntax_err(
4709                        "use `fn Self.name` for static methods, not `static fn`",
4710                        self.peek_line(),
4711                    ));
4712                }
4713
4714                let field_name = match self.advance() {
4715                    (Token::Ident(n), _) => n,
4716                    (tok, err_line) => {
4717                        return Err(self.syntax_err(
4718                            format!("Expected static field name, got {:?}", tok),
4719                            err_line,
4720                        ))
4721                    }
4722                };
4723
4724                let ty = if self.eat(&Token::Colon) {
4725                    self.parse_type_name()?
4726                } else {
4727                    crate::ast::PerlTypeName::Any
4728                };
4729
4730                let default = if self.eat(&Token::Assign) {
4731                    Some(self.parse_ternary()?)
4732                } else {
4733                    None
4734                };
4735
4736                static_fields.push(ClassStaticField {
4737                    name: field_name,
4738                    ty,
4739                    visibility,
4740                    default,
4741                });
4742
4743                if !self.eat(&Token::Comma) {
4744                    self.eat(&Token::Semicolon);
4745                }
4746                continue;
4747            }
4748
4749            // Check for `final` modifier before fn
4750            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
4751            if method_is_final {
4752                self.advance(); // final
4753            }
4754
4755            // Check for method: `fn name` or `fn Self.name` (static)
4756            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
4757            if is_method {
4758                self.advance(); // fn/sub
4759
4760                // Check for static method: `fn Self.name`
4761                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
4762                if is_static {
4763                    self.advance(); // Self
4764                    self.expect(&Token::Dot)?;
4765                }
4766
4767                let method_name = match self.advance() {
4768                    (Token::Ident(n), _) => n,
4769                    (tok, err_line) => {
4770                        return Err(self
4771                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4772                    }
4773                };
4774
4775                // Parse optional signature
4776                let params = if self.eat(&Token::LParen) {
4777                    let p = self.parse_sub_signature_param_list()?;
4778                    self.expect(&Token::RParen)?;
4779                    p
4780                } else {
4781                    Vec::new()
4782                };
4783
4784                // Body is optional (abstract method in trait has no body)
4785                let body = if matches!(self.peek(), Token::LBrace) {
4786                    Some(self.parse_block()?)
4787                } else {
4788                    None
4789                };
4790
4791                methods.push(ClassMethod {
4792                    name: method_name,
4793                    params,
4794                    body,
4795                    visibility,
4796                    is_static,
4797                    is_final: method_is_final,
4798                });
4799                self.eat(&Token::Comma);
4800                self.eat(&Token::Semicolon);
4801                continue;
4802            } else if method_is_final {
4803                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
4804            }
4805
4806            // Parse field: `name: Type = default`
4807            let field_name = match self.advance() {
4808                (Token::Ident(n), _) => n,
4809                (tok, err_line) => {
4810                    return Err(
4811                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4812                    )
4813                }
4814            };
4815
4816            // Type after colon: `name: Type`
4817            let ty = if self.eat(&Token::Colon) {
4818                self.parse_type_name()?
4819            } else {
4820                crate::ast::PerlTypeName::Any
4821            };
4822
4823            // Default value after `=`
4824            let default = if self.eat(&Token::Assign) {
4825                Some(self.parse_ternary()?)
4826            } else {
4827                None
4828            };
4829
4830            fields.push(ClassField {
4831                name: field_name,
4832                ty,
4833                visibility,
4834                default,
4835            });
4836
4837            if !self.eat(&Token::Comma) {
4838                self.eat(&Token::Semicolon);
4839            }
4840        }
4841
4842        self.expect(&Token::RBrace)?;
4843        self.eat(&Token::Semicolon);
4844
4845        Ok(Statement {
4846            label: None,
4847            kind: StmtKind::ClassDecl {
4848                def: ClassDef {
4849                    name,
4850                    is_abstract,
4851                    is_final,
4852                    extends,
4853                    implements,
4854                    fields,
4855                    methods,
4856                    static_fields,
4857                },
4858            },
4859            line,
4860        })
4861    }
4862
4863    /// `trait Name { fn required; fn with_default { } }`
4864    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
4865        use crate::ast::{ClassMethod, TraitDef, Visibility};
4866        let line = self.peek_line();
4867        self.advance(); // trait
4868        let name = self.parse_package_qualified_identifier().map_err(|_| {
4869            self.syntax_err(
4870                format!("Expected trait name, got {:?}", self.peek()),
4871                self.peek_line(),
4872            )
4873        })?;
4874
4875        self.expect(&Token::LBrace)?;
4876        let mut methods = Vec::new();
4877
4878        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4879            // Optional visibility
4880            let visibility = match self.peek() {
4881                Token::Ident(ref s) if s == "pub" => {
4882                    self.advance();
4883                    Visibility::Public
4884                }
4885                Token::Ident(ref s) if s == "priv" => {
4886                    self.advance();
4887                    Visibility::Private
4888                }
4889                Token::Ident(ref s) if s == "prot" => {
4890                    self.advance();
4891                    Visibility::Protected
4892                }
4893                _ => Visibility::Public,
4894            };
4895
4896            // Expect `fn` or `sub`
4897            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4898                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
4899            }
4900            self.advance(); // fn/sub
4901
4902            let method_name = match self.advance() {
4903                (Token::Ident(n), _) => n,
4904                (tok, err_line) => {
4905                    return Err(
4906                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
4907                    )
4908                }
4909            };
4910
4911            // Optional signature
4912            let params = if self.eat(&Token::LParen) {
4913                let p = self.parse_sub_signature_param_list()?;
4914                self.expect(&Token::RParen)?;
4915                p
4916            } else {
4917                Vec::new()
4918            };
4919
4920            // Body is optional (no body = abstract/required method)
4921            let body = if matches!(self.peek(), Token::LBrace) {
4922                Some(self.parse_block()?)
4923            } else {
4924                None
4925            };
4926
4927            methods.push(ClassMethod {
4928                name: method_name,
4929                params,
4930                body,
4931                visibility,
4932                is_static: false,
4933                is_final: false,
4934            });
4935
4936            self.eat(&Token::Comma);
4937            self.eat(&Token::Semicolon);
4938        }
4939
4940        self.expect(&Token::RBrace)?;
4941        self.eat(&Token::Semicolon);
4942
4943        Ok(Statement {
4944            label: None,
4945            kind: StmtKind::TraitDecl {
4946                def: TraitDef { name, methods },
4947            },
4948            line,
4949        })
4950    }
4951
4952    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
4953        match &target.kind {
4954            ExprKind::ScalarVar(name) => Some(VarDecl {
4955                sigil: Sigil::Scalar,
4956                name: name.clone(),
4957                initializer: None,
4958                frozen: false,
4959                type_annotation: None,
4960            }),
4961            ExprKind::ArrayVar(name) => Some(VarDecl {
4962                sigil: Sigil::Array,
4963                name: name.clone(),
4964                initializer: None,
4965                frozen: false,
4966                type_annotation: None,
4967            }),
4968            ExprKind::HashVar(name) => Some(VarDecl {
4969                sigil: Sigil::Hash,
4970                name: name.clone(),
4971                initializer: None,
4972                frozen: false,
4973                type_annotation: None,
4974            }),
4975            ExprKind::Typeglob(name) => Some(VarDecl {
4976                sigil: Sigil::Typeglob,
4977                name: name.clone(),
4978                initializer: None,
4979                frozen: false,
4980                type_annotation: None,
4981            }),
4982            _ => None,
4983        }
4984    }
4985
4986    fn parse_decl_array_destructure(
4987        &mut self,
4988        keyword: &str,
4989        line: usize,
4990    ) -> PerlResult<Statement> {
4991        self.expect(&Token::LBracket)?;
4992        let elems = self.parse_match_array_elems_until_rbracket()?;
4993        self.expect(&Token::Assign)?;
4994        self.suppress_scalar_hash_brace += 1;
4995        let rhs = self.parse_expression()?;
4996        self.suppress_scalar_hash_brace -= 1;
4997        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
4998        self.parse_stmt_postfix_modifier(stmt)
4999    }
5000
5001    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
5002        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
5003            unreachable!("parse_match_hash_pattern returns Hash");
5004        };
5005        self.expect(&Token::Assign)?;
5006        self.suppress_scalar_hash_brace += 1;
5007        let rhs = self.parse_expression()?;
5008        self.suppress_scalar_hash_brace -= 1;
5009        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
5010        self.parse_stmt_postfix_modifier(stmt)
5011    }
5012
5013    fn desugar_array_destructure(
5014        &mut self,
5015        keyword: &str,
5016        line: usize,
5017        elems: Vec<MatchArrayElem>,
5018        rhs: Expr,
5019    ) -> PerlResult<Statement> {
5020        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5021        let mut stmts: Vec<Statement> = Vec::new();
5022        stmts.push(destructure_stmt_from_var_decls(
5023            keyword,
5024            vec![VarDecl {
5025                sigil: Sigil::Scalar,
5026                name: tmp.clone(),
5027                initializer: Some(rhs),
5028                frozen: false,
5029                type_annotation: None,
5030            }],
5031            line,
5032        ));
5033
5034        let has_rest = elems
5035            .iter()
5036            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
5037        let fixed_slots = elems
5038            .iter()
5039            .filter(|e| {
5040                matches!(
5041                    e,
5042                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
5043                )
5044            })
5045            .count();
5046        if !has_rest {
5047            let cond = Expr {
5048                kind: ExprKind::BinOp {
5049                    left: Box::new(destructure_expr_array_len(&tmp, line)),
5050                    op: BinOp::NumEq,
5051                    right: Box::new(Expr {
5052                        kind: ExprKind::Integer(fixed_slots as i64),
5053                        line,
5054                    }),
5055                },
5056                line,
5057            };
5058            stmts.push(destructure_stmt_unless_die(
5059                line,
5060                cond,
5061                "array destructure: length mismatch",
5062            ));
5063        }
5064
5065        let mut idx: i64 = 0;
5066        for elem in elems {
5067            match elem {
5068                MatchArrayElem::Rest => break,
5069                MatchArrayElem::RestBind(name) => {
5070                    let list_source = Expr {
5071                        kind: ExprKind::Deref {
5072                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5073                            kind: Sigil::Array,
5074                        },
5075                        line,
5076                    };
5077                    let last_ix = Expr {
5078                        kind: ExprKind::BinOp {
5079                            left: Box::new(destructure_expr_array_len(&tmp, line)),
5080                            op: BinOp::Sub,
5081                            right: Box::new(Expr {
5082                                kind: ExprKind::Integer(1),
5083                                line,
5084                            }),
5085                        },
5086                        line,
5087                    };
5088                    let range = Expr {
5089                        kind: ExprKind::Range {
5090                            from: Box::new(Expr {
5091                                kind: ExprKind::Integer(idx),
5092                                line,
5093                            }),
5094                            to: Box::new(last_ix),
5095                            exclusive: false,
5096                            step: None,
5097                        },
5098                        line,
5099                    };
5100                    let slice = Expr {
5101                        kind: ExprKind::AnonymousListSlice {
5102                            source: Box::new(list_source),
5103                            indices: vec![range],
5104                        },
5105                        line,
5106                    };
5107                    stmts.push(destructure_stmt_from_var_decls(
5108                        keyword,
5109                        vec![VarDecl {
5110                            sigil: Sigil::Array,
5111                            name,
5112                            initializer: Some(slice),
5113                            frozen: false,
5114                            type_annotation: None,
5115                        }],
5116                        line,
5117                    ));
5118                    break;
5119                }
5120                MatchArrayElem::CaptureScalar(name) => {
5121                    let arrow = Expr {
5122                        kind: ExprKind::ArrowDeref {
5123                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5124                            index: Box::new(Expr {
5125                                kind: ExprKind::Integer(idx),
5126                                line,
5127                            }),
5128                            kind: DerefKind::Array,
5129                        },
5130                        line,
5131                    };
5132                    stmts.push(destructure_stmt_from_var_decls(
5133                        keyword,
5134                        vec![VarDecl {
5135                            sigil: Sigil::Scalar,
5136                            name,
5137                            initializer: Some(arrow),
5138                            frozen: false,
5139                            type_annotation: None,
5140                        }],
5141                        line,
5142                    ));
5143                    idx += 1;
5144                }
5145                MatchArrayElem::Expr(e) => {
5146                    let elem_subj = Expr {
5147                        kind: ExprKind::ArrowDeref {
5148                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5149                            index: Box::new(Expr {
5150                                kind: ExprKind::Integer(idx),
5151                                line,
5152                            }),
5153                            kind: DerefKind::Array,
5154                        },
5155                        line,
5156                    };
5157                    let match_expr = Expr {
5158                        kind: ExprKind::AlgebraicMatch {
5159                            subject: Box::new(elem_subj),
5160                            arms: vec![
5161                                MatchArm {
5162                                    pattern: MatchPattern::Value(Box::new(e.clone())),
5163                                    guard: None,
5164                                    body: Expr {
5165                                        kind: ExprKind::Integer(0),
5166                                        line,
5167                                    },
5168                                },
5169                                MatchArm {
5170                                    pattern: MatchPattern::Any,
5171                                    guard: None,
5172                                    body: Expr {
5173                                        kind: ExprKind::Die(vec![Expr {
5174                                            kind: ExprKind::String(
5175                                                "array destructure: element pattern mismatch"
5176                                                    .to_string(),
5177                                            ),
5178                                            line,
5179                                        }]),
5180                                        line,
5181                                    },
5182                                },
5183                            ],
5184                        },
5185                        line,
5186                    };
5187                    stmts.push(Statement {
5188                        label: None,
5189                        kind: StmtKind::Expression(match_expr),
5190                        line,
5191                    });
5192                    idx += 1;
5193                }
5194            }
5195        }
5196
5197        Ok(Statement {
5198            label: None,
5199            kind: StmtKind::StmtGroup(stmts),
5200            line,
5201        })
5202    }
5203
5204    fn desugar_hash_destructure(
5205        &mut self,
5206        keyword: &str,
5207        line: usize,
5208        pairs: Vec<MatchHashPair>,
5209        rhs: Expr,
5210    ) -> PerlResult<Statement> {
5211        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5212        let mut stmts: Vec<Statement> = Vec::new();
5213        stmts.push(destructure_stmt_from_var_decls(
5214            keyword,
5215            vec![VarDecl {
5216                sigil: Sigil::Scalar,
5217                name: tmp.clone(),
5218                initializer: Some(rhs),
5219                frozen: false,
5220                type_annotation: None,
5221            }],
5222            line,
5223        ));
5224
5225        for pair in pairs {
5226            match pair {
5227                MatchHashPair::KeyOnly { key } => {
5228                    let exists_op = Expr {
5229                        kind: ExprKind::Exists(Box::new(Expr {
5230                            kind: ExprKind::ArrowDeref {
5231                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5232                                index: Box::new(key),
5233                                kind: DerefKind::Hash,
5234                            },
5235                            line,
5236                        })),
5237                        line,
5238                    };
5239                    stmts.push(destructure_stmt_unless_die(
5240                        line,
5241                        exists_op,
5242                        "hash destructure: missing required key",
5243                    ));
5244                }
5245                MatchHashPair::Capture { key, name } => {
5246                    let init = Expr {
5247                        kind: ExprKind::ArrowDeref {
5248                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5249                            index: Box::new(key),
5250                            kind: DerefKind::Hash,
5251                        },
5252                        line,
5253                    };
5254                    stmts.push(destructure_stmt_from_var_decls(
5255                        keyword,
5256                        vec![VarDecl {
5257                            sigil: Sigil::Scalar,
5258                            name,
5259                            initializer: Some(init),
5260                            frozen: false,
5261                            type_annotation: None,
5262                        }],
5263                        line,
5264                    ));
5265                }
5266            }
5267        }
5268
5269        Ok(Statement {
5270            label: None,
5271            kind: StmtKind::StmtGroup(stmts),
5272            line,
5273        })
5274    }
5275
5276    fn parse_my_our_local(
5277        &mut self,
5278        keyword: &str,
5279        allow_type_annotation: bool,
5280    ) -> PerlResult<Statement> {
5281        let line = self.peek_line();
5282        self.advance(); // 'my'/'our'/'local'
5283
5284        if keyword == "local"
5285            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
5286        {
5287            let target = self.parse_postfix()?;
5288            let mut initializer: Option<Expr> = None;
5289            if self.eat(&Token::Assign) {
5290                initializer = Some(self.parse_expression()?);
5291            } else if matches!(
5292                self.peek(),
5293                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
5294            ) {
5295                if matches!(&target.kind, ExprKind::Typeglob(_)) {
5296                    return Err(self.syntax_err(
5297                        "compound assignment on typeglob declaration is not supported",
5298                        self.peek_line(),
5299                    ));
5300                }
5301                let op = match self.peek().clone() {
5302                    Token::OrAssign => BinOp::LogOr,
5303                    Token::DefinedOrAssign => BinOp::DefinedOr,
5304                    Token::AndAssign => BinOp::LogAnd,
5305                    _ => unreachable!(),
5306                };
5307                self.advance();
5308                let rhs = self.parse_assign_expr()?;
5309                let tgt_line = target.line;
5310                initializer = Some(Expr {
5311                    kind: ExprKind::CompoundAssign {
5312                        target: Box::new(target.clone()),
5313                        op,
5314                        value: Box::new(rhs),
5315                    },
5316                    line: tgt_line,
5317                });
5318            }
5319
5320            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
5321                decl.initializer = initializer;
5322                StmtKind::Local(vec![decl])
5323            } else {
5324                StmtKind::LocalExpr {
5325                    target,
5326                    initializer,
5327                }
5328            };
5329            let stmt = Statement {
5330                label: None,
5331                kind,
5332                line,
5333            };
5334            return self.parse_stmt_postfix_modifier(stmt);
5335        }
5336
5337        if matches!(self.peek(), Token::LBracket) {
5338            return self.parse_decl_array_destructure(keyword, line);
5339        }
5340        if matches!(self.peek(), Token::LBrace) {
5341            return self.parse_decl_hash_destructure(keyword, line);
5342        }
5343
5344        let mut decls = Vec::new();
5345
5346        if self.eat(&Token::LParen) {
5347            // my ($a, @b, %c)
5348            while !matches!(self.peek(), Token::RParen | Token::Eof) {
5349                let decl = self.parse_var_decl(allow_type_annotation)?;
5350                decls.push(decl);
5351                if !self.eat(&Token::Comma) {
5352                    break;
5353                }
5354            }
5355            self.expect(&Token::RParen)?;
5356        } else {
5357            decls.push(self.parse_var_decl(allow_type_annotation)?);
5358        }
5359
5360        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
5361        if self.eat(&Token::Assign) {
5362            if keyword == "our" && decls.len() == 1 {
5363                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
5364                    self.advance();
5365                    decls.push(self.parse_var_decl(allow_type_annotation)?);
5366                    if !self.eat(&Token::Assign) {
5367                        return Err(self.syntax_err(
5368                            "expected `=` after `our` in chained our-declaration",
5369                            self.peek_line(),
5370                        ));
5371                    }
5372                }
5373            }
5374            let val = self.parse_expression()?;
5375            // Validate assignment for single variable declarations (not destructuring)
5376            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
5377            if !crate::compat_mode() && decls.len() == 1 {
5378                let decl = &decls[0];
5379                let target_kind = match decl.sigil {
5380                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
5381                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
5382                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
5383                    Sigil::Typeglob => {
5384                        // Skip validation for typeglob
5385                        if decls.len() == 1 {
5386                            decls[0].initializer = Some(val);
5387                        } else {
5388                            for d in &mut decls {
5389                                d.initializer = Some(val.clone());
5390                            }
5391                        }
5392                        return Ok(Statement {
5393                            label: None,
5394                            kind: match keyword {
5395                                "my" => StmtKind::My(decls),
5396                                "mysync" => StmtKind::MySync(decls),
5397                                "our" => StmtKind::Our(decls),
5398                                "local" => StmtKind::Local(decls),
5399                                "state" => StmtKind::State(decls),
5400                                _ => unreachable!(),
5401                            },
5402                            line,
5403                        });
5404                    }
5405                };
5406                let target = Expr {
5407                    kind: target_kind,
5408                    line,
5409                };
5410                self.validate_assignment(&target, &val, line)?;
5411            }
5412            if decls.len() == 1 {
5413                decls[0].initializer = Some(val);
5414            } else {
5415                for decl in &mut decls {
5416                    decl.initializer = Some(val.clone());
5417                }
5418            }
5419        } else if decls.len() == 1 {
5420            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5421            let op = match self.peek().clone() {
5422                Token::OrAssign => Some(BinOp::LogOr),
5423                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5424                Token::AndAssign => Some(BinOp::LogAnd),
5425                _ => None,
5426            };
5427            if let Some(op) = op {
5428                let d = &decls[0];
5429                if matches!(d.sigil, Sigil::Typeglob) {
5430                    return Err(self.syntax_err(
5431                        "compound assignment on typeglob declaration is not supported",
5432                        self.peek_line(),
5433                    ));
5434                }
5435                self.advance();
5436                let rhs = self.parse_assign_expr()?;
5437                let target = Expr {
5438                    kind: match d.sigil {
5439                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5440                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
5441                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
5442                        Sigil::Typeglob => unreachable!(),
5443                    },
5444                    line,
5445                };
5446                decls[0].initializer = Some(Expr {
5447                    kind: ExprKind::CompoundAssign {
5448                        target: Box::new(target),
5449                        op,
5450                        value: Box::new(rhs),
5451                    },
5452                    line,
5453                });
5454            }
5455        }
5456
5457        let kind = match keyword {
5458            "my" => StmtKind::My(decls),
5459            "mysync" => StmtKind::MySync(decls),
5460            "our" => StmtKind::Our(decls),
5461            "local" => StmtKind::Local(decls),
5462            "state" => StmtKind::State(decls),
5463            _ => unreachable!(),
5464        };
5465        let stmt = Statement {
5466            label: None,
5467            kind,
5468            line,
5469        };
5470        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
5471        self.parse_stmt_postfix_modifier(stmt)
5472    }
5473
5474    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
5475        let mut decl = match self.advance() {
5476            (Token::ScalarVar(name), _) => VarDecl {
5477                sigil: Sigil::Scalar,
5478                name,
5479                initializer: None,
5480                frozen: false,
5481                type_annotation: None,
5482            },
5483            (Token::ArrayVar(name), _) => VarDecl {
5484                sigil: Sigil::Array,
5485                name,
5486                initializer: None,
5487                frozen: false,
5488                type_annotation: None,
5489            },
5490            (Token::HashVar(name), line) => {
5491                if !crate::compat_mode() {
5492                    self.check_hash_shadows_reserved(&name, line)?;
5493                }
5494                VarDecl {
5495                    sigil: Sigil::Hash,
5496                    name,
5497                    initializer: None,
5498                    frozen: false,
5499                    type_annotation: None,
5500                }
5501            }
5502            (Token::Star, _line) => {
5503                let name = match self.advance() {
5504                    (Token::Ident(n), _) => n,
5505                    (tok, l) => {
5506                        return Err(self
5507                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
5508                    }
5509                };
5510                VarDecl {
5511                    sigil: Sigil::Typeglob,
5512                    name,
5513                    initializer: None,
5514                    frozen: false,
5515                    type_annotation: None,
5516                }
5517            }
5518            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
5519            // slot in a list assignment. The interpreter treats `undef`-named
5520            // scalar decls as throwaway: declared into a unique sink so the
5521            // distribute-to-decls loop advances past the slot.
5522            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
5523                sigil: Sigil::Scalar,
5524                // Synthesize a name that user code cannot reference. Each
5525                // sink slot in a list-assign gets its own unique name so the
5526                // declarations don't collide.
5527                name: format!("__undef_sink_{}", self.pos),
5528                initializer: None,
5529                frozen: false,
5530                type_annotation: None,
5531            },
5532            (tok, line) => {
5533                return Err(self.syntax_err(
5534                    format!("Expected variable in declaration, got {:?}", tok),
5535                    line,
5536                ));
5537            }
5538        };
5539        if allow_type_annotation && self.eat(&Token::Colon) {
5540            let ty = self.parse_type_name()?;
5541            if decl.sigil != Sigil::Scalar {
5542                return Err(self.syntax_err(
5543                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
5544                    self.peek_line(),
5545                ));
5546            }
5547            decl.type_annotation = Some(ty);
5548        }
5549        Ok(decl)
5550    }
5551
5552    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
5553        match self.advance() {
5554            (Token::Ident(name), _) => match name.as_str() {
5555                "Int" => Ok(PerlTypeName::Int),
5556                "Str" => Ok(PerlTypeName::Str),
5557                "Float" => Ok(PerlTypeName::Float),
5558                "Bool" => Ok(PerlTypeName::Bool),
5559                "Array" => Ok(PerlTypeName::Array),
5560                "Hash" => Ok(PerlTypeName::Hash),
5561                "Ref" => Ok(PerlTypeName::Ref),
5562                "Any" => Ok(PerlTypeName::Any),
5563                _ => Ok(PerlTypeName::Struct(name)),
5564            },
5565            (tok, err_line) => Err(self.syntax_err(
5566                format!("Expected type name after `:`, got {:?}", tok),
5567                err_line,
5568            )),
5569        }
5570    }
5571
5572    fn parse_package(&mut self) -> PerlResult<Statement> {
5573        let line = self.peek_line();
5574        self.advance(); // 'package'
5575        let name = match self.advance() {
5576            (Token::Ident(n), _) => n,
5577            (tok, line) => {
5578                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
5579            }
5580        };
5581        // Handle Foo::Bar
5582        let mut full_name = name;
5583        while self.eat(&Token::PackageSep) {
5584            if let (Token::Ident(part), _) = self.advance() {
5585                full_name = format!("{}::{}", full_name, part);
5586            }
5587        }
5588        self.eat(&Token::Semicolon);
5589        Ok(Statement {
5590            label: None,
5591            kind: StmtKind::Package { name: full_name },
5592            line,
5593        })
5594    }
5595
5596    fn parse_use(&mut self) -> PerlResult<Statement> {
5597        let line = self.peek_line();
5598        self.advance(); // 'use'
5599        let (tok, tok_line) = self.advance();
5600        match tok {
5601            Token::Float(v) => {
5602                self.eat(&Token::Semicolon);
5603                Ok(Statement {
5604                    label: None,
5605                    kind: StmtKind::UsePerlVersion { version: v },
5606                    line,
5607                })
5608            }
5609            Token::Integer(n) => {
5610                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5611                    self.eat(&Token::Semicolon);
5612                    Ok(Statement {
5613                        label: None,
5614                        kind: StmtKind::UsePerlVersion { version: n as f64 },
5615                        line,
5616                    })
5617                } else {
5618                    Err(self.syntax_err(
5619                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
5620                        line,
5621                    ))
5622                }
5623            }
5624            Token::Ident(n) => {
5625                let mut full_name = n;
5626                while self.eat(&Token::PackageSep) {
5627                    if let (Token::Ident(part), _) = self.advance() {
5628                        full_name = format!("{}::{}", full_name, part);
5629                    }
5630                }
5631                if full_name == "overload" {
5632                    let mut pairs = Vec::new();
5633                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
5634                        loop {
5635                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
5636                            {
5637                                break;
5638                            }
5639                            let key_e = this.parse_assign_expr()?;
5640                            this.expect(&Token::FatArrow)?;
5641                            let val_e = this.parse_assign_expr()?;
5642                            let key = this.expr_to_overload_key(&key_e)?;
5643                            let val = this.expr_to_overload_sub(&val_e)?;
5644                            pairs.push((key, val));
5645                            if !this.eat(&Token::Comma) {
5646                                break;
5647                            }
5648                        }
5649                        Ok(())
5650                    };
5651                    if self.eat(&Token::LParen) {
5652                        // `use overload ();` — common in JSON::PP and other modules.
5653                        parse_overload_pairs(self)?;
5654                        self.expect(&Token::RParen)?;
5655                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
5656                        parse_overload_pairs(self)?;
5657                    }
5658                    self.eat(&Token::Semicolon);
5659                    return Ok(Statement {
5660                        label: None,
5661                        kind: StmtKind::UseOverload { pairs },
5662                        line,
5663                    });
5664                }
5665                let mut imports = Vec::new();
5666                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5667                    && !self.next_is_new_statement_start(tok_line)
5668                {
5669                    loop {
5670                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5671                            break;
5672                        }
5673                        imports.push(self.parse_expression()?);
5674                        if !self.eat(&Token::Comma) {
5675                            break;
5676                        }
5677                    }
5678                }
5679                self.eat(&Token::Semicolon);
5680                Ok(Statement {
5681                    label: None,
5682                    kind: StmtKind::Use {
5683                        module: full_name,
5684                        imports,
5685                    },
5686                    line,
5687                })
5688            }
5689            other => Err(self.syntax_err(
5690                format!("Expected module name or version after use, got {:?}", other),
5691                tok_line,
5692            )),
5693        }
5694    }
5695
5696    fn parse_no(&mut self) -> PerlResult<Statement> {
5697        let line = self.peek_line();
5698        self.advance(); // 'no'
5699        let module = match self.advance() {
5700            (Token::Ident(n), tok_line) => (n, tok_line),
5701            (tok, line) => {
5702                return Err(self.syntax_err(
5703                    format!("Expected module name after no, got {:?}", tok),
5704                    line,
5705                ))
5706            }
5707        };
5708        let (module_name, tok_line) = module;
5709        let mut full_name = module_name;
5710        while self.eat(&Token::PackageSep) {
5711            if let (Token::Ident(part), _) = self.advance() {
5712                full_name = format!("{}::{}", full_name, part);
5713            }
5714        }
5715        let mut imports = Vec::new();
5716        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5717            && !self.next_is_new_statement_start(tok_line)
5718        {
5719            loop {
5720                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5721                    break;
5722                }
5723                imports.push(self.parse_expression()?);
5724                if !self.eat(&Token::Comma) {
5725                    break;
5726                }
5727            }
5728        }
5729        self.eat(&Token::Semicolon);
5730        Ok(Statement {
5731            label: None,
5732            kind: StmtKind::No {
5733                module: full_name,
5734                imports,
5735            },
5736            line,
5737        })
5738    }
5739
5740    fn parse_return(&mut self) -> PerlResult<Statement> {
5741        let line = self.peek_line();
5742        self.advance(); // 'return'
5743                        // No-value return: terminator tokens AND any postfix statement-modifier
5744                        // keyword (`if`/`unless`/`while`/`until`/`for`/`foreach`). Without this
5745                        // the postfix-modifier check below never fires for valueless returns —
5746                        // `parse_assign_expr` would see `if` and look it up as a sub call,
5747                        // producing the misleading "Undefined subroutine &if" error.
5748        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
5749            || self.peek_is_postfix_stmt_modifier_keyword()
5750        {
5751            None
5752        } else {
5753            // Parse the operand as a comma-list — Perl's `return` is a
5754            // list-operator, so `return 1, 2, 3` returns the list (1, 2, 3).
5755            // (BUG-010) Stay below pipe-forward and stop at postfix
5756            // statement-modifier keywords like `if` / `unless`.
5757            let first = self.parse_assign_expr()?;
5758            if matches!(self.peek(), Token::Comma | Token::FatArrow) {
5759                let mut items = vec![first];
5760                while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5761                    if matches!(
5762                        self.peek(),
5763                        Token::Semicolon | Token::RBrace | Token::Eof
5764                    ) || self.peek_is_postfix_stmt_modifier_keyword()
5765                    {
5766                        break;
5767                    }
5768                    items.push(self.parse_assign_expr()?);
5769                }
5770                let line = items.first().map(|e| e.line).unwrap_or(line);
5771                Some(Expr {
5772                    kind: ExprKind::List(items),
5773                    line,
5774                })
5775            } else {
5776                Some(first)
5777            }
5778        };
5779        // Check for postfix modifiers on return
5780        let stmt = Statement {
5781            label: None,
5782            kind: StmtKind::Return(val),
5783            line,
5784        };
5785        if let Token::Ident(ref kw) = self.peek().clone() {
5786            match kw.as_str() {
5787                "if" => {
5788                    self.advance();
5789                    let cond = self.parse_expression()?;
5790                    self.eat(&Token::Semicolon);
5791                    return Ok(Statement {
5792                        label: None,
5793                        kind: StmtKind::If {
5794                            condition: cond,
5795                            body: vec![stmt],
5796                            elsifs: vec![],
5797                            else_block: None,
5798                        },
5799                        line,
5800                    });
5801                }
5802                "unless" => {
5803                    self.advance();
5804                    let cond = self.parse_expression()?;
5805                    self.eat(&Token::Semicolon);
5806                    return Ok(Statement {
5807                        label: None,
5808                        kind: StmtKind::Unless {
5809                            condition: cond,
5810                            body: vec![stmt],
5811                            else_block: None,
5812                        },
5813                        line,
5814                    });
5815                }
5816                _ => {}
5817            }
5818        }
5819        self.eat(&Token::Semicolon);
5820        Ok(stmt)
5821    }
5822
5823    // ── Expressions (Pratt / precedence climbing) ──
5824
5825    fn parse_expression(&mut self) -> PerlResult<Expr> {
5826        self.parse_comma_expr()
5827    }
5828
5829    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
5830        let expr = self.parse_assign_expr()?;
5831        let mut exprs = vec![expr];
5832        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5833            if matches!(
5834                self.peek(),
5835                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
5836            ) {
5837                break; // trailing comma
5838            }
5839            exprs.push(self.parse_assign_expr()?);
5840        }
5841        if exprs.len() == 1 {
5842            return Ok(exprs.pop().unwrap());
5843        }
5844        let line = exprs[0].line;
5845        Ok(Expr {
5846            kind: ExprKind::List(exprs),
5847            line,
5848        })
5849    }
5850
5851    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
5852        let expr = self.parse_ternary()?;
5853        let line = expr.line;
5854
5855        match self.peek().clone() {
5856            Token::Assign => {
5857                self.advance();
5858                let right = self.parse_assign_expr()?;
5859                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
5860                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
5861                    if args.is_empty() {
5862                        // Destructure again to take ownership
5863                        let ExprKind::MethodCall {
5864                            object,
5865                            method,
5866                            super_call,
5867                            ..
5868                        } = expr.kind
5869                        else {
5870                            unreachable!()
5871                        };
5872                        return Ok(Expr {
5873                            kind: ExprKind::MethodCall {
5874                                object,
5875                                method,
5876                                args: vec![right],
5877                                super_call,
5878                            },
5879                            line,
5880                        });
5881                    }
5882                }
5883                self.validate_assignment(&expr, &right, line)?;
5884                Ok(Expr {
5885                    kind: ExprKind::Assign {
5886                        target: Box::new(expr),
5887                        value: Box::new(right),
5888                    },
5889                    line,
5890                })
5891            }
5892            Token::PlusAssign => {
5893                self.advance();
5894                let r = self.parse_assign_expr()?;
5895                Ok(Expr {
5896                    kind: ExprKind::CompoundAssign {
5897                        target: Box::new(expr),
5898                        op: BinOp::Add,
5899                        value: Box::new(r),
5900                    },
5901                    line,
5902                })
5903            }
5904            Token::MinusAssign => {
5905                self.advance();
5906                let r = self.parse_assign_expr()?;
5907                Ok(Expr {
5908                    kind: ExprKind::CompoundAssign {
5909                        target: Box::new(expr),
5910                        op: BinOp::Sub,
5911                        value: Box::new(r),
5912                    },
5913                    line,
5914                })
5915            }
5916            Token::MulAssign => {
5917                self.advance();
5918                let r = self.parse_assign_expr()?;
5919                Ok(Expr {
5920                    kind: ExprKind::CompoundAssign {
5921                        target: Box::new(expr),
5922                        op: BinOp::Mul,
5923                        value: Box::new(r),
5924                    },
5925                    line,
5926                })
5927            }
5928            Token::DivAssign => {
5929                self.advance();
5930                let r = self.parse_assign_expr()?;
5931                Ok(Expr {
5932                    kind: ExprKind::CompoundAssign {
5933                        target: Box::new(expr),
5934                        op: BinOp::Div,
5935                        value: Box::new(r),
5936                    },
5937                    line,
5938                })
5939            }
5940            Token::ModAssign => {
5941                self.advance();
5942                let r = self.parse_assign_expr()?;
5943                Ok(Expr {
5944                    kind: ExprKind::CompoundAssign {
5945                        target: Box::new(expr),
5946                        op: BinOp::Mod,
5947                        value: Box::new(r),
5948                    },
5949                    line,
5950                })
5951            }
5952            Token::PowAssign => {
5953                self.advance();
5954                let r = self.parse_assign_expr()?;
5955                Ok(Expr {
5956                    kind: ExprKind::CompoundAssign {
5957                        target: Box::new(expr),
5958                        op: BinOp::Pow,
5959                        value: Box::new(r),
5960                    },
5961                    line,
5962                })
5963            }
5964            Token::DotAssign => {
5965                self.advance();
5966                let r = self.parse_assign_expr()?;
5967                Ok(Expr {
5968                    kind: ExprKind::CompoundAssign {
5969                        target: Box::new(expr),
5970                        op: BinOp::Concat,
5971                        value: Box::new(r),
5972                    },
5973                    line,
5974                })
5975            }
5976            Token::BitAndAssign => {
5977                self.advance();
5978                let r = self.parse_assign_expr()?;
5979                Ok(Expr {
5980                    kind: ExprKind::CompoundAssign {
5981                        target: Box::new(expr),
5982                        op: BinOp::BitAnd,
5983                        value: Box::new(r),
5984                    },
5985                    line,
5986                })
5987            }
5988            Token::BitOrAssign => {
5989                self.advance();
5990                let r = self.parse_assign_expr()?;
5991                Ok(Expr {
5992                    kind: ExprKind::CompoundAssign {
5993                        target: Box::new(expr),
5994                        op: BinOp::BitOr,
5995                        value: Box::new(r),
5996                    },
5997                    line,
5998                })
5999            }
6000            Token::XorAssign => {
6001                self.advance();
6002                let r = self.parse_assign_expr()?;
6003                Ok(Expr {
6004                    kind: ExprKind::CompoundAssign {
6005                        target: Box::new(expr),
6006                        op: BinOp::BitXor,
6007                        value: Box::new(r),
6008                    },
6009                    line,
6010                })
6011            }
6012            Token::ShiftLeftAssign => {
6013                self.advance();
6014                let r = self.parse_assign_expr()?;
6015                Ok(Expr {
6016                    kind: ExprKind::CompoundAssign {
6017                        target: Box::new(expr),
6018                        op: BinOp::ShiftLeft,
6019                        value: Box::new(r),
6020                    },
6021                    line,
6022                })
6023            }
6024            Token::ShiftRightAssign => {
6025                self.advance();
6026                let r = self.parse_assign_expr()?;
6027                Ok(Expr {
6028                    kind: ExprKind::CompoundAssign {
6029                        target: Box::new(expr),
6030                        op: BinOp::ShiftRight,
6031                        value: Box::new(r),
6032                    },
6033                    line,
6034                })
6035            }
6036            Token::OrAssign => {
6037                self.advance();
6038                let r = self.parse_assign_expr()?;
6039                Ok(Expr {
6040                    kind: ExprKind::CompoundAssign {
6041                        target: Box::new(expr),
6042                        op: BinOp::LogOr,
6043                        value: Box::new(r),
6044                    },
6045                    line,
6046                })
6047            }
6048            Token::DefinedOrAssign => {
6049                self.advance();
6050                let r = self.parse_assign_expr()?;
6051                Ok(Expr {
6052                    kind: ExprKind::CompoundAssign {
6053                        target: Box::new(expr),
6054                        op: BinOp::DefinedOr,
6055                        value: Box::new(r),
6056                    },
6057                    line,
6058                })
6059            }
6060            Token::AndAssign => {
6061                self.advance();
6062                let r = self.parse_assign_expr()?;
6063                Ok(Expr {
6064                    kind: ExprKind::CompoundAssign {
6065                        target: Box::new(expr),
6066                        op: BinOp::LogAnd,
6067                        value: Box::new(r),
6068                    },
6069                    line,
6070                })
6071            }
6072            _ => Ok(expr),
6073        }
6074    }
6075
6076    fn parse_ternary(&mut self) -> PerlResult<Expr> {
6077        let expr = self.parse_pipe_forward()?;
6078        if self.eat(&Token::Question) {
6079            let line = expr.line;
6080            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
6081            let then_expr = self.parse_assign_expr();
6082            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
6083            let then_expr = then_expr?;
6084            self.expect(&Token::Colon)?;
6085            let else_expr = self.parse_assign_expr()?;
6086            return Ok(Expr {
6087                kind: ExprKind::Ternary {
6088                    condition: Box::new(expr),
6089                    then_expr: Box::new(then_expr),
6090                    else_expr: Box::new(else_expr),
6091                },
6092                line,
6093            });
6094        }
6095        Ok(expr)
6096    }
6097
6098    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
6099    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
6100    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
6101    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
6102    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
6103    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
6104        let mut left = self.parse_or_word()?;
6105        // Inside a paren-less arg list, `|>` is a hard terminator for the
6106        // enclosing call — leave it for the outer `parse_pipe_forward` loop
6107        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
6108        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
6109        // outer `|>` via its first-arg `parse_assign_expr`.
6110        if self.no_pipe_forward_depth > 0 {
6111            return Ok(left);
6112        }
6113        while matches!(self.peek(), Token::PipeForward) {
6114            if crate::compat_mode() {
6115                return Err(self.syntax_err(
6116                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
6117                    left.line,
6118                ));
6119            }
6120            let line = left.line;
6121            self.advance();
6122            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
6123            // `join`, …) accept a placeholder in place of their list operand.
6124            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
6125            let right_result = self.parse_or_word();
6126            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
6127            let right = right_result?;
6128            left = self.pipe_forward_apply(left, right, line)?;
6129        }
6130        Ok(left)
6131    }
6132
6133    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
6134    /// its **first** argument (Elixir / R / proposed-JS convention).
6135    ///
6136    /// The strategy depends on the shape of `rhs`:
6137    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
6138    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
6139    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
6140    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
6141    ///   matching the `(data, filter)` signature the builtin expects.
6142    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
6143    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
6144    ///   `lhs` (these parse a single default `$_` when called without an arg, so
6145    ///   piping overrides that default; first-arg and last-arg are identical).
6146    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
6147    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
6148    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
6149    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
6150    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
6151    ///   as the sole argument.
6152    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
6153    ///   since silently calling a non-callable at runtime would be worse.
6154    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
6155        let Expr { kind, line: rline } = rhs;
6156        let new_kind = match kind {
6157            // ── Generic / user-defined calls ───────────────────────────────────
6158            ExprKind::FuncCall { name, mut args } => {
6159                // Stryke builtins are unprefixed; `CORE::` callers route back to the
6160                // bare-name pipe-forward dispatch below.
6161                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
6162                match dispatch_name {
6163                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
6164                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
6165                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
6166                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "letters"
6167                    | "letters_uc" | "letters_lc" | "punctuation" | "numbers" | "graphemes"
6168                    | "columns" | "sentences" | "paragraphs" | "sections" | "trim" | "avg"
6169                    | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml" | "to_html"
6170                    | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
6171                    | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline" | "bar_chart"
6172                    | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed" | "cb"
6173                    | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
6174                        if args.is_empty() {
6175                            args.push(lhs);
6176                        } else {
6177                            args[0] = lhs;
6178                        }
6179                    }
6180                    "chunked" | "windowed" => {
6181                        if args.is_empty() {
6182                            return Err(self.syntax_err(
6183                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
6184                                line,
6185                            ));
6186                        }
6187                        args.insert(0, lhs);
6188                    }
6189                    "reduce" | "fold" => {
6190                        args.push(lhs);
6191                    }
6192                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
6193                        // data |> grep_v "pattern" → grep_v("pattern", data...)
6194                        // data |> pluck "key" → pluck("key", data...)
6195                        // data |> tee "file" → tee("file", data...)
6196                        // data |> nth N → nth(N, data...)
6197                        // data |> chunk N → chunk(N, data...)
6198                        args.push(lhs);
6199                    }
6200                    "enumerate" | "dedup" => {
6201                        // data |> enumerate → enumerate(data)
6202                        // data |> dedup → dedup(data)
6203                        args.insert(0, lhs);
6204                    }
6205                    "clamp" => {
6206                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
6207                        args.push(lhs);
6208                    }
6209                    n if Self::is_block_then_list_pipe_builtin(n) => {
6210                        if args.len() < 2 {
6211                            return Err(self.syntax_err(
6212                                format!(
6213                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
6214                                ),
6215                                line,
6216                            ));
6217                        }
6218                        args[1] = lhs;
6219                    }
6220                    "take" | "head" | "tail" | "drop" => {
6221                        if args.is_empty() {
6222                            return Err(self.syntax_err(
6223                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
6224                                line,
6225                            ));
6226                        }
6227                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
6228                        args.insert(0, lhs);
6229                    }
6230                    _ => {
6231                        if self.thread_last_mode {
6232                            args.push(lhs);
6233                        } else {
6234                            args.insert(0, lhs);
6235                        }
6236                    }
6237                }
6238                ExprKind::FuncCall { name, args }
6239            }
6240            ExprKind::MethodCall {
6241                object,
6242                method,
6243                mut args,
6244                super_call,
6245            } => {
6246                if self.thread_last_mode {
6247                    args.push(lhs);
6248                } else {
6249                    args.insert(0, lhs);
6250                }
6251                ExprKind::MethodCall {
6252                    object,
6253                    method,
6254                    args,
6255                    super_call,
6256                }
6257            }
6258            ExprKind::IndirectCall {
6259                target,
6260                mut args,
6261                ampersand,
6262                pass_caller_arglist: _,
6263            } => {
6264                if self.thread_last_mode {
6265                    args.push(lhs);
6266                } else {
6267                    args.insert(0, lhs);
6268                }
6269                ExprKind::IndirectCall {
6270                    target,
6271                    args,
6272                    ampersand,
6273                    // Prepending an explicit first arg means this is no longer
6274                    // "pass the caller's @_" — that form is only bare `&$cr`.
6275                    pass_caller_arglist: false,
6276                }
6277            }
6278
6279            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
6280            ExprKind::Print { handle, mut args } => {
6281                if self.thread_last_mode {
6282                    args.push(lhs);
6283                } else {
6284                    args.insert(0, lhs);
6285                }
6286                ExprKind::Print { handle, args }
6287            }
6288            ExprKind::Say { handle, mut args } => {
6289                if self.thread_last_mode {
6290                    args.push(lhs);
6291                } else {
6292                    args.insert(0, lhs);
6293                }
6294                ExprKind::Say { handle, args }
6295            }
6296            ExprKind::Printf { handle, mut args } => {
6297                if self.thread_last_mode {
6298                    args.push(lhs);
6299                } else {
6300                    args.insert(0, lhs);
6301                }
6302                ExprKind::Printf { handle, args }
6303            }
6304            ExprKind::Die(mut args) => {
6305                if self.thread_last_mode {
6306                    args.push(lhs);
6307                } else {
6308                    args.insert(0, lhs);
6309                }
6310                ExprKind::Die(args)
6311            }
6312            ExprKind::Warn(mut args) => {
6313                if self.thread_last_mode {
6314                    args.push(lhs);
6315                } else {
6316                    args.insert(0, lhs);
6317                }
6318                ExprKind::Warn(args)
6319            }
6320
6321            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
6322            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
6323            //   but piping the format string is the rarer case. Prepending
6324            //   to the values list gives `sprintf(format, lhs, ...args)` for
6325            //   the common `$n |> sprintf "count=%d"` case.
6326            ExprKind::Sprintf { format, mut args } => {
6327                if self.thread_last_mode {
6328                    args.push(lhs);
6329                } else {
6330                    args.insert(0, lhs);
6331                }
6332                ExprKind::Sprintf { format, args }
6333            }
6334
6335            // ── System / exec / globbing / filesystem variadics ────────────────
6336            ExprKind::System(mut args) => {
6337                if self.thread_last_mode {
6338                    args.push(lhs);
6339                } else {
6340                    args.insert(0, lhs);
6341                }
6342                ExprKind::System(args)
6343            }
6344            ExprKind::Exec(mut args) => {
6345                if self.thread_last_mode {
6346                    args.push(lhs);
6347                } else {
6348                    args.insert(0, lhs);
6349                }
6350                ExprKind::Exec(args)
6351            }
6352            ExprKind::Unlink(mut args) => {
6353                if self.thread_last_mode {
6354                    args.push(lhs);
6355                } else {
6356                    args.insert(0, lhs);
6357                }
6358                ExprKind::Unlink(args)
6359            }
6360            ExprKind::Chmod(mut args) => {
6361                if self.thread_last_mode {
6362                    args.push(lhs);
6363                } else {
6364                    args.insert(0, lhs);
6365                }
6366                ExprKind::Chmod(args)
6367            }
6368            ExprKind::Chown(mut args) => {
6369                if self.thread_last_mode {
6370                    args.push(lhs);
6371                } else {
6372                    args.insert(0, lhs);
6373                }
6374                ExprKind::Chown(args)
6375            }
6376            ExprKind::Glob(mut args) => {
6377                if self.thread_last_mode {
6378                    args.push(lhs);
6379                } else {
6380                    args.insert(0, lhs);
6381                }
6382                ExprKind::Glob(args)
6383            }
6384            ExprKind::Files(mut args) => {
6385                if self.thread_last_mode {
6386                    args.push(lhs);
6387                } else {
6388                    args.insert(0, lhs);
6389                }
6390                ExprKind::Files(args)
6391            }
6392            ExprKind::Filesf(mut args) => {
6393                if self.thread_last_mode {
6394                    args.push(lhs);
6395                } else {
6396                    args.insert(0, lhs);
6397                }
6398                ExprKind::Filesf(args)
6399            }
6400            ExprKind::FilesfRecursive(mut args) => {
6401                if self.thread_last_mode {
6402                    args.push(lhs);
6403                } else {
6404                    args.insert(0, lhs);
6405                }
6406                ExprKind::FilesfRecursive(args)
6407            }
6408            ExprKind::Dirs(mut args) => {
6409                if self.thread_last_mode {
6410                    args.push(lhs);
6411                } else {
6412                    args.insert(0, lhs);
6413                }
6414                ExprKind::Dirs(args)
6415            }
6416            ExprKind::DirsRecursive(mut args) => {
6417                if self.thread_last_mode {
6418                    args.push(lhs);
6419                } else {
6420                    args.insert(0, lhs);
6421                }
6422                ExprKind::DirsRecursive(args)
6423            }
6424            ExprKind::SymLinks(mut args) => {
6425                if self.thread_last_mode {
6426                    args.push(lhs);
6427                } else {
6428                    args.insert(0, lhs);
6429                }
6430                ExprKind::SymLinks(args)
6431            }
6432            ExprKind::Sockets(mut args) => {
6433                if self.thread_last_mode {
6434                    args.push(lhs);
6435                } else {
6436                    args.insert(0, lhs);
6437                }
6438                ExprKind::Sockets(args)
6439            }
6440            ExprKind::Pipes(mut args) => {
6441                if self.thread_last_mode {
6442                    args.push(lhs);
6443                } else {
6444                    args.insert(0, lhs);
6445                }
6446                ExprKind::Pipes(args)
6447            }
6448            ExprKind::BlockDevices(mut args) => {
6449                if self.thread_last_mode {
6450                    args.push(lhs);
6451                } else {
6452                    args.insert(0, lhs);
6453                }
6454                ExprKind::BlockDevices(args)
6455            }
6456            ExprKind::CharDevices(mut args) => {
6457                if self.thread_last_mode {
6458                    args.push(lhs);
6459                } else {
6460                    args.insert(0, lhs);
6461                }
6462                ExprKind::CharDevices(args)
6463            }
6464            ExprKind::GlobPar { mut args, progress } => {
6465                if self.thread_last_mode {
6466                    args.push(lhs);
6467                } else {
6468                    args.insert(0, lhs);
6469                }
6470                ExprKind::GlobPar { args, progress }
6471            }
6472            ExprKind::ParSed { mut args, progress } => {
6473                if self.thread_last_mode {
6474                    args.push(lhs);
6475                } else {
6476                    args.insert(0, lhs);
6477                }
6478                ExprKind::ParSed { args, progress }
6479            }
6480
6481            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
6482            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
6483            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
6484            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
6485            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
6486            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
6487            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
6488            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
6489            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
6490            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
6491            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
6492            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
6493            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
6494            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
6495            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
6496            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
6497            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
6498            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
6499            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
6500            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
6501            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
6502            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
6503            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
6504            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
6505            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
6506            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
6507            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
6508            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
6509            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
6510            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
6511            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
6512            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
6513            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
6514            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
6515            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
6516            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
6517            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
6518            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
6519            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
6520            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
6521            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
6522            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
6523            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
6524            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
6525            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
6526            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
6527            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
6528            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
6529            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
6530            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
6531            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
6532            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
6533
6534            // ── Higher-order / list-taking forms: replace the `list` slot ──────
6535            ExprKind::MapExpr {
6536                block,
6537                list: _,
6538                flatten_array_refs,
6539                stream,
6540            } => ExprKind::MapExpr {
6541                block,
6542                list: Box::new(lhs),
6543                flatten_array_refs,
6544                stream,
6545            },
6546            ExprKind::MapExprComma {
6547                expr,
6548                list: _,
6549                flatten_array_refs,
6550                stream,
6551            } => ExprKind::MapExprComma {
6552                expr,
6553                list: Box::new(lhs),
6554                flatten_array_refs,
6555                stream,
6556            },
6557            ExprKind::GrepExpr {
6558                block,
6559                list: _,
6560                keyword,
6561            } => ExprKind::GrepExpr {
6562                block,
6563                list: Box::new(lhs),
6564                keyword,
6565            },
6566            ExprKind::GrepExprComma {
6567                expr,
6568                list: _,
6569                keyword,
6570            } => ExprKind::GrepExprComma {
6571                expr,
6572                list: Box::new(lhs),
6573                keyword,
6574            },
6575            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
6576                block,
6577                list: Box::new(lhs),
6578            },
6579            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
6580                cmp,
6581                list: Box::new(lhs),
6582            },
6583            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
6584                separator,
6585                list: Box::new(lhs),
6586            },
6587            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
6588                block,
6589                list: Box::new(lhs),
6590            },
6591            ExprKind::PMapExpr {
6592                block,
6593                list: _,
6594                progress,
6595                flat_outputs,
6596                on_cluster,
6597                stream,
6598            } => ExprKind::PMapExpr {
6599                block,
6600                list: Box::new(lhs),
6601                progress,
6602                flat_outputs,
6603                on_cluster,
6604                stream,
6605            },
6606            ExprKind::PMapChunkedExpr {
6607                chunk_size,
6608                block,
6609                list: _,
6610                progress,
6611            } => ExprKind::PMapChunkedExpr {
6612                chunk_size,
6613                block,
6614                list: Box::new(lhs),
6615                progress,
6616            },
6617            ExprKind::PGrepExpr {
6618                block,
6619                list: _,
6620                progress,
6621                stream,
6622            } => ExprKind::PGrepExpr {
6623                block,
6624                list: Box::new(lhs),
6625                progress,
6626                stream,
6627            },
6628            ExprKind::PForExpr {
6629                block,
6630                list: _,
6631                progress,
6632            } => ExprKind::PForExpr {
6633                block,
6634                list: Box::new(lhs),
6635                progress,
6636            },
6637            ExprKind::PSortExpr {
6638                cmp,
6639                list: _,
6640                progress,
6641            } => ExprKind::PSortExpr {
6642                cmp,
6643                list: Box::new(lhs),
6644                progress,
6645            },
6646            ExprKind::PReduceExpr {
6647                block,
6648                list: _,
6649                progress,
6650            } => ExprKind::PReduceExpr {
6651                block,
6652                list: Box::new(lhs),
6653                progress,
6654            },
6655            ExprKind::PcacheExpr {
6656                block,
6657                list: _,
6658                progress,
6659            } => ExprKind::PcacheExpr {
6660                block,
6661                list: Box::new(lhs),
6662                progress,
6663            },
6664            ExprKind::PReduceInitExpr {
6665                init,
6666                block,
6667                list: _,
6668                progress,
6669            } => ExprKind::PReduceInitExpr {
6670                init,
6671                block,
6672                list: Box::new(lhs),
6673                progress,
6674            },
6675            ExprKind::PMapReduceExpr {
6676                map_block,
6677                reduce_block,
6678                list: _,
6679                progress,
6680            } => ExprKind::PMapReduceExpr {
6681                map_block,
6682                reduce_block,
6683                list: Box::new(lhs),
6684                progress,
6685            },
6686
6687            // ── Push / unshift: first arg is the array, so pipe the LHS
6688            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
6689            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
6690            //     directly for that.
6691            ExprKind::Push { array, mut values } => {
6692                values.insert(0, lhs);
6693                ExprKind::Push { array, values }
6694            }
6695            ExprKind::Unshift { array, mut values } => {
6696                values.insert(0, lhs);
6697                ExprKind::Unshift { array, values }
6698            }
6699
6700            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
6701            ExprKind::SplitExpr {
6702                pattern,
6703                string: _,
6704                limit,
6705            } => ExprKind::SplitExpr {
6706                pattern,
6707                string: Box::new(lhs),
6708                limit,
6709            },
6710
6711            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
6712            //    Auto-inject `r` flag so the substitution returns the modified
6713            //    string instead of the match count (non-destructive / Perl /r).
6714            ExprKind::Substitution {
6715                pattern,
6716                replacement,
6717                mut flags,
6718                expr: _,
6719                delim,
6720            } => {
6721                if !flags.contains('r') {
6722                    flags.push('r');
6723                }
6724                ExprKind::Substitution {
6725                    expr: Box::new(lhs),
6726                    pattern,
6727                    replacement,
6728                    flags,
6729                    delim,
6730                }
6731            }
6732            ExprKind::Transliterate {
6733                from,
6734                to,
6735                mut flags,
6736                expr: _,
6737                delim,
6738            } => {
6739                if !flags.contains('r') {
6740                    flags.push('r');
6741                }
6742                ExprKind::Transliterate {
6743                    expr: Box::new(lhs),
6744                    from,
6745                    to,
6746                    flags,
6747                    delim,
6748                }
6749            }
6750            ExprKind::Match {
6751                pattern,
6752                flags,
6753                scalar_g,
6754                expr: _,
6755                delim,
6756            } => ExprKind::Match {
6757                expr: Box::new(lhs),
6758                pattern,
6759                flags,
6760                scalar_g,
6761                delim,
6762            },
6763            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
6764            ExprKind::Regex(pattern, flags) => ExprKind::Match {
6765                expr: Box::new(lhs),
6766                pattern,
6767                flags,
6768                scalar_g: false,
6769                delim: '/',
6770            },
6771
6772            // ── Bareword function name → plain unary call ──────────────────────
6773            ExprKind::Bareword(name) => match name.as_str() {
6774                "reverse" => {
6775                    if crate::no_interop_mode() {
6776                        return Err(self.syntax_err(
6777                            "stryke uses `rev` instead of `reverse` (--no-interop)",
6778                            line,
6779                        ));
6780                    }
6781                    ExprKind::ReverseExpr(Box::new(lhs))
6782                }
6783                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
6784                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
6785                    name: "uniq".to_string(),
6786                    args: vec![lhs],
6787                },
6788                "fl" | "flatten" => ExprKind::FuncCall {
6789                    name: "flatten".to_string(),
6790                    args: vec![lhs],
6791                },
6792                _ => ExprKind::FuncCall {
6793                    name,
6794                    args: vec![lhs],
6795                },
6796            },
6797
6798            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
6799            kind @ (ExprKind::ScalarVar(_)
6800            | ExprKind::ArrayElement { .. }
6801            | ExprKind::HashElement { .. }
6802            | ExprKind::Deref { .. }
6803            | ExprKind::ArrowDeref { .. }
6804            | ExprKind::CodeRef { .. }
6805            | ExprKind::SubroutineRef(_)
6806            | ExprKind::SubroutineCodeRef(_)
6807            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
6808                target: Box::new(Expr { kind, line: rline }),
6809                args: vec![lhs],
6810                ampersand: false,
6811                pass_caller_arglist: false,
6812            },
6813
6814            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
6815            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
6816            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
6817            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
6818                ExprKind::IndirectCall {
6819                    target: inner,
6820                    args: vec![lhs],
6821                    ampersand: false,
6822                    pass_caller_arglist: false,
6823                }
6824            }
6825
6826            other => {
6827                return Err(self.syntax_err(
6828                    format!(
6829                        "right-hand side of `|>` must be a call, builtin, or coderef \
6830                         expression (got {})",
6831                        Self::expr_kind_name(&other)
6832                    ),
6833                    line,
6834                ));
6835            }
6836        };
6837        Ok(Expr {
6838            kind: new_kind,
6839            line,
6840        })
6841    }
6842
6843    /// Short label for an `ExprKind` (used in `|>` error messages).
6844    fn expr_kind_name(kind: &ExprKind) -> &'static str {
6845        match kind {
6846            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
6847            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
6848            ExprKind::BinOp { .. } => "binary expression",
6849            ExprKind::UnaryOp { .. } => "unary expression",
6850            ExprKind::Ternary { .. } => "ternary expression",
6851            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
6852            ExprKind::List(_) => "list expression",
6853            ExprKind::Range { .. } => "range expression",
6854            _ => "expression",
6855        }
6856    }
6857
6858    // or / not (lowest precedence word operators)
6859    fn parse_or_word(&mut self) -> PerlResult<Expr> {
6860        let mut left = self.parse_and_word()?;
6861        while matches!(self.peek(), Token::LogOrWord) {
6862            let line = left.line;
6863            self.advance();
6864            let right = self.parse_and_word()?;
6865            left = Expr {
6866                kind: ExprKind::BinOp {
6867                    left: Box::new(left),
6868                    op: BinOp::LogOrWord,
6869                    right: Box::new(right),
6870                },
6871                line,
6872            };
6873        }
6874        Ok(left)
6875    }
6876
6877    fn parse_and_word(&mut self) -> PerlResult<Expr> {
6878        let mut left = self.parse_not_word()?;
6879        while matches!(self.peek(), Token::LogAndWord) {
6880            let line = left.line;
6881            self.advance();
6882            let right = self.parse_not_word()?;
6883            left = Expr {
6884                kind: ExprKind::BinOp {
6885                    left: Box::new(left),
6886                    op: BinOp::LogAndWord,
6887                    right: Box::new(right),
6888                },
6889                line,
6890            };
6891        }
6892        Ok(left)
6893    }
6894
6895    fn parse_not_word(&mut self) -> PerlResult<Expr> {
6896        if matches!(self.peek(), Token::LogNotWord) {
6897            let line = self.peek_line();
6898            self.advance();
6899            let expr = self.parse_not_word()?;
6900            return Ok(Expr {
6901                kind: ExprKind::UnaryOp {
6902                    op: UnaryOp::LogNotWord,
6903                    expr: Box::new(expr),
6904                },
6905                line,
6906            });
6907        }
6908        self.parse_range()
6909    }
6910
6911    fn parse_log_or(&mut self) -> PerlResult<Expr> {
6912        let mut left = self.parse_log_and()?;
6913        loop {
6914            let op = match self.peek() {
6915                Token::LogOr => BinOp::LogOr,
6916                Token::DefinedOr => BinOp::DefinedOr,
6917                _ => break,
6918            };
6919            let line = left.line;
6920            self.advance();
6921            let right = self.parse_log_and()?;
6922            left = Expr {
6923                kind: ExprKind::BinOp {
6924                    left: Box::new(left),
6925                    op,
6926                    right: Box::new(right),
6927                },
6928                line,
6929            };
6930        }
6931        Ok(left)
6932    }
6933
6934    fn parse_log_and(&mut self) -> PerlResult<Expr> {
6935        let mut left = self.parse_bit_or()?;
6936        while matches!(self.peek(), Token::LogAnd) {
6937            let line = left.line;
6938            self.advance();
6939            let right = self.parse_bit_or()?;
6940            left = Expr {
6941                kind: ExprKind::BinOp {
6942                    left: Box::new(left),
6943                    op: BinOp::LogAnd,
6944                    right: Box::new(right),
6945                },
6946                line,
6947            };
6948        }
6949        Ok(left)
6950    }
6951
6952    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
6953        let mut left = self.parse_bit_xor()?;
6954        while matches!(self.peek(), Token::BitOr) {
6955            let line = left.line;
6956            self.advance();
6957            let right = self.parse_bit_xor()?;
6958            left = Expr {
6959                kind: ExprKind::BinOp {
6960                    left: Box::new(left),
6961                    op: BinOp::BitOr,
6962                    right: Box::new(right),
6963                },
6964                line,
6965            };
6966        }
6967        Ok(left)
6968    }
6969
6970    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
6971        let mut left = self.parse_bit_and()?;
6972        while matches!(self.peek(), Token::BitXor) {
6973            let line = left.line;
6974            self.advance();
6975            let right = self.parse_bit_and()?;
6976            left = Expr {
6977                kind: ExprKind::BinOp {
6978                    left: Box::new(left),
6979                    op: BinOp::BitXor,
6980                    right: Box::new(right),
6981                },
6982                line,
6983            };
6984        }
6985        Ok(left)
6986    }
6987
6988    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
6989        let mut left = self.parse_equality()?;
6990        while matches!(self.peek(), Token::BitAnd) {
6991            let line = left.line;
6992            self.advance();
6993            let right = self.parse_equality()?;
6994            left = Expr {
6995                kind: ExprKind::BinOp {
6996                    left: Box::new(left),
6997                    op: BinOp::BitAnd,
6998                    right: Box::new(right),
6999                },
7000                line,
7001            };
7002        }
7003        Ok(left)
7004    }
7005
7006    fn parse_equality(&mut self) -> PerlResult<Expr> {
7007        let mut left = self.parse_comparison()?;
7008        loop {
7009            let op = match self.peek() {
7010                Token::NumEq => BinOp::NumEq,
7011                Token::NumNe => BinOp::NumNe,
7012                Token::StrEq => BinOp::StrEq,
7013                Token::StrNe => BinOp::StrNe,
7014                Token::Spaceship => BinOp::Spaceship,
7015                Token::StrCmp => BinOp::StrCmp,
7016                _ => break,
7017            };
7018            let line = left.line;
7019            self.advance();
7020            let right = self.parse_comparison()?;
7021            left = Expr {
7022                kind: ExprKind::BinOp {
7023                    left: Box::new(left),
7024                    op,
7025                    right: Box::new(right),
7026                },
7027                line,
7028            };
7029        }
7030        Ok(left)
7031    }
7032
7033    fn parse_comparison(&mut self) -> PerlResult<Expr> {
7034        let left = self.parse_shift()?;
7035        let first_op = match self.peek() {
7036            Token::NumLt => BinOp::NumLt,
7037            Token::NumGt => BinOp::NumGt,
7038            Token::NumLe => BinOp::NumLe,
7039            Token::NumGe => BinOp::NumGe,
7040            Token::StrLt => BinOp::StrLt,
7041            Token::StrGt => BinOp::StrGt,
7042            Token::StrLe => BinOp::StrLe,
7043            Token::StrGe => BinOp::StrGe,
7044            _ => return Ok(left),
7045        };
7046        let line = left.line;
7047        self.advance();
7048        let middle = self.parse_shift()?;
7049
7050        let second_op = match self.peek() {
7051            Token::NumLt => Some(BinOp::NumLt),
7052            Token::NumGt => Some(BinOp::NumGt),
7053            Token::NumLe => Some(BinOp::NumLe),
7054            Token::NumGe => Some(BinOp::NumGe),
7055            Token::StrLt => Some(BinOp::StrLt),
7056            Token::StrGt => Some(BinOp::StrGt),
7057            Token::StrLe => Some(BinOp::StrLe),
7058            Token::StrGe => Some(BinOp::StrGe),
7059            _ => None,
7060        };
7061
7062        if second_op.is_none() {
7063            return Ok(Expr {
7064                kind: ExprKind::BinOp {
7065                    left: Box::new(left),
7066                    op: first_op,
7067                    right: Box::new(middle),
7068                },
7069                line,
7070            });
7071        }
7072
7073        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
7074        // Collect all operands and operators for chains like `1 < x < 10 < y`
7075        let mut operands = vec![left, middle];
7076        let mut ops = vec![first_op];
7077
7078        loop {
7079            let op = match self.peek() {
7080                Token::NumLt => BinOp::NumLt,
7081                Token::NumGt => BinOp::NumGt,
7082                Token::NumLe => BinOp::NumLe,
7083                Token::NumGe => BinOp::NumGe,
7084                Token::StrLt => BinOp::StrLt,
7085                Token::StrGt => BinOp::StrGt,
7086                Token::StrLe => BinOp::StrLe,
7087                Token::StrGe => BinOp::StrGe,
7088                _ => break,
7089            };
7090            self.advance();
7091            ops.push(op);
7092            operands.push(self.parse_shift()?);
7093        }
7094
7095        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
7096        let mut result = Expr {
7097            kind: ExprKind::BinOp {
7098                left: Box::new(operands[0].clone()),
7099                op: ops[0],
7100                right: Box::new(operands[1].clone()),
7101            },
7102            line,
7103        };
7104
7105        for i in 1..ops.len() {
7106            let cmp = Expr {
7107                kind: ExprKind::BinOp {
7108                    left: Box::new(operands[i].clone()),
7109                    op: ops[i],
7110                    right: Box::new(operands[i + 1].clone()),
7111                },
7112                line,
7113            };
7114            result = Expr {
7115                kind: ExprKind::BinOp {
7116                    left: Box::new(result),
7117                    op: BinOp::LogAnd,
7118                    right: Box::new(cmp),
7119                },
7120                line,
7121            };
7122        }
7123
7124        Ok(result)
7125    }
7126
7127    fn parse_shift(&mut self) -> PerlResult<Expr> {
7128        let mut left = self.parse_addition()?;
7129        loop {
7130            let op = match self.peek() {
7131                Token::ShiftLeft => BinOp::ShiftLeft,
7132                Token::ShiftRight => BinOp::ShiftRight,
7133                _ => break,
7134            };
7135            let line = left.line;
7136            self.advance();
7137            let right = self.parse_addition()?;
7138            left = Expr {
7139                kind: ExprKind::BinOp {
7140                    left: Box::new(left),
7141                    op,
7142                    right: Box::new(right),
7143                },
7144                line,
7145            };
7146        }
7147        Ok(left)
7148    }
7149
7150    fn parse_addition(&mut self) -> PerlResult<Expr> {
7151        let mut left = self.parse_multiplication()?;
7152        loop {
7153            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
7154            // the next statement, not a binary operator continuing this expression.
7155            let op = match self.peek() {
7156                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
7157                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
7158                Token::Dot => BinOp::Concat,
7159                _ => break,
7160            };
7161            let line = left.line;
7162            self.advance();
7163            let right = self.parse_multiplication()?;
7164            left = Expr {
7165                kind: ExprKind::BinOp {
7166                    left: Box::new(left),
7167                    op,
7168                    right: Box::new(right),
7169                },
7170                line,
7171            };
7172        }
7173        Ok(left)
7174    }
7175
7176    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
7177        let mut left = self.parse_regex_bind()?;
7178        loop {
7179            let op = match self.peek() {
7180                Token::Star => BinOp::Mul,
7181                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
7182                // Implicit semicolon: `%` on a new line is a hash dereference or hash
7183                // sigil for the next statement, not modulo operator on this expression.
7184                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
7185                Token::X => {
7186                    let line = left.line;
7187                    // List-repeat fires when the LHS was just closed by a
7188                    // list-constructor paren (`(EXPR)`, `(LIST)`, `()`) or
7189                    // `qw(...)`. `parse_primary` records the post-close
7190                    // position; an exact match against `self.pos` here means
7191                    // no postfix consumed any tokens between the close and
7192                    // the `x`, so the LHS is intrinsically a list construct.
7193                    let list_repeat = self.list_construct_close_pos == Some(self.pos);
7194                    self.advance();
7195                    let right = self.parse_regex_bind()?;
7196                    left = Expr {
7197                        kind: ExprKind::Repeat {
7198                            expr: Box::new(left),
7199                            count: Box::new(right),
7200                            list_repeat,
7201                        },
7202                        line,
7203                    };
7204                    continue;
7205                }
7206                _ => break,
7207            };
7208            let line = left.line;
7209            self.advance();
7210            let right = self.parse_regex_bind()?;
7211            left = Expr {
7212                kind: ExprKind::BinOp {
7213                    left: Box::new(left),
7214                    op,
7215                    right: Box::new(right),
7216                },
7217                line,
7218            };
7219        }
7220        Ok(left)
7221    }
7222
7223    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
7224        let left = self.parse_unary()?;
7225        match self.peek() {
7226            Token::BindMatch => {
7227                let line = left.line;
7228                self.advance();
7229                match self.peek().clone() {
7230                    Token::Regex(pattern, flags, delim) => {
7231                        self.advance();
7232                        Ok(Expr {
7233                            kind: ExprKind::Match {
7234                                expr: Box::new(left),
7235                                pattern,
7236                                flags,
7237                                scalar_g: false,
7238                                delim,
7239                            },
7240                            line,
7241                        })
7242                    }
7243                    Token::Ident(ref s) if s.starts_with('\x00') => {
7244                        let (Token::Ident(encoded), _) = self.advance() else {
7245                            unreachable!()
7246                        };
7247                        let parts: Vec<&str> = encoded.split('\x00').collect();
7248                        if parts.len() >= 4 && parts[1] == "s" {
7249                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7250                            Ok(Expr {
7251                                kind: ExprKind::Substitution {
7252                                    expr: Box::new(left),
7253                                    pattern: parts[2].to_string(),
7254                                    replacement: parts[3].to_string(),
7255                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7256                                    delim,
7257                                },
7258                                line,
7259                            })
7260                        } else if parts.len() >= 4 && parts[1] == "tr" {
7261                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7262                            Ok(Expr {
7263                                kind: ExprKind::Transliterate {
7264                                    expr: Box::new(left),
7265                                    from: parts[2].to_string(),
7266                                    to: parts[3].to_string(),
7267                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7268                                    delim,
7269                                },
7270                                line,
7271                            })
7272                        } else {
7273                            Err(self.syntax_err("Invalid regex binding", line))
7274                        }
7275                    }
7276                    _ => {
7277                        let rhs = self.parse_unary()?;
7278                        Ok(Expr {
7279                            kind: ExprKind::BinOp {
7280                                left: Box::new(left),
7281                                op: BinOp::BindMatch,
7282                                right: Box::new(rhs),
7283                            },
7284                            line,
7285                        })
7286                    }
7287                }
7288            }
7289            Token::BindNotMatch => {
7290                let line = left.line;
7291                self.advance();
7292                match self.peek().clone() {
7293                    Token::Regex(pattern, flags, delim) => {
7294                        self.advance();
7295                        Ok(Expr {
7296                            kind: ExprKind::UnaryOp {
7297                                op: UnaryOp::LogNot,
7298                                expr: Box::new(Expr {
7299                                    kind: ExprKind::Match {
7300                                        expr: Box::new(left),
7301                                        pattern,
7302                                        flags,
7303                                        scalar_g: false,
7304                                        delim,
7305                                    },
7306                                    line,
7307                                }),
7308                            },
7309                            line,
7310                        })
7311                    }
7312                    Token::Ident(ref s) if s.starts_with('\x00') => {
7313                        let (Token::Ident(encoded), _) = self.advance() else {
7314                            unreachable!()
7315                        };
7316                        let parts: Vec<&str> = encoded.split('\x00').collect();
7317                        if parts.len() >= 4 && parts[1] == "s" {
7318                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7319                            Ok(Expr {
7320                                kind: ExprKind::UnaryOp {
7321                                    op: UnaryOp::LogNot,
7322                                    expr: Box::new(Expr {
7323                                        kind: ExprKind::Substitution {
7324                                            expr: Box::new(left),
7325                                            pattern: parts[2].to_string(),
7326                                            replacement: parts[3].to_string(),
7327                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7328                                            delim,
7329                                        },
7330                                        line,
7331                                    }),
7332                                },
7333                                line,
7334                            })
7335                        } else if parts.len() >= 4 && parts[1] == "tr" {
7336                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7337                            Ok(Expr {
7338                                kind: ExprKind::UnaryOp {
7339                                    op: UnaryOp::LogNot,
7340                                    expr: Box::new(Expr {
7341                                        kind: ExprKind::Transliterate {
7342                                            expr: Box::new(left),
7343                                            from: parts[2].to_string(),
7344                                            to: parts[3].to_string(),
7345                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7346                                            delim,
7347                                        },
7348                                        line,
7349                                    }),
7350                                },
7351                                line,
7352                            })
7353                        } else {
7354                            Err(self.syntax_err("Invalid regex binding after !~", line))
7355                        }
7356                    }
7357                    _ => {
7358                        let rhs = self.parse_unary()?;
7359                        Ok(Expr {
7360                            kind: ExprKind::BinOp {
7361                                left: Box::new(left),
7362                                op: BinOp::BindNotMatch,
7363                                right: Box::new(rhs),
7364                            },
7365                            line,
7366                        })
7367                    }
7368                }
7369            }
7370            _ => Ok(left),
7371        }
7372    }
7373
7374    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
7375    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
7376    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
7377        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
7378        let result = self.parse_range();
7379        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
7380        result
7381    }
7382
7383    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
7384    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
7385    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
7386    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
7387    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
7388    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
7389    fn parse_range(&mut self) -> PerlResult<Expr> {
7390        let left = self.parse_log_or()?;
7391        let line = left.line;
7392        // `1..10` (traditional inclusive) / `1...10` (exclusive) / `1:10`
7393        // (short form) / `1~10` (universal short form). The `~` separator
7394        // works for every range type and is the only viable separator for
7395        // IPv6 since IPv6 already uses `:` internally; `:` would collide.
7396        // It also dodges `!`'s collision with the `_!N!` paired char-index
7397        // syntax. Single-`~` (vs `!!!` triple) keeps the surface simple.
7398        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
7399            (true, false)
7400        } else if self.eat(&Token::Range) {
7401            (false, false)
7402        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
7403            // `1:10` short form — only valid for numeric ranges, not ternary
7404            // Lookahead: must be followed by something that looks like a range endpoint
7405            (false, true)
7406        } else if self.suppress_tilde_range == 0 && self.eat(&Token::BitNot) {
7407            (false, true)
7408        } else {
7409            return Ok(left);
7410        };
7411        let right = self.parse_log_or()?;
7412        // Optional step: `1..100:2` / `1:100:2` / `IPV6~IPV6~STEP`. `~` is
7413        // gated by `suppress_tilde_range` so paired char-index (`$x~5~`)
7414        // doesn't get its closing delimiter eaten as a range op.
7415        let step = if self.eat(&Token::Colon)
7416            || (self.suppress_tilde_range == 0 && self.eat(&Token::BitNot))
7417        {
7418            Some(Box::new(self.parse_unary()?))
7419        } else {
7420            None
7421        };
7422        Ok(Expr {
7423            kind: ExprKind::Range {
7424                from: Box::new(left),
7425                to: Box::new(right),
7426                exclusive,
7427                step,
7428            },
7429            line,
7430        })
7431    }
7432
7433    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
7434    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
7435        let mut name = match self.advance() {
7436            (Token::Ident(n), _) => n,
7437            (tok, l) => {
7438                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
7439            }
7440        };
7441        while self.eat(&Token::PackageSep) {
7442            match self.advance() {
7443                (Token::Ident(part), _) => {
7444                    name.push_str("::");
7445                    name.push_str(&part);
7446                }
7447                (tok, l) => {
7448                    return Err(self
7449                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
7450                }
7451            }
7452        }
7453        Ok(name)
7454    }
7455
7456    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
7457    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
7458        self.parse_package_qualified_identifier()
7459    }
7460
7461    fn parse_unary(&mut self) -> PerlResult<Expr> {
7462        let line = self.peek_line();
7463        match self.peek().clone() {
7464            Token::Minus => {
7465                self.advance();
7466                let expr = self.parse_power()?;
7467                Ok(Expr {
7468                    kind: ExprKind::UnaryOp {
7469                        op: UnaryOp::Negate,
7470                        expr: Box::new(expr),
7471                    },
7472                    line,
7473                })
7474            }
7475            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
7476            // and for scalar context; treat as a no-op on the parsed operand.
7477            // Special case: `+{ ... }` forces hashref interpretation (Perl idiom),
7478            // even when the body is a list-yielding expression like `+{ map { ... } @arr }`.
7479            // Without this, `{ map { ... } @arr }` falls back to block/CodeRef parsing
7480            // because the body doesn't fit `KEY => VAL` shape.
7481            Token::Plus => {
7482                self.advance();
7483                if matches!(self.peek(), Token::LBrace) {
7484                    let line = self.peek_line();
7485                    self.advance(); // consume {
7486                    return self.parse_forced_hashref_body(line);
7487                }
7488                self.parse_unary()
7489            }
7490            Token::LogNot => {
7491                self.advance();
7492                let expr = self.parse_unary()?;
7493                Ok(Expr {
7494                    kind: ExprKind::UnaryOp {
7495                        op: UnaryOp::LogNot,
7496                        expr: Box::new(expr),
7497                    },
7498                    line,
7499                })
7500            }
7501            Token::BitNot => {
7502                self.advance();
7503                let expr = self.parse_unary()?;
7504                Ok(Expr {
7505                    kind: ExprKind::UnaryOp {
7506                        op: UnaryOp::BitNot,
7507                        expr: Box::new(expr),
7508                    },
7509                    line,
7510                })
7511            }
7512            Token::Increment => {
7513                self.advance();
7514                let expr = self.parse_postfix()?;
7515                Ok(Expr {
7516                    kind: ExprKind::UnaryOp {
7517                        op: UnaryOp::PreIncrement,
7518                        expr: Box::new(expr),
7519                    },
7520                    line,
7521                })
7522            }
7523            Token::Decrement => {
7524                self.advance();
7525                let expr = self.parse_postfix()?;
7526                Ok(Expr {
7527                    kind: ExprKind::UnaryOp {
7528                        op: UnaryOp::PreDecrement,
7529                        expr: Box::new(expr),
7530                    },
7531                    line,
7532                })
7533            }
7534            Token::BitAnd => {
7535                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
7536                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
7537                self.advance();
7538                if matches!(self.peek(), Token::LBrace) {
7539                    self.advance();
7540                    let inner = self.parse_expression()?;
7541                    self.expect(&Token::RBrace)?;
7542                    return Ok(Expr {
7543                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
7544                        line,
7545                    });
7546                }
7547                if matches!(self.peek(), Token::Ident(_)) {
7548                    let name = self.parse_qualified_subroutine_name()?;
7549                    return Ok(Expr {
7550                        kind: ExprKind::SubroutineRef(name),
7551                        line,
7552                    });
7553                }
7554                let target = self.parse_primary()?;
7555                if matches!(self.peek(), Token::LParen) {
7556                    self.advance();
7557                    let args = self.parse_arg_list()?;
7558                    self.expect(&Token::RParen)?;
7559                    return Ok(Expr {
7560                        kind: ExprKind::IndirectCall {
7561                            target: Box::new(target),
7562                            args,
7563                            ampersand: true,
7564                            pass_caller_arglist: false,
7565                        },
7566                        line,
7567                    });
7568                }
7569                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
7570                Ok(Expr {
7571                    kind: ExprKind::IndirectCall {
7572                        target: Box::new(target),
7573                        args: vec![],
7574                        ampersand: true,
7575                        pass_caller_arglist: true,
7576                    },
7577                    line,
7578                })
7579            }
7580            Token::Backslash => {
7581                self.advance();
7582                let expr = self.parse_unary()?;
7583                if let ExprKind::SubroutineRef(name) = expr.kind {
7584                    return Ok(Expr {
7585                        kind: ExprKind::SubroutineCodeRef(name),
7586                        line,
7587                    });
7588                }
7589                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
7590                    return Ok(expr);
7591                }
7592                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
7593                Ok(Expr {
7594                    kind: ExprKind::ScalarRef(Box::new(expr)),
7595                    line,
7596                })
7597            }
7598            Token::FileTest(op) => {
7599                self.advance();
7600                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
7601                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
7602                    Expr {
7603                        kind: ExprKind::ScalarVar("_".into()),
7604                        line: self.peek_line(),
7605                    }
7606                } else {
7607                    self.parse_unary()?
7608                };
7609                Ok(Expr {
7610                    kind: ExprKind::FileTest {
7611                        op,
7612                        expr: Box::new(expr),
7613                    },
7614                    line,
7615                })
7616            }
7617            _ => self.parse_power(),
7618        }
7619    }
7620
7621    fn parse_power(&mut self) -> PerlResult<Expr> {
7622        let left = self.parse_postfix()?;
7623        if matches!(self.peek(), Token::Power) {
7624            let line = left.line;
7625            self.advance();
7626            let right = self.parse_unary()?; // right-associative
7627            return Ok(Expr {
7628                kind: ExprKind::BinOp {
7629                    left: Box::new(left),
7630                    op: BinOp::Pow,
7631                    right: Box::new(right),
7632                },
7633                line,
7634            });
7635        }
7636        Ok(left)
7637    }
7638
7639    fn parse_postfix(&mut self) -> PerlResult<Expr> {
7640        let mut expr = self.parse_primary()?;
7641        loop {
7642            match self.peek().clone() {
7643                Token::Increment => {
7644                    // Implicit semicolon: `++` on a new line is a prefix operator
7645                    // on the next statement, not postfix on the previous expression.
7646                    if self.peek_line() > self.prev_line() {
7647                        break;
7648                    }
7649                    let line = expr.line;
7650                    self.advance();
7651                    expr = Expr {
7652                        kind: ExprKind::PostfixOp {
7653                            expr: Box::new(expr),
7654                            op: PostfixOp::Increment,
7655                        },
7656                        line,
7657                    };
7658                }
7659                Token::Decrement => {
7660                    // Implicit semicolon: `--` on a new line is a prefix operator
7661                    // on the next statement, not postfix on the previous expression.
7662                    if self.peek_line() > self.prev_line() {
7663                        break;
7664                    }
7665                    let line = expr.line;
7666                    self.advance();
7667                    expr = Expr {
7668                        kind: ExprKind::PostfixOp {
7669                            expr: Box::new(expr),
7670                            op: PostfixOp::Decrement,
7671                        },
7672                        line,
7673                    };
7674                }
7675                Token::LParen => {
7676                    if self.suppress_indirect_paren_call > 0 {
7677                        break;
7678                    }
7679                    // Implicit semicolon: `(` on a new line after an expression
7680                    // is a new statement, not a postfix code-ref call.
7681                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
7682                    if self.peek_line() > self.prev_line() {
7683                        break;
7684                    }
7685                    let line = expr.line;
7686                    self.advance();
7687                    let args = self.parse_arg_list()?;
7688                    self.expect(&Token::RParen)?;
7689                    expr = Expr {
7690                        kind: ExprKind::IndirectCall {
7691                            target: Box::new(expr),
7692                            args,
7693                            ampersand: false,
7694                            pass_caller_arglist: false,
7695                        },
7696                        line,
7697                    };
7698                }
7699                Token::Arrow => {
7700                    let line = expr.line;
7701                    self.advance();
7702                    match self.peek().clone() {
7703                        Token::LBracket => {
7704                            self.advance();
7705                            let index = self.parse_expression()?;
7706                            self.expect(&Token::RBracket)?;
7707                            expr = Expr {
7708                                kind: ExprKind::ArrowDeref {
7709                                    expr: Box::new(expr),
7710                                    index: Box::new(index),
7711                                    kind: DerefKind::Array,
7712                                },
7713                                line,
7714                            };
7715                        }
7716                        Token::LBrace => {
7717                            self.advance();
7718                            let key = self.parse_hash_subscript_key()?;
7719                            self.expect(&Token::RBrace)?;
7720                            expr = Expr {
7721                                kind: ExprKind::ArrowDeref {
7722                                    expr: Box::new(expr),
7723                                    index: Box::new(key),
7724                                    kind: DerefKind::Hash,
7725                                },
7726                                line,
7727                            };
7728                        }
7729                        Token::LParen => {
7730                            self.advance();
7731                            let args = self.parse_arg_list()?;
7732                            self.expect(&Token::RParen)?;
7733                            expr = Expr {
7734                                kind: ExprKind::ArrowDeref {
7735                                    expr: Box::new(expr),
7736                                    index: Box::new(Expr {
7737                                        kind: ExprKind::List(args),
7738                                        line,
7739                                    }),
7740                                    kind: DerefKind::Call,
7741                                },
7742                                line,
7743                            };
7744                        }
7745                        Token::Ident(method) => {
7746                            self.advance();
7747                            if method == "SUPER" {
7748                                self.expect(&Token::PackageSep)?;
7749                                let real_method = match self.advance() {
7750                                    (Token::Ident(n), _) => n,
7751                                    (tok, l) => {
7752                                        return Err(self.syntax_err(
7753                                            format!(
7754                                                "Expected method name after SUPER::, got {:?}",
7755                                                tok
7756                                            ),
7757                                            l,
7758                                        ));
7759                                    }
7760                                };
7761                                let args = if self.eat(&Token::LParen) {
7762                                    let a = self.parse_arg_list()?;
7763                                    self.expect(&Token::RParen)?;
7764                                    a
7765                                } else {
7766                                    self.parse_method_arg_list_no_paren()?
7767                                };
7768                                expr = Expr {
7769                                    kind: ExprKind::MethodCall {
7770                                        object: Box::new(expr),
7771                                        method: real_method,
7772                                        args,
7773                                        super_call: true,
7774                                    },
7775                                    line,
7776                                };
7777                            } else {
7778                                let mut method_name = method;
7779                                while self.eat(&Token::PackageSep) {
7780                                    match self.advance() {
7781                                        (Token::Ident(part), _) => {
7782                                            method_name.push_str("::");
7783                                            method_name.push_str(&part);
7784                                        }
7785                                        (tok, l) => {
7786                                            return Err(self.syntax_err(
7787                                                format!(
7788                                                    "Expected identifier after :: in method name, got {:?}",
7789                                                    tok
7790                                                ),
7791                                                l,
7792                                            ));
7793                                        }
7794                                    }
7795                                }
7796                                let args = if self.eat(&Token::LParen) {
7797                                    let a = self.parse_arg_list()?;
7798                                    self.expect(&Token::RParen)?;
7799                                    a
7800                                } else {
7801                                    self.parse_method_arg_list_no_paren()?
7802                                };
7803                                expr = Expr {
7804                                    kind: ExprKind::MethodCall {
7805                                        object: Box::new(expr),
7806                                        method: method_name,
7807                                        args,
7808                                        super_call: false,
7809                                    },
7810                                    line,
7811                                };
7812                            }
7813                        }
7814                        // Postfix dereference (Perl 5.20+, default 5.24+):
7815                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
7816                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
7817                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
7818                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
7819                        Token::ArrayAt => {
7820                            self.advance(); // consume `@`
7821                            match self.peek().clone() {
7822                                Token::Star => {
7823                                    self.advance();
7824                                    expr = Expr {
7825                                        kind: ExprKind::Deref {
7826                                            expr: Box::new(expr),
7827                                            kind: Sigil::Array,
7828                                        },
7829                                        line,
7830                                    };
7831                                }
7832                                Token::LBracket => {
7833                                    self.advance();
7834                                    let indices = self.parse_slice_arg_list(false)?;
7835                                    self.expect(&Token::RBracket)?;
7836                                    let source = Expr {
7837                                        kind: ExprKind::Deref {
7838                                            expr: Box::new(expr),
7839                                            kind: Sigil::Array,
7840                                        },
7841                                        line,
7842                                    };
7843                                    expr = Expr {
7844                                        kind: ExprKind::AnonymousListSlice {
7845                                            source: Box::new(source),
7846                                            indices,
7847                                        },
7848                                        line,
7849                                    };
7850                                }
7851                                Token::LBrace => {
7852                                    self.advance();
7853                                    let keys = self.parse_slice_arg_list(true)?;
7854                                    self.expect(&Token::RBrace)?;
7855                                    expr = Expr {
7856                                        kind: ExprKind::HashSliceDeref {
7857                                            container: Box::new(expr),
7858                                            keys,
7859                                        },
7860                                        line,
7861                                    };
7862                                }
7863                                tok => {
7864                                    return Err(self.syntax_err(
7865                                        format!(
7866                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
7867                                            tok
7868                                        ),
7869                                        line,
7870                                    ));
7871                                }
7872                            }
7873                        }
7874                        Token::HashPercent => {
7875                            self.advance(); // consume `%`
7876                            match self.peek().clone() {
7877                                Token::Star => {
7878                                    self.advance();
7879                                    expr = Expr {
7880                                        kind: ExprKind::Deref {
7881                                            expr: Box::new(expr),
7882                                            kind: Sigil::Hash,
7883                                        },
7884                                        line,
7885                                    };
7886                                }
7887                                tok => {
7888                                    return Err(self.syntax_err(
7889                                        format!("Expected `*` after `->%`, got {:?}", tok),
7890                                        line,
7891                                    ));
7892                                }
7893                            }
7894                        }
7895                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
7896                        Token::X => {
7897                            self.advance();
7898                            let args = if self.eat(&Token::LParen) {
7899                                let a = self.parse_arg_list()?;
7900                                self.expect(&Token::RParen)?;
7901                                a
7902                            } else {
7903                                self.parse_method_arg_list_no_paren()?
7904                            };
7905                            expr = Expr {
7906                                kind: ExprKind::MethodCall {
7907                                    object: Box::new(expr),
7908                                    method: "x".to_string(),
7909                                    args,
7910                                    super_call: false,
7911                                },
7912                                line,
7913                            };
7914                        }
7915                        _ => break,
7916                    }
7917                }
7918                Token::LBracket => {
7919                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
7920                    // not an array subscript on the preceding expression.
7921                    if self.peek_line() > self.prev_line() {
7922                        break;
7923                    }
7924                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
7925                    let line = expr.line;
7926                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
7927                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7928                            let name = name.clone();
7929                            self.advance();
7930                            let index = self.parse_expression()?;
7931                            self.expect(&Token::RBracket)?;
7932                            expr = Expr {
7933                                kind: ExprKind::ArrayElement {
7934                                    array: name,
7935                                    index: Box::new(index),
7936                                },
7937                                line,
7938                            };
7939                        }
7940                    } else if postfix_lbracket_is_arrow_container(&expr) {
7941                        self.advance();
7942                        let indices = self.parse_arg_list()?;
7943                        self.expect(&Token::RBracket)?;
7944                        expr = Expr {
7945                            kind: ExprKind::ArrowDeref {
7946                                expr: Box::new(expr),
7947                                index: Box::new(Expr {
7948                                    kind: ExprKind::List(indices),
7949                                    line,
7950                                }),
7951                                kind: DerefKind::Array,
7952                            },
7953                            line,
7954                        };
7955                    } else {
7956                        self.advance();
7957                        let indices = self.parse_arg_list()?;
7958                        self.expect(&Token::RBracket)?;
7959                        expr = Expr {
7960                            kind: ExprKind::AnonymousListSlice {
7961                                source: Box::new(expr),
7962                                indices,
7963                            },
7964                            line,
7965                        };
7966                    }
7967                }
7968                Token::LBrace => {
7969                    if self.suppress_scalar_hash_brace > 0 {
7970                        break;
7971                    }
7972                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
7973                    // not a hash subscript on the preceding expression.
7974                    if self.peek_line() > self.prev_line() {
7975                        break;
7976                    }
7977                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
7978                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
7979                    let line = expr.line;
7980                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
7981                    let is_chainable_hash_subscript = is_scalar_named_hash
7982                        || matches!(
7983                            expr.kind,
7984                            ExprKind::HashElement { .. }
7985                                | ExprKind::ArrayElement { .. }
7986                                | ExprKind::ArrowDeref { .. }
7987                                | ExprKind::Deref {
7988                                    kind: Sigil::Scalar,
7989                                    ..
7990                                }
7991                        );
7992                    if !is_chainable_hash_subscript {
7993                        break;
7994                    }
7995                    self.advance();
7996                    let key = self.parse_hash_subscript_key()?;
7997                    self.expect(&Token::RBrace)?;
7998                    expr = if is_scalar_named_hash {
7999                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8000                            let name = name.clone();
8001                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
8002                            if name == "_" {
8003                                Expr {
8004                                    kind: ExprKind::ArrowDeref {
8005                                        expr: Box::new(Expr {
8006                                            kind: ExprKind::ScalarVar("_".into()),
8007                                            line,
8008                                        }),
8009                                        index: Box::new(key),
8010                                        kind: DerefKind::Hash,
8011                                    },
8012                                    line,
8013                                }
8014                            } else {
8015                                Expr {
8016                                    kind: ExprKind::HashElement {
8017                                        hash: name,
8018                                        key: Box::new(key),
8019                                    },
8020                                    line,
8021                                }
8022                            }
8023                        } else {
8024                            unreachable!("is_scalar_named_hash implies ScalarVar");
8025                        }
8026                    } else {
8027                        Expr {
8028                            kind: ExprKind::ArrowDeref {
8029                                expr: Box::new(expr),
8030                                index: Box::new(key),
8031                                kind: DerefKind::Hash,
8032                            },
8033                            line,
8034                        }
8035                    };
8036                }
8037                Token::LogNot | Token::BitNot => {
8038                    // Stryke universal string-subscript sugar — paired `!…!`
8039                    // OR paired `~…~`: `$VAR!N!`, `$VAR~N~`, `$VAR!1:5:2!`,
8040                    // `_!N!`, `_~from:to:step~`. Returns substring of the
8041                    // scalar (Unicode chars).  Distinct from `[N]` which has
8042                    // Perl's `@VAR[N]` / `$_[N]` semantics. Both forms work on
8043                    // any scalar (named or topic) without colliding: `!` and
8044                    // `~` after a value have no current postfix meaning (`!=`
8045                    // / `!~` are pre-merged binary tokens; `~` is prefix-only
8046                    // bit-not). The opening and closing delimiter must match.
8047                    //
8048                    // Implementation: rewrite to ArrayElement with a
8049                    // synthetic name `__topicstr__$NAME`. The interpreter
8050                    // and VM strip the prefix and dispatch to char-of-string
8051                    // (and slice-of-string for Range indices).
8052                    if !matches!(expr.kind, ExprKind::ScalarVar(_)) {
8053                        break;
8054                    }
8055                    if self.peek_line() > self.prev_line() {
8056                        break;
8057                    }
8058                    let opener = self.peek().clone();
8059                    let line = expr.line;
8060                    let name = if let ExprKind::ScalarVar(ref n) = expr.kind {
8061                        n.clone()
8062                    } else {
8063                        unreachable!()
8064                    };
8065                    self.advance(); // consume opening `!` or `~`
8066                                    // Suppress `~` as a range separator while parsing the
8067                                    // paired index — `$_~5~` would otherwise consume the
8068                                    // closing `~` as a range op. `:` is still allowed so
8069                                    // `$_~1:3~` (slice with `:` range index) keeps working.
8070                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_add(1);
8071                    let index_result = self.parse_expression();
8072                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_sub(1);
8073                    let index = index_result?;
8074                    let close_match = matches!(
8075                        (&opener, self.peek()),
8076                        (Token::LogNot, Token::LogNot) | (Token::BitNot, Token::BitNot)
8077                    );
8078                    if !close_match {
8079                        let want = if matches!(opener, Token::LogNot) {
8080                            "!"
8081                        } else {
8082                            "~"
8083                        };
8084                        return Err(self.syntax_err(
8085                            format!("expected closing `{}` for string subscript", want),
8086                            self.peek_line(),
8087                        ));
8088                    }
8089                    self.advance(); // consume closing delimiter
8090                    expr = Expr {
8091                        kind: ExprKind::ArrayElement {
8092                            array: format!("__topicstr__{}", name),
8093                            index: Box::new(index),
8094                        },
8095                        line,
8096                    };
8097                }
8098                _ => break,
8099            }
8100        }
8101        Ok(expr)
8102    }
8103
8104    fn parse_primary(&mut self) -> PerlResult<Expr> {
8105        let line = self.peek_line();
8106        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
8107        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
8108        // assigned value(s); has the side effect of declaring the variable in
8109        // the current scope.  See `ExprKind::MyExpr`.
8110        if let Token::Ident(ref kw) = self.peek().clone() {
8111            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
8112                let kw_owned = kw.clone();
8113                // Parse exactly like the statement form via `parse_my_our_local`,
8114                // then unwrap the resulting `StmtKind::*` back into a list of
8115                // `VarDecl`s for the expression node.  This re-uses the full
8116                // syntax (typed sigs, list destructuring, type annotations).
8117                let saved_pos = self.pos;
8118                let stmt = self.parse_my_our_local(&kw_owned, false)?;
8119                let decls = match stmt.kind {
8120                    StmtKind::My(d)
8121                    | StmtKind::Our(d)
8122                    | StmtKind::State(d)
8123                    | StmtKind::Local(d) => d,
8124                    _ => {
8125                        // `local *FOO = …` / non-decl forms — fall back to the
8126                        // statement parser (already advanced); restore position
8127                        // and let the surrounding code handle it as a statement
8128                        // by erroring loudly here.
8129                        self.pos = saved_pos;
8130                        return Err(self.syntax_err(
8131                            "`my`/`our`/`local` in expression must declare variables",
8132                            line,
8133                        ));
8134                    }
8135                };
8136                return Ok(Expr {
8137                    kind: ExprKind::MyExpr {
8138                        keyword: kw_owned,
8139                        decls,
8140                    },
8141                    line,
8142                });
8143            }
8144        }
8145        match self.peek().clone() {
8146            Token::Integer(n) => {
8147                self.advance();
8148                Ok(Expr {
8149                    kind: ExprKind::Integer(n),
8150                    line,
8151                })
8152            }
8153            Token::Float(f) => {
8154                self.advance();
8155                Ok(Expr {
8156                    kind: ExprKind::Float(f),
8157                    line,
8158                })
8159            }
8160            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
8161            // Valid in any expression position; evaluates the block and yields its last value.
8162            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
8163            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
8164            // instead pipe-applied as a coderef — that path is never reached from here.
8165            Token::ArrowBrace => {
8166                self.advance();
8167                let mut stmts = Vec::new();
8168                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8169                    if self.eat(&Token::Semicolon) {
8170                        continue;
8171                    }
8172                    stmts.push(self.parse_statement()?);
8173                }
8174                self.expect(&Token::RBrace)?;
8175                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
8176                let inner = Expr {
8177                    kind: ExprKind::CodeRef {
8178                        params: vec![],
8179                        body: stmts,
8180                    },
8181                    line: inner_line,
8182                };
8183                Ok(Expr {
8184                    kind: ExprKind::Do(Box::new(inner)),
8185                    line,
8186                })
8187            }
8188            Token::Star => {
8189                self.advance();
8190                if matches!(self.peek(), Token::LBrace) {
8191                    self.advance();
8192                    let inner = self.parse_expression()?;
8193                    self.expect(&Token::RBrace)?;
8194                    return Ok(Expr {
8195                        kind: ExprKind::Deref {
8196                            expr: Box::new(inner),
8197                            kind: Sigil::Typeglob,
8198                        },
8199                        line,
8200                    });
8201                }
8202                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
8203                if matches!(
8204                    self.peek(),
8205                    Token::ScalarVar(_)
8206                        | Token::ArrayVar(_)
8207                        | Token::HashVar(_)
8208                        | Token::DerefScalarVar(_)
8209                        | Token::HashPercent
8210                ) {
8211                    let inner = self.parse_postfix()?;
8212                    return Ok(Expr {
8213                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
8214                        line,
8215                    });
8216                }
8217                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
8218                let mut full_name = match self.advance() {
8219                    (Token::Ident(n), _) => n,
8220                    (Token::X, _) => "x".to_string(),
8221                    (tok, l) => {
8222                        return Err(self
8223                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
8224                    }
8225                };
8226                while self.eat(&Token::PackageSep) {
8227                    match self.advance() {
8228                        (Token::Ident(part), _) => {
8229                            full_name = format!("{}::{}", full_name, part);
8230                        }
8231                        (Token::X, _) => {
8232                            full_name = format!("{}::x", full_name);
8233                        }
8234                        (tok, l) => {
8235                            return Err(self.syntax_err(
8236                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
8237                                l,
8238                            ));
8239                        }
8240                    }
8241                }
8242                Ok(Expr {
8243                    kind: ExprKind::Typeglob(full_name),
8244                    line,
8245                })
8246            }
8247            Token::SingleString(s) => {
8248                self.advance();
8249                Ok(Expr {
8250                    kind: ExprKind::String(s),
8251                    line,
8252                })
8253            }
8254            Token::DoubleString(s) => {
8255                self.advance();
8256                self.parse_interpolated_string(&s, line)
8257            }
8258            Token::BacktickString(s) => {
8259                self.advance();
8260                let inner = self.parse_interpolated_string(&s, line)?;
8261                Ok(Expr {
8262                    kind: ExprKind::Qx(Box::new(inner)),
8263                    line,
8264                })
8265            }
8266            Token::HereDoc(_, body, interpolate) => {
8267                self.advance();
8268                if interpolate {
8269                    self.parse_interpolated_string(&body, line)
8270                } else {
8271                    Ok(Expr {
8272                        kind: ExprKind::String(body),
8273                        line,
8274                    })
8275                }
8276            }
8277            Token::Regex(pattern, flags, _delim) => {
8278                self.advance();
8279                Ok(Expr {
8280                    kind: ExprKind::Regex(pattern, flags),
8281                    line,
8282                })
8283            }
8284            Token::QW(words) => {
8285                self.advance();
8286                // `qw(a b c) x N` is list-repeat in Perl even without explicit
8287                // outer parens — `qw(...)` is itself a list constructor.
8288                self.list_construct_close_pos = Some(self.pos);
8289                Ok(Expr {
8290                    kind: ExprKind::QW(words),
8291                    line,
8292                })
8293            }
8294            Token::DerefScalarVar(name) => {
8295                self.advance();
8296                Ok(Expr {
8297                    kind: ExprKind::Deref {
8298                        expr: Box::new(Expr {
8299                            kind: ExprKind::ScalarVar(name),
8300                            line,
8301                        }),
8302                        kind: Sigil::Scalar,
8303                    },
8304                    line,
8305                })
8306            }
8307            Token::ScalarVar(name) => {
8308                self.advance();
8309                Ok(Expr {
8310                    kind: ExprKind::ScalarVar(name),
8311                    line,
8312                })
8313            }
8314            Token::ArrayVar(name) => {
8315                self.advance();
8316                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
8317                match self.peek() {
8318                    Token::LBracket => {
8319                        self.advance();
8320                        let indices = self.parse_slice_arg_list(false)?;
8321                        self.expect(&Token::RBracket)?;
8322                        Ok(Expr {
8323                            kind: ExprKind::ArraySlice {
8324                                array: name,
8325                                indices,
8326                            },
8327                            line,
8328                        })
8329                    }
8330                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
8331                        self.advance();
8332                        let keys = self.parse_slice_arg_list(true)?;
8333                        self.expect(&Token::RBrace)?;
8334                        Ok(Expr {
8335                            kind: ExprKind::HashSlice { hash: name, keys },
8336                            line,
8337                        })
8338                    }
8339                    _ => Ok(Expr {
8340                        kind: ExprKind::ArrayVar(name),
8341                        line,
8342                    }),
8343                }
8344            }
8345            Token::HashVar(name) => {
8346                self.advance();
8347                // `%h{KEYS}` — Perl 5.20+ key-value slice. Parser-level
8348                // disambiguation: `%h` immediately followed by `{` is a kv-
8349                // slice; `%h` alone (or followed by `=`, list ops, etc.) is
8350                // the bare hash. (BUG-008)
8351                if matches!(self.peek(), Token::LBrace)
8352                    && self.suppress_scalar_hash_brace == 0
8353                {
8354                    self.advance(); // {
8355                    let keys = self.parse_slice_arg_list(true)?;
8356                    self.expect(&Token::RBrace)?;
8357                    return Ok(Expr {
8358                        kind: ExprKind::HashKvSlice { hash: name, keys },
8359                        line,
8360                    });
8361                }
8362                Ok(Expr {
8363                    kind: ExprKind::HashVar(name),
8364                    line,
8365                })
8366            }
8367            Token::HashPercent => {
8368                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
8369                self.advance();
8370                if matches!(self.peek(), Token::ScalarVar(_)) {
8371                    let n = match self.advance() {
8372                        (Token::ScalarVar(n), _) => n,
8373                        (tok, l) => {
8374                            return Err(self.syntax_err(
8375                                format!("Expected scalar variable after %%, got {:?}", tok),
8376                                l,
8377                            ));
8378                        }
8379                    };
8380                    return Ok(Expr {
8381                        kind: ExprKind::Deref {
8382                            expr: Box::new(Expr {
8383                                kind: ExprKind::ScalarVar(n),
8384                                line,
8385                            }),
8386                            kind: Sigil::Hash,
8387                        },
8388                        line,
8389                    });
8390                }
8391                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
8392                // anonymous hashref inline, using `[...]` as the delimiter to avoid
8393                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
8394                // Real Perl errors on `%[...]` syntactically, so no compat risk.
8395                if matches!(self.peek(), Token::LBracket) {
8396                    self.advance();
8397                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8398                    self.expect(&Token::RBracket)?;
8399                    let href = Expr {
8400                        kind: ExprKind::HashRef(pairs),
8401                        line,
8402                    };
8403                    return Ok(Expr {
8404                        kind: ExprKind::Deref {
8405                            expr: Box::new(href),
8406                            kind: Sigil::Hash,
8407                        },
8408                        line,
8409                    });
8410                }
8411                self.expect(&Token::LBrace)?;
8412                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
8413                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
8414                // heuristic is famously unreliable — when the first non-whitespace
8415                // token is an ident/string followed by `=>`, treat the whole thing
8416                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
8417                let looks_like_pair = matches!(
8418                    self.peek(),
8419                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8420                ) && matches!(self.peek_at(1), Token::FatArrow);
8421                let inner = if looks_like_pair {
8422                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8423                    Expr {
8424                        kind: ExprKind::HashRef(pairs),
8425                        line,
8426                    }
8427                } else {
8428                    self.parse_expression()?
8429                };
8430                self.expect(&Token::RBrace)?;
8431                Ok(Expr {
8432                    kind: ExprKind::Deref {
8433                        expr: Box::new(inner),
8434                        kind: Sigil::Hash,
8435                    },
8436                    line,
8437                })
8438            }
8439            Token::ArrayAt => {
8440                self.advance();
8441                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
8442                if matches!(self.peek(), Token::LBrace) {
8443                    self.advance();
8444                    let inner = self.parse_expression()?;
8445                    self.expect(&Token::RBrace)?;
8446                    return Ok(Expr {
8447                        kind: ExprKind::Deref {
8448                            expr: Box::new(inner),
8449                            kind: Sigil::Array,
8450                        },
8451                        line,
8452                    });
8453                }
8454                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
8455                // anonymous arrayref inline. Real Perl rejects `@[...]` at
8456                // the parser level, so this extension has no compat risk.
8457                if matches!(self.peek(), Token::LBracket) {
8458                    self.advance();
8459                    let mut elems = Vec::new();
8460                    if !matches!(self.peek(), Token::RBracket) {
8461                        elems.push(self.parse_assign_expr()?);
8462                        while self.eat(&Token::Comma) {
8463                            if matches!(self.peek(), Token::RBracket) {
8464                                break;
8465                            }
8466                            elems.push(self.parse_assign_expr()?);
8467                        }
8468                    }
8469                    self.expect(&Token::RBracket)?;
8470                    let aref = Expr {
8471                        kind: ExprKind::ArrayRef(elems),
8472                        line,
8473                    };
8474                    return Ok(Expr {
8475                        kind: ExprKind::Deref {
8476                            expr: Box::new(aref),
8477                            kind: Sigil::Array,
8478                        },
8479                        line,
8480                    });
8481                }
8482                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
8483                let container = match self.peek().clone() {
8484                    Token::ScalarVar(n) => {
8485                        self.advance();
8486                        Expr {
8487                            kind: ExprKind::ScalarVar(n),
8488                            line,
8489                        }
8490                    }
8491                    _ => {
8492                        return Err(self.syntax_err(
8493                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
8494                            line,
8495                        ));
8496                    }
8497                };
8498                if matches!(self.peek(), Token::LBrace) {
8499                    self.advance();
8500                    let keys = self.parse_slice_arg_list(true)?;
8501                    self.expect(&Token::RBrace)?;
8502                    return Ok(Expr {
8503                        kind: ExprKind::HashSliceDeref {
8504                            container: Box::new(container),
8505                            keys,
8506                        },
8507                        line,
8508                    });
8509                }
8510                Ok(Expr {
8511                    kind: ExprKind::Deref {
8512                        expr: Box::new(container),
8513                        kind: Sigil::Array,
8514                    },
8515                    line,
8516                })
8517            }
8518            Token::LParen => {
8519                self.advance();
8520                if matches!(self.peek(), Token::RParen) {
8521                    self.advance();
8522                    // Empty `() x 3` is a no-op list repeat — record the close
8523                    // position so `Token::X` knows the LHS was a list literal.
8524                    self.list_construct_close_pos = Some(self.pos);
8525                    return Ok(Expr {
8526                        kind: ExprKind::List(vec![]),
8527                        line,
8528                    });
8529                }
8530                // Inside parens, pipe-forward is allowed even if we're in a
8531                // paren-less arg context. Save and restore no_pipe_forward_depth.
8532                let saved_no_pipe = self.no_pipe_forward_depth;
8533                self.no_pipe_forward_depth = 0;
8534                let expr = self.parse_expression();
8535                self.no_pipe_forward_depth = saved_no_pipe;
8536                let expr = expr?;
8537                self.expect(&Token::RParen)?;
8538                // Mark this paren as a list-constructor for the `x` operator
8539                // (parse_multiplication compares `self.pos` at the X token to
8540                // this checkpoint). Function-call parens (`f(args)`) don't
8541                // reach this branch; they're parsed by the call machinery.
8542                self.list_construct_close_pos = Some(self.pos);
8543                Ok(expr)
8544            }
8545            Token::LBracket => {
8546                self.advance();
8547                let elems = self.parse_arg_list()?;
8548                self.expect(&Token::RBracket)?;
8549                Ok(Expr {
8550                    kind: ExprKind::ArrayRef(elems),
8551                    line,
8552                })
8553            }
8554            Token::LBrace => {
8555                // Could be hash ref or block — disambiguate
8556                self.advance();
8557                // Try to parse as hash ref: { key => val, ... }
8558                let saved = self.pos;
8559                match self.try_parse_hash_ref() {
8560                    Ok(pairs) => Ok(Expr {
8561                        kind: ExprKind::HashRef(pairs),
8562                        line,
8563                    }),
8564                    Err(_) => {
8565                        self.pos = saved;
8566                        // Parse as block, wrap in code ref
8567                        let mut stmts = Vec::new();
8568                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8569                            if self.eat(&Token::Semicolon) {
8570                                continue;
8571                            }
8572                            stmts.push(self.parse_statement()?);
8573                        }
8574                        self.expect(&Token::RBrace)?;
8575                        Ok(Expr {
8576                            kind: ExprKind::CodeRef {
8577                                params: vec![],
8578                                body: stmts,
8579                            },
8580                            line,
8581                        })
8582                    }
8583                }
8584            }
8585            Token::Diamond => {
8586                self.advance();
8587                Ok(Expr {
8588                    kind: ExprKind::ReadLine(None),
8589                    line,
8590                })
8591            }
8592            Token::ReadLine(handle) => {
8593                self.advance();
8594                Ok(Expr {
8595                    kind: ExprKind::ReadLine(Some(handle)),
8596                    line,
8597                })
8598            }
8599
8600            // Named functions / builtins
8601            Token::ThreadArrow => {
8602                self.advance();
8603                self.parse_thread_macro(line, false)
8604            }
8605            Token::ThreadArrowLast => {
8606                self.advance();
8607                self.parse_thread_macro(line, true)
8608            }
8609            Token::Ident(ref name) => {
8610                let name = name.clone();
8611                // Handle s///
8612                if name.starts_with('\x00') {
8613                    self.advance();
8614                    let parts: Vec<&str> = name.split('\x00').collect();
8615                    if parts.len() >= 4 && parts[1] == "s" {
8616                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8617                        return Ok(Expr {
8618                            kind: ExprKind::Substitution {
8619                                expr: Box::new(Expr {
8620                                    kind: ExprKind::ScalarVar("_".into()),
8621                                    line,
8622                                }),
8623                                pattern: parts[2].to_string(),
8624                                replacement: parts[3].to_string(),
8625                                flags: parts.get(4).unwrap_or(&"").to_string(),
8626                                delim,
8627                            },
8628                            line,
8629                        });
8630                    }
8631                    if parts.len() >= 4 && parts[1] == "tr" {
8632                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8633                        return Ok(Expr {
8634                            kind: ExprKind::Transliterate {
8635                                expr: Box::new(Expr {
8636                                    kind: ExprKind::ScalarVar("_".into()),
8637                                    line,
8638                                }),
8639                                from: parts[2].to_string(),
8640                                to: parts[3].to_string(),
8641                                flags: parts.get(4).unwrap_or(&"").to_string(),
8642                                delim,
8643                            },
8644                            line,
8645                        });
8646                    }
8647                    return Err(self.syntax_err("Unexpected encoded token", line));
8648                }
8649                self.parse_named_expr(name)
8650            }
8651
8652            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
8653            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
8654            Token::Percent => {
8655                self.advance();
8656                match self.peek().clone() {
8657                    Token::Ident(name) => {
8658                        self.advance();
8659                        Ok(Expr {
8660                            kind: ExprKind::HashVar(name),
8661                            line,
8662                        })
8663                    }
8664                    Token::ScalarVar(n) => {
8665                        self.advance();
8666                        Ok(Expr {
8667                            kind: ExprKind::Deref {
8668                                expr: Box::new(Expr {
8669                                    kind: ExprKind::ScalarVar(n),
8670                                    line,
8671                                }),
8672                                kind: Sigil::Hash,
8673                            },
8674                            line,
8675                        })
8676                    }
8677                    Token::LBrace => {
8678                        self.advance();
8679                        let looks_like_pair = matches!(
8680                            self.peek(),
8681                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8682                        ) && matches!(self.peek_at(1), Token::FatArrow);
8683                        let inner = if looks_like_pair {
8684                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8685                            Expr {
8686                                kind: ExprKind::HashRef(pairs),
8687                                line,
8688                            }
8689                        } else {
8690                            self.parse_expression()?
8691                        };
8692                        self.expect(&Token::RBrace)?;
8693                        Ok(Expr {
8694                            kind: ExprKind::Deref {
8695                                expr: Box::new(inner),
8696                                kind: Sigil::Hash,
8697                            },
8698                            line,
8699                        })
8700                    }
8701                    Token::LBracket => {
8702                        self.advance();
8703                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8704                        self.expect(&Token::RBracket)?;
8705                        let href = Expr {
8706                            kind: ExprKind::HashRef(pairs),
8707                            line,
8708                        };
8709                        Ok(Expr {
8710                            kind: ExprKind::Deref {
8711                                expr: Box::new(href),
8712                                kind: Sigil::Hash,
8713                            },
8714                            line,
8715                        })
8716                    }
8717                    tok => Err(self.syntax_err(
8718                        format!(
8719                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
8720                            tok
8721                        ),
8722                        line,
8723                    )),
8724                }
8725            }
8726
8727            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
8728        }
8729    }
8730
8731    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
8732        let line = self.peek_line();
8733        self.advance(); // consume the ident
8734        while self.eat(&Token::PackageSep) {
8735            match self.advance() {
8736                (Token::Ident(part), _) => {
8737                    name = format!("{}::{}", name, part);
8738                }
8739                (tok, err_line) => {
8740                    return Err(self.syntax_err(
8741                        format!("Expected identifier after `::`, got {:?}", tok),
8742                        err_line,
8743                    ));
8744                }
8745            }
8746        }
8747
8748        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
8749        // before `=>` is treated as a string key, matching Perl 5 semantics.
8750        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
8751        // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …) are
8752        // scalar references to the topic / positional / outer-topic chain — they
8753        // must evaluate as the topic value, not the literal name.
8754        if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name) {
8755            return Ok(Expr {
8756                kind: ExprKind::String(name),
8757                line,
8758            });
8759        }
8760
8761        if crate::compat_mode() {
8762            if let Some(ext) = Self::stryke_extension_name(&name) {
8763                if !self.declared_subs.contains(&name) {
8764                    return Err(self.syntax_err(
8765                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
8766                        line,
8767                    ));
8768                }
8769            }
8770        }
8771
8772        // `CORE::length(...)` etc. — strip the explicit core-dispatch prefix so
8773        // the keyword arms below match the bare name and produce the same
8774        // `ExprKind::Length` / `ExprKind::Print` / etc. as the unprefixed form.
8775        // Matches Perl 5's `CORE::` namespace, which routes back to the
8776        // built-in implementation regardless of any same-named user sub.
8777        // (PARITY-011)
8778        if let Some(rest) = name.strip_prefix("CORE::") {
8779            name = rest.to_string();
8780        }
8781
8782        match name.as_str() {
8783            "__FILE__" => Ok(Expr {
8784                kind: ExprKind::MagicConst(MagicConstKind::File),
8785                line,
8786            }),
8787            "__LINE__" => Ok(Expr {
8788                kind: ExprKind::MagicConst(MagicConstKind::Line),
8789                line,
8790            }),
8791            "__SUB__" => Ok(Expr {
8792                kind: ExprKind::MagicConst(MagicConstKind::Sub),
8793                line,
8794            }),
8795            "stdin" => Ok(Expr {
8796                kind: ExprKind::FuncCall {
8797                    name: "stdin".into(),
8798                    args: vec![],
8799                },
8800                line,
8801            }),
8802            "range" => {
8803                let args = self.parse_builtin_args()?;
8804                Ok(Expr {
8805                    kind: ExprKind::FuncCall {
8806                        name: "range".into(),
8807                        args,
8808                    },
8809                    line,
8810                })
8811            }
8812            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
8813            "say" => {
8814                if crate::no_interop_mode() {
8815                    return Err(
8816                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
8817                    );
8818                }
8819                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
8820            }
8821            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
8822            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
8823            "die" => {
8824                let args = self.parse_list_until_terminator()?;
8825                Ok(Expr {
8826                    kind: ExprKind::Die(args),
8827                    line,
8828                })
8829            }
8830            "warn" => {
8831                let args = self.parse_list_until_terminator()?;
8832                Ok(Expr {
8833                    kind: ExprKind::Warn(args),
8834                    line,
8835                })
8836            }
8837            // `croak` / `confess` — `Carp` builtins available without `use Carp`
8838            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
8839            // `die` — TODO: croak should report caller's file/line, confess
8840            // should append a full stack trace.
8841            "croak" | "confess" => {
8842                let args = self.parse_list_until_terminator()?;
8843                Ok(Expr {
8844                    kind: ExprKind::Die(args),
8845                    line,
8846                })
8847            }
8848            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
8849            "carp" | "cluck" => {
8850                let args = self.parse_list_until_terminator()?;
8851                Ok(Expr {
8852                    kind: ExprKind::Warn(args),
8853                    line,
8854                })
8855            }
8856            "chomp" => {
8857                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8858                    return Ok(e);
8859                }
8860                let a = self.parse_one_arg_or_default()?;
8861                Ok(Expr {
8862                    kind: ExprKind::Chomp(Box::new(a)),
8863                    line,
8864                })
8865            }
8866            "chop" => {
8867                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8868                    return Ok(e);
8869                }
8870                let a = self.parse_one_arg_or_default()?;
8871                Ok(Expr {
8872                    kind: ExprKind::Chop(Box::new(a)),
8873                    line,
8874                })
8875            }
8876            "length" => {
8877                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8878                    return Ok(e);
8879                }
8880                let a = self.parse_one_arg_or_default()?;
8881                Ok(Expr {
8882                    kind: ExprKind::Length(Box::new(a)),
8883                    line,
8884                })
8885            }
8886            "defined" => {
8887                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8888                    return Ok(e);
8889                }
8890                // Named-unary precedence: `defined X && Y` is `(defined X) && Y`,
8891                // not `defined(X && Y)`. The default `parse_one_arg_or_default`
8892                // path is greedy (calls `parse_assign_expr_stop_at_pipe`), which
8893                // would let `&&` bind into the argument and silently make
8894                // `defined $h{k} && $h{k} > 0`-style guards always-true when the
8895                // hash element existed. `parse_named_unary_arg` stops at shift
8896                // level so logical operators stay outside.
8897                let a = if matches!(
8898                    self.peek(),
8899                    Token::Semicolon
8900                        | Token::RBrace
8901                        | Token::RParen
8902                        | Token::RBracket
8903                        | Token::Eof
8904                        | Token::Comma
8905                        | Token::FatArrow
8906                        | Token::PipeForward
8907                        | Token::Question
8908                        | Token::Colon
8909                        | Token::NumEq
8910                        | Token::NumNe
8911                        | Token::NumLt
8912                        | Token::NumGt
8913                        | Token::NumLe
8914                        | Token::NumGe
8915                        | Token::Spaceship
8916                        | Token::StrEq
8917                        | Token::StrNe
8918                        | Token::StrLt
8919                        | Token::StrGt
8920                        | Token::StrLe
8921                        | Token::StrGe
8922                        | Token::StrCmp
8923                        | Token::LogAnd
8924                        | Token::LogOr
8925                        | Token::LogNot
8926                        | Token::LogAndWord
8927                        | Token::LogOrWord
8928                        | Token::LogNotWord
8929                        | Token::DefinedOr
8930                        | Token::Range
8931                        | Token::RangeExclusive
8932                        | Token::Assign
8933                        | Token::PlusAssign
8934                        | Token::MinusAssign
8935                        | Token::MulAssign
8936                        | Token::DivAssign
8937                        | Token::ModAssign
8938                        | Token::PowAssign
8939                        | Token::DotAssign
8940                        | Token::AndAssign
8941                        | Token::OrAssign
8942                        | Token::XorAssign
8943                        | Token::DefinedOrAssign
8944                        | Token::ShiftLeftAssign
8945                        | Token::ShiftRightAssign
8946                        | Token::BitAndAssign
8947                        | Token::BitOrAssign
8948                ) {
8949                    Expr {
8950                        kind: ExprKind::ScalarVar("_".into()),
8951                        line: self.peek_line(),
8952                    }
8953                } else if matches!(self.peek(), Token::LParen)
8954                    && matches!(self.peek_at(1), Token::RParen)
8955                {
8956                    let pl = self.peek_line();
8957                    self.advance();
8958                    self.advance();
8959                    Expr {
8960                        kind: ExprKind::ScalarVar("_".into()),
8961                        line: pl,
8962                    }
8963                } else {
8964                    self.parse_named_unary_arg()?
8965                };
8966                Ok(Expr {
8967                    kind: ExprKind::Defined(Box::new(a)),
8968                    line,
8969                })
8970            }
8971            "ref" => {
8972                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8973                    return Ok(e);
8974                }
8975                let a = self.parse_one_arg_or_default()?;
8976                Ok(Expr {
8977                    kind: ExprKind::Ref(Box::new(a)),
8978                    line,
8979                })
8980            }
8981            "undef" => {
8982                // `undef $var` sets `$var` to undef — but a variable on a new line
8983                // is a separate statement (implicit semicolon), not an argument.
8984                if self.peek_line() == self.prev_line()
8985                    && matches!(
8986                        self.peek(),
8987                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
8988                    )
8989                {
8990                    let target = self.parse_primary()?;
8991                    return Ok(Expr {
8992                        kind: ExprKind::Assign {
8993                            target: Box::new(target),
8994                            value: Box::new(Expr {
8995                                kind: ExprKind::Undef,
8996                                line,
8997                            }),
8998                        },
8999                        line,
9000                    });
9001                }
9002                Ok(Expr {
9003                    kind: ExprKind::Undef,
9004                    line,
9005                })
9006            }
9007            "scalar" => {
9008                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9009                    return Ok(e);
9010                }
9011                if crate::no_interop_mode() {
9012                    return Err(self.syntax_err(
9013                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
9014                        line,
9015                    ));
9016                }
9017                let a = self.parse_one_arg_or_default()?;
9018                Ok(Expr {
9019                    kind: ExprKind::ScalarContext(Box::new(a)),
9020                    line,
9021                })
9022            }
9023            "abs" => {
9024                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9025                    return Ok(e);
9026                }
9027                let a = self.parse_one_arg_or_default()?;
9028                Ok(Expr {
9029                    kind: ExprKind::Abs(Box::new(a)),
9030                    line,
9031                })
9032            }
9033            // stryke unary numeric extensions — treat like `abs` so a bare
9034            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
9035            // call with implicit `$_` rather than falling through to the
9036            // generic `Bareword` arm (which stringifies to `"inc"`).
9037            "inc" | "dec" => {
9038                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9039                    return Ok(e);
9040                }
9041                let a = self.parse_one_arg_or_default()?;
9042                Ok(Expr {
9043                    kind: ExprKind::FuncCall {
9044                        name,
9045                        args: vec![a],
9046                    },
9047                    line,
9048                })
9049            }
9050            "int" => {
9051                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9052                    return Ok(e);
9053                }
9054                let a = self.parse_one_arg_or_default()?;
9055                Ok(Expr {
9056                    kind: ExprKind::Int(Box::new(a)),
9057                    line,
9058                })
9059            }
9060            "sqrt" => {
9061                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9062                    return Ok(e);
9063                }
9064                let a = self.parse_one_arg_or_default()?;
9065                Ok(Expr {
9066                    kind: ExprKind::Sqrt(Box::new(a)),
9067                    line,
9068                })
9069            }
9070            "sin" => {
9071                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9072                    return Ok(e);
9073                }
9074                let a = self.parse_one_arg_or_default()?;
9075                Ok(Expr {
9076                    kind: ExprKind::Sin(Box::new(a)),
9077                    line,
9078                })
9079            }
9080            "cos" => {
9081                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9082                    return Ok(e);
9083                }
9084                let a = self.parse_one_arg_or_default()?;
9085                Ok(Expr {
9086                    kind: ExprKind::Cos(Box::new(a)),
9087                    line,
9088                })
9089            }
9090            "atan2" => {
9091                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9092                    return Ok(e);
9093                }
9094                let args = self.parse_builtin_args()?;
9095                if args.len() != 2 {
9096                    return Err(self.syntax_err("atan2 requires two arguments", line));
9097                }
9098                Ok(Expr {
9099                    kind: ExprKind::Atan2 {
9100                        y: Box::new(args[0].clone()),
9101                        x: Box::new(args[1].clone()),
9102                    },
9103                    line,
9104                })
9105            }
9106            "exp" => {
9107                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9108                    return Ok(e);
9109                }
9110                let a = self.parse_one_arg_or_default()?;
9111                Ok(Expr {
9112                    kind: ExprKind::Exp(Box::new(a)),
9113                    line,
9114                })
9115            }
9116            "log" => {
9117                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9118                    return Ok(e);
9119                }
9120                let a = self.parse_one_arg_or_default()?;
9121                Ok(Expr {
9122                    kind: ExprKind::Log(Box::new(a)),
9123                    line,
9124                })
9125            }
9126            "input" => {
9127                let args = if matches!(
9128                    self.peek(),
9129                    Token::Semicolon
9130                        | Token::RBrace
9131                        | Token::RParen
9132                        | Token::Eof
9133                        | Token::Comma
9134                        | Token::PipeForward
9135                ) {
9136                    vec![]
9137                } else if matches!(self.peek(), Token::LParen) {
9138                    self.advance();
9139                    if matches!(self.peek(), Token::RParen) {
9140                        self.advance();
9141                        vec![]
9142                    } else {
9143                        let a = self.parse_expression()?;
9144                        self.expect(&Token::RParen)?;
9145                        vec![a]
9146                    }
9147                } else {
9148                    let a = self.parse_one_arg()?;
9149                    vec![a]
9150                };
9151                Ok(Expr {
9152                    kind: ExprKind::FuncCall {
9153                        name: "input".to_string(),
9154                        args,
9155                    },
9156                    line,
9157                })
9158            }
9159            "rand" => {
9160                if matches!(
9161                    self.peek(),
9162                    Token::Semicolon
9163                        | Token::RBrace
9164                        | Token::RParen
9165                        | Token::Eof
9166                        | Token::Comma
9167                        | Token::PipeForward
9168                ) {
9169                    Ok(Expr {
9170                        kind: ExprKind::Rand(None),
9171                        line,
9172                    })
9173                } else if matches!(self.peek(), Token::LParen) {
9174                    self.advance();
9175                    if matches!(self.peek(), Token::RParen) {
9176                        self.advance();
9177                        Ok(Expr {
9178                            kind: ExprKind::Rand(None),
9179                            line,
9180                        })
9181                    } else {
9182                        let a = self.parse_expression()?;
9183                        self.expect(&Token::RParen)?;
9184                        Ok(Expr {
9185                            kind: ExprKind::Rand(Some(Box::new(a))),
9186                            line,
9187                        })
9188                    }
9189                } else {
9190                    let a = self.parse_one_arg()?;
9191                    Ok(Expr {
9192                        kind: ExprKind::Rand(Some(Box::new(a))),
9193                        line,
9194                    })
9195                }
9196            }
9197            "srand" => {
9198                if matches!(
9199                    self.peek(),
9200                    Token::Semicolon
9201                        | Token::RBrace
9202                        | Token::RParen
9203                        | Token::Eof
9204                        | Token::Comma
9205                        | Token::PipeForward
9206                ) {
9207                    Ok(Expr {
9208                        kind: ExprKind::Srand(None),
9209                        line,
9210                    })
9211                } else if matches!(self.peek(), Token::LParen) {
9212                    self.advance();
9213                    if matches!(self.peek(), Token::RParen) {
9214                        self.advance();
9215                        Ok(Expr {
9216                            kind: ExprKind::Srand(None),
9217                            line,
9218                        })
9219                    } else {
9220                        let a = self.parse_expression()?;
9221                        self.expect(&Token::RParen)?;
9222                        Ok(Expr {
9223                            kind: ExprKind::Srand(Some(Box::new(a))),
9224                            line,
9225                        })
9226                    }
9227                } else {
9228                    let a = self.parse_one_arg()?;
9229                    Ok(Expr {
9230                        kind: ExprKind::Srand(Some(Box::new(a))),
9231                        line,
9232                    })
9233                }
9234            }
9235            "hex" => {
9236                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9237                    return Ok(e);
9238                }
9239                let a = self.parse_one_arg_or_default()?;
9240                Ok(Expr {
9241                    kind: ExprKind::Hex(Box::new(a)),
9242                    line,
9243                })
9244            }
9245            "oct" => {
9246                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9247                    return Ok(e);
9248                }
9249                let a = self.parse_one_arg_or_default()?;
9250                Ok(Expr {
9251                    kind: ExprKind::Oct(Box::new(a)),
9252                    line,
9253                })
9254            }
9255            "chr" => {
9256                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9257                    return Ok(e);
9258                }
9259                let a = self.parse_one_arg_or_default()?;
9260                Ok(Expr {
9261                    kind: ExprKind::Chr(Box::new(a)),
9262                    line,
9263                })
9264            }
9265            "ord" => {
9266                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9267                    return Ok(e);
9268                }
9269                let a = self.parse_one_arg_or_default()?;
9270                Ok(Expr {
9271                    kind: ExprKind::Ord(Box::new(a)),
9272                    line,
9273                })
9274            }
9275            "lc" => {
9276                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9277                    return Ok(e);
9278                }
9279                let a = self.parse_one_arg_or_default()?;
9280                Ok(Expr {
9281                    kind: ExprKind::Lc(Box::new(a)),
9282                    line,
9283                })
9284            }
9285            "uc" => {
9286                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9287                    return Ok(e);
9288                }
9289                let a = self.parse_one_arg_or_default()?;
9290                Ok(Expr {
9291                    kind: ExprKind::Uc(Box::new(a)),
9292                    line,
9293                })
9294            }
9295            "lcfirst" => {
9296                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9297                    return Ok(e);
9298                }
9299                let a = self.parse_one_arg_or_default()?;
9300                Ok(Expr {
9301                    kind: ExprKind::Lcfirst(Box::new(a)),
9302                    line,
9303                })
9304            }
9305            "ucfirst" => {
9306                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9307                    return Ok(e);
9308                }
9309                let a = self.parse_one_arg_or_default()?;
9310                Ok(Expr {
9311                    kind: ExprKind::Ucfirst(Box::new(a)),
9312                    line,
9313                })
9314            }
9315            "fc" => {
9316                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9317                    return Ok(e);
9318                }
9319                let a = self.parse_one_arg_or_default()?;
9320                Ok(Expr {
9321                    kind: ExprKind::Fc(Box::new(a)),
9322                    line,
9323                })
9324            }
9325            "crypt" => {
9326                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9327                    return Ok(e);
9328                }
9329                let args = self.parse_builtin_args()?;
9330                if args.len() != 2 {
9331                    return Err(self.syntax_err("crypt requires two arguments", line));
9332                }
9333                Ok(Expr {
9334                    kind: ExprKind::Crypt {
9335                        plaintext: Box::new(args[0].clone()),
9336                        salt: Box::new(args[1].clone()),
9337                    },
9338                    line,
9339                })
9340            }
9341            "pos" => {
9342                if matches!(
9343                    self.peek(),
9344                    Token::Semicolon
9345                        | Token::RBrace
9346                        | Token::RParen
9347                        | Token::Eof
9348                        | Token::Comma
9349                        | Token::PipeForward
9350                ) {
9351                    Ok(Expr {
9352                        kind: ExprKind::Pos(None),
9353                        line,
9354                    })
9355                } else if matches!(self.peek(), Token::Assign) {
9356                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
9357                    self.advance();
9358                    let rhs = self.parse_assign_expr()?;
9359                    Ok(Expr {
9360                        kind: ExprKind::Assign {
9361                            target: Box::new(Expr {
9362                                kind: ExprKind::Pos(Some(Box::new(Expr {
9363                                    kind: ExprKind::ScalarVar("_".into()),
9364                                    line,
9365                                }))),
9366                                line,
9367                            }),
9368                            value: Box::new(rhs),
9369                        },
9370                        line,
9371                    })
9372                } else if matches!(self.peek(), Token::LParen) {
9373                    self.advance();
9374                    if matches!(self.peek(), Token::RParen) {
9375                        self.advance();
9376                        Ok(Expr {
9377                            kind: ExprKind::Pos(None),
9378                            line,
9379                        })
9380                    } else {
9381                        let a = self.parse_expression()?;
9382                        self.expect(&Token::RParen)?;
9383                        Ok(Expr {
9384                            kind: ExprKind::Pos(Some(Box::new(a))),
9385                            line,
9386                        })
9387                    }
9388                } else {
9389                    let saved = self.pos;
9390                    let subj = self.parse_unary()?;
9391                    if matches!(self.peek(), Token::Assign) {
9392                        self.advance();
9393                        let rhs = self.parse_assign_expr()?;
9394                        Ok(Expr {
9395                            kind: ExprKind::Assign {
9396                                target: Box::new(Expr {
9397                                    kind: ExprKind::Pos(Some(Box::new(subj))),
9398                                    line,
9399                                }),
9400                                value: Box::new(rhs),
9401                            },
9402                            line,
9403                        })
9404                    } else {
9405                        self.pos = saved;
9406                        let a = self.parse_one_arg()?;
9407                        Ok(Expr {
9408                            kind: ExprKind::Pos(Some(Box::new(a))),
9409                            line,
9410                        })
9411                    }
9412                }
9413            }
9414            "study" => {
9415                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9416                    return Ok(e);
9417                }
9418                let a = self.parse_one_arg_or_default()?;
9419                Ok(Expr {
9420                    kind: ExprKind::Study(Box::new(a)),
9421                    line,
9422                })
9423            }
9424            "push" => {
9425                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9426                    return Ok(e);
9427                }
9428                let args = self.parse_builtin_args()?;
9429                let (first, rest) = args
9430                    .split_first()
9431                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
9432                Ok(Expr {
9433                    kind: ExprKind::Push {
9434                        array: Box::new(first.clone()),
9435                        values: rest.to_vec(),
9436                    },
9437                    line,
9438                })
9439            }
9440            "pop" => {
9441                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9442                    return Ok(e);
9443                }
9444                let a = self.parse_one_arg_or_argv()?;
9445                Ok(Expr {
9446                    kind: ExprKind::Pop(Box::new(a)),
9447                    line,
9448                })
9449            }
9450            "shift" => {
9451                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9452                    return Ok(e);
9453                }
9454                let a = self.parse_one_arg_or_argv()?;
9455                Ok(Expr {
9456                    kind: ExprKind::Shift(Box::new(a)),
9457                    line,
9458                })
9459            }
9460            "unshift" => {
9461                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9462                    return Ok(e);
9463                }
9464                let args = self.parse_builtin_args()?;
9465                let (first, rest) = args
9466                    .split_first()
9467                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
9468                Ok(Expr {
9469                    kind: ExprKind::Unshift {
9470                        array: Box::new(first.clone()),
9471                        values: rest.to_vec(),
9472                    },
9473                    line,
9474                })
9475            }
9476            "splice" => {
9477                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9478                    return Ok(e);
9479                }
9480                let args = self.parse_builtin_args()?;
9481                let mut iter = args.into_iter();
9482                let array = Box::new(
9483                    iter.next()
9484                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
9485                );
9486                let offset = iter.next().map(Box::new);
9487                let length = iter.next().map(Box::new);
9488                let replacement: Vec<Expr> = iter.collect();
9489                Ok(Expr {
9490                    kind: ExprKind::Splice {
9491                        array,
9492                        offset,
9493                        length,
9494                        replacement,
9495                    },
9496                    line,
9497                })
9498            }
9499            // `splice_last(@a, off[, n])` is the stryke spelling of Perl's
9500            // `scalar splice(@a, off, n)` — returns the LAST removed element
9501            // (or undef if nothing was removed). Desugars to `tail(splice(...))`
9502            // so the array is still mutated in place.
9503            "splice_last" | "splice1" | "spl_last" => {
9504                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9505                    return Ok(e);
9506                }
9507                let args = self.parse_builtin_args()?;
9508                let mut iter = args.into_iter();
9509                let array = Box::new(
9510                    iter.next()
9511                        .ok_or_else(|| self.syntax_err("splice_last requires arguments", line))?,
9512                );
9513                let offset = iter.next().map(Box::new);
9514                let length = iter.next().map(Box::new);
9515                let replacement: Vec<Expr> = iter.collect();
9516                let splice_expr = Expr {
9517                    kind: ExprKind::Splice {
9518                        array,
9519                        offset,
9520                        length,
9521                        replacement,
9522                    },
9523                    line,
9524                };
9525                Ok(Expr {
9526                    kind: ExprKind::FuncCall {
9527                        name: "tail".to_string(),
9528                        args: vec![splice_expr],
9529                    },
9530                    line,
9531                })
9532            }
9533            "delete" => {
9534                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9535                    return Ok(e);
9536                }
9537                let a = self.parse_postfix()?;
9538                Ok(Expr {
9539                    kind: ExprKind::Delete(Box::new(a)),
9540                    line,
9541                })
9542            }
9543            "exists" => {
9544                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9545                    return Ok(e);
9546                }
9547                let a = self.parse_postfix()?;
9548                Ok(Expr {
9549                    kind: ExprKind::Exists(Box::new(a)),
9550                    line,
9551                })
9552            }
9553            "keys" => {
9554                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9555                    return Ok(e);
9556                }
9557                let a = self.parse_one_arg_or_default()?;
9558                Ok(Expr {
9559                    kind: ExprKind::Keys(Box::new(a)),
9560                    line,
9561                })
9562            }
9563            "values" => {
9564                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9565                    return Ok(e);
9566                }
9567                let a = self.parse_one_arg_or_default()?;
9568                Ok(Expr {
9569                    kind: ExprKind::Values(Box::new(a)),
9570                    line,
9571                })
9572            }
9573            "each" => {
9574                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9575                    return Ok(e);
9576                }
9577                let a = self.parse_one_arg_or_default()?;
9578                Ok(Expr {
9579                    kind: ExprKind::Each(Box::new(a)),
9580                    line,
9581                })
9582            }
9583            "fore" | "e" | "ep" => {
9584                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
9585                if matches!(self.peek(), Token::LBrace) {
9586                    let (block, list) = self.parse_block_list()?;
9587                    Ok(Expr {
9588                        kind: ExprKind::ForEachExpr {
9589                            block,
9590                            list: Box::new(list),
9591                        },
9592                        line,
9593                    })
9594                } else if self.in_pipe_rhs() {
9595                    // `|> ep` — bare ep at end of pipe: default to `say $_`
9596                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
9597                    let is_terminal = matches!(
9598                        self.peek(),
9599                        Token::Semicolon
9600                            | Token::RParen
9601                            | Token::Eof
9602                            | Token::PipeForward
9603                            | Token::RBrace
9604                    );
9605                    let block = if name == "ep" && is_terminal {
9606                        vec![Statement {
9607                            label: None,
9608                            kind: StmtKind::Expression(Expr {
9609                                kind: ExprKind::Say {
9610                                    handle: None,
9611                                    args: vec![Expr {
9612                                        kind: ExprKind::ScalarVar("_".into()),
9613                                        line,
9614                                    }],
9615                                },
9616                                line,
9617                            }),
9618                            line,
9619                        }]
9620                    } else {
9621                        let expr = self.parse_assign_expr_stop_at_pipe()?;
9622                        let expr = Self::lift_bareword_to_topic_call(expr);
9623                        vec![Statement {
9624                            label: None,
9625                            kind: StmtKind::Expression(expr),
9626                            line,
9627                        }]
9628                    };
9629                    let list = self.pipe_placeholder_list(line);
9630                    Ok(Expr {
9631                        kind: ExprKind::ForEachExpr {
9632                            block,
9633                            list: Box::new(list),
9634                        },
9635                        line,
9636                    })
9637                } else {
9638                    // Two surface forms share this branch:
9639                    //   `fore EXPR, LIST` — comma form (explicit per-item EXPR + list)
9640                    //   `ep LIST`         — list-only form: print each item with `say $_`
9641                    // We disambiguate by peeking after the first parsed expression:
9642                    // if the next token is a comma we're in the EXPR-then-LIST form;
9643                    // otherwise the first parse *was* the LIST and we default the
9644                    // block to `say $_` (only for `ep` — `fore`/`e` keep their
9645                    // explicit-expression contract).
9646                    let expr = self.parse_assign_expr()?;
9647                    let expr = Self::lift_bareword_to_topic_call(expr);
9648                    if !matches!(self.peek(), Token::Comma) && name == "ep" {
9649                        let block = vec![Statement {
9650                            label: None,
9651                            kind: StmtKind::Expression(Expr {
9652                                kind: ExprKind::Say {
9653                                    handle: None,
9654                                    args: vec![Expr {
9655                                        kind: ExprKind::ScalarVar("_".into()),
9656                                        line,
9657                                    }],
9658                                },
9659                                line,
9660                            }),
9661                            line,
9662                        }];
9663                        return Ok(Expr {
9664                            kind: ExprKind::ForEachExpr {
9665                                block,
9666                                list: Box::new(expr),
9667                            },
9668                            line,
9669                        });
9670                    }
9671                    self.expect(&Token::Comma)?;
9672                    let list_parts = self.parse_list_until_terminator()?;
9673                    let list_expr = if list_parts.len() == 1 {
9674                        list_parts.into_iter().next().unwrap()
9675                    } else {
9676                        Expr {
9677                            kind: ExprKind::List(list_parts),
9678                            line,
9679                        }
9680                    };
9681                    let block = vec![Statement {
9682                        label: None,
9683                        kind: StmtKind::Expression(expr),
9684                        line,
9685                    }];
9686                    Ok(Expr {
9687                        kind: ExprKind::ForEachExpr {
9688                            block,
9689                            list: Box::new(list_expr),
9690                        },
9691                        line,
9692                    })
9693                }
9694            }
9695            "rev" => {
9696                // `rev` — context-aware reverse: string in scalar, list in list context.
9697                // List-operator precedence (so `rev 1..3` parses as `rev(1..3)`, not
9698                // `(rev 1)..3`). Defaults to $_ when no argument given.
9699                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
9700                // RBrace means we're inside a block like `map { rev }` - use $_ default.
9701                let a = if self.in_pipe_rhs()
9702                    && matches!(
9703                        self.peek(),
9704                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
9705                    ) {
9706                    self.pipe_placeholder_list(line)
9707                } else if matches!(
9708                    self.peek(),
9709                    Token::Semicolon
9710                        | Token::RBrace
9711                        | Token::RParen
9712                        | Token::RBracket
9713                        | Token::Eof
9714                        | Token::Comma
9715                        | Token::FatArrow
9716                        | Token::PipeForward
9717                ) {
9718                    Expr {
9719                        kind: ExprKind::ScalarVar("_".into()),
9720                        line: self.peek_line(),
9721                    }
9722                } else if matches!(self.peek(), Token::LParen)
9723                    && matches!(self.peek_at(1), Token::RParen)
9724                {
9725                    // `rev()` — empty parens default to `$_` (matches Perl's
9726                    // `length()` / `uc()` etc. and the `|> rev()` pipe form).
9727                    let pl = self.peek_line();
9728                    self.advance(); // (
9729                    self.advance(); // )
9730                    Expr {
9731                        kind: ExprKind::ScalarVar("_".into()),
9732                        line: pl,
9733                    }
9734                } else {
9735                    self.parse_one_arg()?
9736                };
9737                Ok(Expr {
9738                    kind: ExprKind::Rev(Box::new(a)),
9739                    line,
9740                })
9741            }
9742            "reverse" => {
9743                if crate::no_interop_mode() {
9744                    return Err(self.syntax_err(
9745                        "stryke uses `rev` instead of `reverse` (--no-interop)",
9746                        line,
9747                    ));
9748                }
9749                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9750                let a = if self.in_pipe_rhs()
9751                    && matches!(
9752                        self.peek(),
9753                        Token::Semicolon
9754                            | Token::RBrace
9755                            | Token::RParen
9756                            | Token::Eof
9757                            | Token::PipeForward
9758                    ) {
9759                    self.pipe_placeholder_list(line)
9760                } else {
9761                    self.parse_one_arg()?
9762                };
9763                Ok(Expr {
9764                    kind: ExprKind::ReverseExpr(Box::new(a)),
9765                    line,
9766                })
9767            }
9768            "reversed" | "rv" => {
9769                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9770                let a = if self.in_pipe_rhs()
9771                    && matches!(
9772                        self.peek(),
9773                        Token::Semicolon
9774                            | Token::RBrace
9775                            | Token::RParen
9776                            | Token::Eof
9777                            | Token::PipeForward
9778                    ) {
9779                    self.pipe_placeholder_list(line)
9780                } else {
9781                    self.parse_one_arg()?
9782                };
9783                Ok(Expr {
9784                    kind: ExprKind::Rev(Box::new(a)),
9785                    line,
9786                })
9787            }
9788            "join" => {
9789                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9790                    return Ok(e);
9791                }
9792                let args = self.parse_builtin_args()?;
9793                if args.is_empty() {
9794                    return Err(self.syntax_err("join requires separator and list", line));
9795                }
9796                // `@list |> join(",")` — list slot is filled by the piped LHS.
9797                if args.len() < 2 && !self.in_pipe_rhs() {
9798                    return Err(self.syntax_err("join requires separator and list", line));
9799                }
9800                Ok(Expr {
9801                    kind: ExprKind::JoinExpr {
9802                        separator: Box::new(args[0].clone()),
9803                        list: Box::new(Expr {
9804                            kind: ExprKind::List(args[1..].to_vec()),
9805                            line,
9806                        }),
9807                    },
9808                    line,
9809                })
9810            }
9811            "split" => {
9812                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9813                    return Ok(e);
9814                }
9815                let args = self.parse_builtin_args()?;
9816                let pattern = args.first().cloned().unwrap_or(Expr {
9817                    kind: ExprKind::String(" ".into()),
9818                    line,
9819                });
9820                let string = args.get(1).cloned().unwrap_or(Expr {
9821                    kind: ExprKind::ScalarVar("_".into()),
9822                    line,
9823                });
9824                let limit = args.get(2).cloned().map(Box::new);
9825                Ok(Expr {
9826                    kind: ExprKind::SplitExpr {
9827                        pattern: Box::new(pattern),
9828                        string: Box::new(string),
9829                        limit,
9830                    },
9831                    line,
9832                })
9833            }
9834            "substr" => {
9835                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9836                    return Ok(e);
9837                }
9838                let args = self.parse_builtin_args()?;
9839                Ok(Expr {
9840                    kind: ExprKind::Substr {
9841                        string: Box::new(args[0].clone()),
9842                        offset: Box::new(args[1].clone()),
9843                        length: args.get(2).cloned().map(Box::new),
9844                        replacement: args.get(3).cloned().map(Box::new),
9845                    },
9846                    line,
9847                })
9848            }
9849            "index" => {
9850                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9851                    return Ok(e);
9852                }
9853                let args = self.parse_builtin_args()?;
9854                Ok(Expr {
9855                    kind: ExprKind::Index {
9856                        string: Box::new(args[0].clone()),
9857                        substr: Box::new(args[1].clone()),
9858                        position: args.get(2).cloned().map(Box::new),
9859                    },
9860                    line,
9861                })
9862            }
9863            "rindex" => {
9864                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9865                    return Ok(e);
9866                }
9867                let args = self.parse_builtin_args()?;
9868                Ok(Expr {
9869                    kind: ExprKind::Rindex {
9870                        string: Box::new(args[0].clone()),
9871                        substr: Box::new(args[1].clone()),
9872                        position: args.get(2).cloned().map(Box::new),
9873                    },
9874                    line,
9875                })
9876            }
9877            "sprintf" => {
9878                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9879                    return Ok(e);
9880                }
9881                let args = self.parse_builtin_args()?;
9882                let (first, rest) = args
9883                    .split_first()
9884                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
9885                Ok(Expr {
9886                    kind: ExprKind::Sprintf {
9887                        format: Box::new(first.clone()),
9888                        args: rest.to_vec(),
9889                    },
9890                    line,
9891                })
9892            }
9893            "map" | "flat_map" | "maps" | "flat_maps" => {
9894                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
9895                let stream = matches!(name.as_str(), "maps" | "flat_maps");
9896                if matches!(self.peek(), Token::LBrace) {
9897                    let (block, list) = self.parse_block_list()?;
9898                    Ok(Expr {
9899                        kind: ExprKind::MapExpr {
9900                            block,
9901                            list: Box::new(list),
9902                            flatten_array_refs,
9903                            stream,
9904                        },
9905                        line,
9906                    })
9907                } else {
9908                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9909                    // Lift bareword to FuncCall($_) so `map sha512, @list`
9910                    // calls sha512($_) for each element instead of stringifying.
9911                    let expr = Self::lift_bareword_to_topic_call(expr);
9912                    let list_expr = if self.in_pipe_rhs()
9913                        && matches!(
9914                            self.peek(),
9915                            Token::Semicolon
9916                                | Token::RBrace
9917                                | Token::RParen
9918                                | Token::Eof
9919                                | Token::PipeForward
9920                        ) {
9921                        self.pipe_placeholder_list(line)
9922                    } else {
9923                        self.expect(&Token::Comma)?;
9924                        let list_parts = self.parse_list_until_terminator()?;
9925                        if list_parts.len() == 1 {
9926                            list_parts.into_iter().next().unwrap()
9927                        } else {
9928                            Expr {
9929                                kind: ExprKind::List(list_parts),
9930                                line,
9931                            }
9932                        }
9933                    };
9934                    Ok(Expr {
9935                        kind: ExprKind::MapExprComma {
9936                            expr: Box::new(expr),
9937                            list: Box::new(list_expr),
9938                            flatten_array_refs,
9939                            stream,
9940                        },
9941                        line,
9942                    })
9943                }
9944            }
9945            "cond" => {
9946                if crate::compat_mode() {
9947                    return Err(self
9948                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
9949                }
9950                self.parse_cond_expr(line)
9951            }
9952            "match" => {
9953                if crate::compat_mode() {
9954                    return Err(self.syntax_err(
9955                        "algebraic `match` is a stryke extension (disabled by --compat)",
9956                        line,
9957                    ));
9958                }
9959                self.parse_algebraic_match_expr(line)
9960            }
9961            "grep" | "greps" | "filter" | "fi" | "find_all" => {
9962                let keyword = match name.as_str() {
9963                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
9964                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
9965                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
9966                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
9967                    _ => unreachable!(),
9968                };
9969                if matches!(self.peek(), Token::LBrace) {
9970                    let (block, list) = self.parse_block_list()?;
9971                    Ok(Expr {
9972                        kind: ExprKind::GrepExpr {
9973                            block,
9974                            list: Box::new(list),
9975                            keyword,
9976                        },
9977                        line,
9978                    })
9979                } else {
9980                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9981                    if self.in_pipe_rhs()
9982                        && matches!(
9983                            self.peek(),
9984                            Token::Semicolon
9985                                | Token::RBrace
9986                                | Token::RParen
9987                                | Token::Eof
9988                                | Token::PipeForward
9989                        )
9990                    {
9991                        // Pipe-RHS blockless form: `|> grep EXPR`
9992                        // For literals, desugar to `$_ eq/== EXPR` so
9993                        // `|> filter 't'` keeps only elements equal to 't'.
9994                        // For regexes, desugar to `$_ =~ EXPR`.
9995                        let list = self.pipe_placeholder_list(line);
9996                        let topic = Expr {
9997                            kind: ExprKind::ScalarVar("_".into()),
9998                            line,
9999                        };
10000                        let test = match &expr.kind {
10001                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
10002                                kind: ExprKind::BinOp {
10003                                    op: BinOp::NumEq,
10004                                    left: Box::new(topic),
10005                                    right: Box::new(expr),
10006                                },
10007                                line,
10008                            },
10009                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
10010                                kind: ExprKind::BinOp {
10011                                    op: BinOp::StrEq,
10012                                    left: Box::new(topic),
10013                                    right: Box::new(expr),
10014                                },
10015                                line,
10016                            },
10017                            ExprKind::Regex { .. } => Expr {
10018                                kind: ExprKind::BinOp {
10019                                    op: BinOp::BindMatch,
10020                                    left: Box::new(topic),
10021                                    right: Box::new(expr),
10022                                },
10023                                line,
10024                            },
10025                            _ => {
10026                                // Non-literal (e.g. `defined`): lift bareword to call
10027                                Self::lift_bareword_to_topic_call(expr)
10028                            }
10029                        };
10030                        let block = vec![Statement {
10031                            label: None,
10032                            kind: StmtKind::Expression(test),
10033                            line,
10034                        }];
10035                        Ok(Expr {
10036                            kind: ExprKind::GrepExpr {
10037                                block,
10038                                list: Box::new(list),
10039                                keyword,
10040                            },
10041                            line,
10042                        })
10043                    } else {
10044                        let expr = Self::lift_bareword_to_topic_call(expr);
10045                        self.expect(&Token::Comma)?;
10046                        let list_parts = self.parse_list_until_terminator()?;
10047                        let list_expr = if list_parts.len() == 1 {
10048                            list_parts.into_iter().next().unwrap()
10049                        } else {
10050                            Expr {
10051                                kind: ExprKind::List(list_parts),
10052                                line,
10053                            }
10054                        };
10055                        Ok(Expr {
10056                            kind: ExprKind::GrepExprComma {
10057                                expr: Box::new(expr),
10058                                list: Box::new(list_expr),
10059                                keyword,
10060                            },
10061                            line,
10062                        })
10063                    }
10064                }
10065            }
10066            "sort" => {
10067                use crate::ast::SortComparator;
10068                if matches!(self.peek(), Token::LBrace) {
10069                    let block = self.parse_block()?;
10070                    let block_end_line = self.prev_line();
10071                    let _ = self.eat(&Token::Comma);
10072                    let list = if self.in_pipe_rhs()
10073                        && (matches!(
10074                            self.peek(),
10075                            Token::Semicolon
10076                                | Token::RBrace
10077                                | Token::RParen
10078                                | Token::Eof
10079                                | Token::PipeForward
10080                        ) || self.peek_line() > block_end_line)
10081                    {
10082                        self.pipe_placeholder_list(line)
10083                    } else {
10084                        self.parse_expression()?
10085                    };
10086                    Ok(Expr {
10087                        kind: ExprKind::SortExpr {
10088                            cmp: Some(SortComparator::Block(block)),
10089                            list: Box::new(list),
10090                        },
10091                        line,
10092                    })
10093                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
10094                    // Blockless comparator: `sort $a <=> $b, @list`
10095                    let block = self.parse_block_or_bareword_cmp_block()?;
10096                    let _ = self.eat(&Token::Comma);
10097                    let list = if self.in_pipe_rhs()
10098                        && matches!(
10099                            self.peek(),
10100                            Token::Semicolon
10101                                | Token::RBrace
10102                                | Token::RParen
10103                                | Token::Eof
10104                                | Token::PipeForward
10105                        ) {
10106                        self.pipe_placeholder_list(line)
10107                    } else {
10108                        self.parse_expression()?
10109                    };
10110                    Ok(Expr {
10111                        kind: ExprKind::SortExpr {
10112                            cmp: Some(SortComparator::Block(block)),
10113                            list: Box::new(list),
10114                        },
10115                        line,
10116                    })
10117                } else if matches!(self.peek(), Token::ScalarVar(_)) {
10118                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized
10119                    self.suppress_indirect_paren_call =
10120                        self.suppress_indirect_paren_call.saturating_add(1);
10121                    let code = self.parse_assign_expr()?;
10122                    self.suppress_indirect_paren_call =
10123                        self.suppress_indirect_paren_call.saturating_sub(1);
10124                    let list = if matches!(self.peek(), Token::LParen) {
10125                        self.advance();
10126                        let e = self.parse_expression()?;
10127                        self.expect(&Token::RParen)?;
10128                        e
10129                    } else {
10130                        self.parse_expression()?
10131                    };
10132                    Ok(Expr {
10133                        kind: ExprKind::SortExpr {
10134                            cmp: Some(SortComparator::Code(Box::new(code))),
10135                            list: Box::new(list),
10136                        },
10137                        line,
10138                    })
10139                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
10140                {
10141                    // Blockless comparator via bare sub name: `sort my_cmp @list`
10142                    let block = self.parse_block_or_bareword_cmp_block()?;
10143                    let _ = self.eat(&Token::Comma);
10144                    let list = if self.in_pipe_rhs()
10145                        && matches!(
10146                            self.peek(),
10147                            Token::Semicolon
10148                                | Token::RBrace
10149                                | Token::RParen
10150                                | Token::Eof
10151                                | Token::PipeForward
10152                        ) {
10153                        self.pipe_placeholder_list(line)
10154                    } else {
10155                        self.parse_expression()?
10156                    };
10157                    Ok(Expr {
10158                        kind: ExprKind::SortExpr {
10159                            cmp: Some(SortComparator::Block(block)),
10160                            list: Box::new(list),
10161                        },
10162                        line,
10163                    })
10164                } else {
10165                    // Bare `sort` with no comparator and no list: only allowed
10166                    // as the RHS of `|>`, where the list comes from the LHS.
10167                    let list = if self.in_pipe_rhs()
10168                        && matches!(
10169                            self.peek(),
10170                            Token::Semicolon
10171                                | Token::RBrace
10172                                | Token::RParen
10173                                | Token::Eof
10174                                | Token::PipeForward
10175                        ) {
10176                        self.pipe_placeholder_list(line)
10177                    } else {
10178                        self.parse_expression()?
10179                    };
10180                    Ok(Expr {
10181                        kind: ExprKind::SortExpr {
10182                            cmp: None,
10183                            list: Box::new(list),
10184                        },
10185                        line,
10186                    })
10187                }
10188            }
10189            "reduce" | "fold" | "inject" => {
10190                let (block, list) = self.parse_block_list()?;
10191                Ok(Expr {
10192                    kind: ExprKind::ReduceExpr {
10193                        block,
10194                        list: Box::new(list),
10195                    },
10196                    line,
10197                })
10198            }
10199            // Parallel extensions
10200            "pmap" => {
10201                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10202                Ok(Expr {
10203                    kind: ExprKind::PMapExpr {
10204                        block,
10205                        list: Box::new(list),
10206                        progress: progress.map(Box::new),
10207                        flat_outputs: false,
10208                        on_cluster: None,
10209                        stream: false,
10210                    },
10211                    line,
10212                })
10213            }
10214            "pmap_on" => {
10215                let (cluster, block, list, progress) =
10216                    self.parse_cluster_block_then_list_optional_progress()?;
10217                Ok(Expr {
10218                    kind: ExprKind::PMapExpr {
10219                        block,
10220                        list: Box::new(list),
10221                        progress: progress.map(Box::new),
10222                        flat_outputs: false,
10223                        on_cluster: Some(Box::new(cluster)),
10224                        stream: false,
10225                    },
10226                    line,
10227                })
10228            }
10229            "pflat_map" => {
10230                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10231                Ok(Expr {
10232                    kind: ExprKind::PMapExpr {
10233                        block,
10234                        list: Box::new(list),
10235                        progress: progress.map(Box::new),
10236                        flat_outputs: true,
10237                        on_cluster: None,
10238                        stream: false,
10239                    },
10240                    line,
10241                })
10242            }
10243            "pflat_map_on" => {
10244                let (cluster, block, list, progress) =
10245                    self.parse_cluster_block_then_list_optional_progress()?;
10246                Ok(Expr {
10247                    kind: ExprKind::PMapExpr {
10248                        block,
10249                        list: Box::new(list),
10250                        progress: progress.map(Box::new),
10251                        flat_outputs: true,
10252                        on_cluster: Some(Box::new(cluster)),
10253                        stream: false,
10254                    },
10255                    line,
10256                })
10257            }
10258            "pmaps" => {
10259                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10260                Ok(Expr {
10261                    kind: ExprKind::PMapExpr {
10262                        block,
10263                        list: Box::new(list),
10264                        progress: progress.map(Box::new),
10265                        flat_outputs: false,
10266                        on_cluster: None,
10267                        stream: true,
10268                    },
10269                    line,
10270                })
10271            }
10272            "pflat_maps" => {
10273                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10274                Ok(Expr {
10275                    kind: ExprKind::PMapExpr {
10276                        block,
10277                        list: Box::new(list),
10278                        progress: progress.map(Box::new),
10279                        flat_outputs: true,
10280                        on_cluster: None,
10281                        stream: true,
10282                    },
10283                    line,
10284                })
10285            }
10286            "pgreps" => {
10287                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10288                Ok(Expr {
10289                    kind: ExprKind::PGrepExpr {
10290                        block,
10291                        list: Box::new(list),
10292                        progress: progress.map(Box::new),
10293                        stream: true,
10294                    },
10295                    line,
10296                })
10297            }
10298            "pmap_chunked" => {
10299                let chunk_size = self.parse_assign_expr()?;
10300                let block = self.parse_block_or_bareword_block()?;
10301                self.eat(&Token::Comma);
10302                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10303                Ok(Expr {
10304                    kind: ExprKind::PMapChunkedExpr {
10305                        chunk_size: Box::new(chunk_size),
10306                        block,
10307                        list: Box::new(list),
10308                        progress: progress.map(Box::new),
10309                    },
10310                    line,
10311                })
10312            }
10313            "pgrep" => {
10314                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10315                Ok(Expr {
10316                    kind: ExprKind::PGrepExpr {
10317                        block,
10318                        list: Box::new(list),
10319                        progress: progress.map(Box::new),
10320                        stream: false,
10321                    },
10322                    line,
10323                })
10324            }
10325            "pfor" => {
10326                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10327                Ok(Expr {
10328                    kind: ExprKind::PForExpr {
10329                        block,
10330                        list: Box::new(list),
10331                        progress: progress.map(Box::new),
10332                    },
10333                    line,
10334                })
10335            }
10336            "par_lines" | "par_walk" => {
10337                let args = self.parse_builtin_args()?;
10338                if args.len() < 2 {
10339                    return Err(
10340                        self.syntax_err(format!("{} requires at least two arguments", name), line)
10341                    );
10342                }
10343
10344                if name == "par_lines" {
10345                    Ok(Expr {
10346                        kind: ExprKind::ParLinesExpr {
10347                            path: Box::new(args[0].clone()),
10348                            callback: Box::new(args[1].clone()),
10349                            progress: None,
10350                        },
10351                        line,
10352                    })
10353                } else {
10354                    Ok(Expr {
10355                        kind: ExprKind::ParWalkExpr {
10356                            path: Box::new(args[0].clone()),
10357                            callback: Box::new(args[1].clone()),
10358                            progress: None,
10359                        },
10360                        line,
10361                    })
10362                }
10363            }
10364            "pwatch" | "watch" => {
10365                let args = self.parse_builtin_args()?;
10366                if args.len() < 2 {
10367                    return Err(
10368                        self.syntax_err(format!("{} requires at least two arguments", name), line)
10369                    );
10370                }
10371                Ok(Expr {
10372                    kind: ExprKind::PwatchExpr {
10373                        path: Box::new(args[0].clone()),
10374                        callback: Box::new(args[1].clone()),
10375                    },
10376                    line,
10377                })
10378            }
10379            "fan" => {
10380                // fan { BLOCK }            — no count, block body
10381                // fan COUNT { BLOCK }      — count + block body
10382                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
10383                // fan COUNT EXPR;          — count + blockless body
10384                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
10385                let (count, block) = self.parse_fan_count_and_block(line)?;
10386                let progress = self.parse_fan_optional_progress("fan")?;
10387                Ok(Expr {
10388                    kind: ExprKind::FanExpr {
10389                        count,
10390                        block,
10391                        progress,
10392                        capture: false,
10393                    },
10394                    line,
10395                })
10396            }
10397            "fan_cap" => {
10398                let (count, block) = self.parse_fan_count_and_block(line)?;
10399                let progress = self.parse_fan_optional_progress("fan_cap")?;
10400                Ok(Expr {
10401                    kind: ExprKind::FanExpr {
10402                        count,
10403                        block,
10404                        progress,
10405                        capture: true,
10406                    },
10407                    line,
10408                })
10409            }
10410            "async" => {
10411                if !matches!(self.peek(), Token::LBrace) {
10412                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
10413                }
10414                let block = self.parse_block()?;
10415                Ok(Expr {
10416                    kind: ExprKind::AsyncBlock { body: block },
10417                    line,
10418                })
10419            }
10420            "spawn" => {
10421                if !matches!(self.peek(), Token::LBrace) {
10422                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
10423                }
10424                let block = self.parse_block()?;
10425                Ok(Expr {
10426                    kind: ExprKind::SpawnBlock { body: block },
10427                    line,
10428                })
10429            }
10430            "trace" => {
10431                if !matches!(self.peek(), Token::LBrace) {
10432                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
10433                }
10434                let block = self.parse_block()?;
10435                Ok(Expr {
10436                    kind: ExprKind::Trace { body: block },
10437                    line,
10438                })
10439            }
10440            "timer" => {
10441                let block = self.parse_block_or_bareword_block_no_args()?;
10442                Ok(Expr {
10443                    kind: ExprKind::Timer { body: block },
10444                    line,
10445                })
10446            }
10447            "bench" => {
10448                let block = self.parse_block_or_bareword_block_no_args()?;
10449                let times = Box::new(self.parse_expression()?);
10450                Ok(Expr {
10451                    kind: ExprKind::Bench { body: block, times },
10452                    line,
10453                })
10454            }
10455            "spinner" => {
10456                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
10457                let (message, body) = if matches!(self.peek(), Token::LBrace) {
10458                    let body = self.parse_block()?;
10459                    (
10460                        Box::new(Expr {
10461                            kind: ExprKind::String("working".to_string()),
10462                            line,
10463                        }),
10464                        body,
10465                    )
10466                } else {
10467                    let msg = self.parse_assign_expr()?;
10468                    let body = self.parse_block()?;
10469                    (Box::new(msg), body)
10470                };
10471                Ok(Expr {
10472                    kind: ExprKind::Spinner { message, body },
10473                    line,
10474                })
10475            }
10476            "thread" | "t" => {
10477                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
10478                // `t` is a short alias for `thread`
10479                // Each stage is either:
10480                //   - `ident` — bare function call
10481                //   - `ident { block }` — function with block arg
10482                //   - `ident arg1 arg2 { block }` — function with args and optional block
10483                //   - `fn { block }` — standalone anonymous block
10484                //   - `>{ block }` — shorthand for standalone anonymous block
10485                // Desugars to: EXPR |> stage1 |> stage2 |> ...
10486                self.parse_thread_macro(line, false)
10487            }
10488            "retry" => {
10489                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
10490                // An optional comma before `times` is allowed in both forms.
10491                let body = if matches!(self.peek(), Token::LBrace) {
10492                    self.parse_block()?
10493                } else {
10494                    let bw_line = self.peek_line();
10495                    let Token::Ident(ref name) = self.peek().clone() else {
10496                        return Err(self
10497                            .syntax_err("retry: expected block or bareword function name", line));
10498                    };
10499                    let name = name.clone();
10500                    self.advance();
10501                    vec![Statement::new(
10502                        StmtKind::Expression(Expr {
10503                            kind: ExprKind::FuncCall { name, args: vec![] },
10504                            line: bw_line,
10505                        }),
10506                        bw_line,
10507                    )]
10508                };
10509                self.eat(&Token::Comma);
10510                match self.peek() {
10511                    Token::Ident(ref s) if s == "times" => {
10512                        self.advance();
10513                    }
10514                    _ => {
10515                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
10516                    }
10517                }
10518                self.expect(&Token::FatArrow)?;
10519                let times = Box::new(self.parse_assign_expr()?);
10520                let mut backoff = RetryBackoff::None;
10521                if self.eat(&Token::Comma) {
10522                    match self.peek() {
10523                        Token::Ident(ref s) if s == "backoff" => {
10524                            self.advance();
10525                        }
10526                        _ => {
10527                            return Err(
10528                                self.syntax_err("retry: expected `backoff =>` after comma", line)
10529                            );
10530                        }
10531                    }
10532                    self.expect(&Token::FatArrow)?;
10533                    let Token::Ident(mode) = self.peek().clone() else {
10534                        return Err(self.syntax_err(
10535                            "retry: expected backoff mode (none, linear, exponential)",
10536                            line,
10537                        ));
10538                    };
10539                    backoff = match mode.as_str() {
10540                        "none" => RetryBackoff::None,
10541                        "linear" => RetryBackoff::Linear,
10542                        "exponential" => RetryBackoff::Exponential,
10543                        _ => {
10544                            return Err(
10545                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
10546                            );
10547                        }
10548                    };
10549                    self.advance();
10550                }
10551                Ok(Expr {
10552                    kind: ExprKind::RetryBlock {
10553                        body,
10554                        times,
10555                        backoff,
10556                    },
10557                    line,
10558                })
10559            }
10560            "rate_limit" => {
10561                self.expect(&Token::LParen)?;
10562                let max = Box::new(self.parse_assign_expr()?);
10563                self.expect(&Token::Comma)?;
10564                let window = Box::new(self.parse_assign_expr()?);
10565                self.expect(&Token::RParen)?;
10566                let body = self.parse_block_or_bareword_block_no_args()?;
10567                let slot = self.alloc_rate_limit_slot();
10568                Ok(Expr {
10569                    kind: ExprKind::RateLimitBlock {
10570                        slot,
10571                        max,
10572                        window,
10573                        body,
10574                    },
10575                    line,
10576                })
10577            }
10578            "every" => {
10579                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
10580                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
10581                let has_paren = self.eat(&Token::LParen);
10582                let interval = Box::new(self.parse_assign_expr()?);
10583                if has_paren {
10584                    self.expect(&Token::RParen)?;
10585                }
10586                let body = if matches!(self.peek(), Token::LBrace) {
10587                    self.parse_block()?
10588                } else {
10589                    let bline = self.peek_line();
10590                    let expr = self.parse_assign_expr()?;
10591                    vec![Statement::new(StmtKind::Expression(expr), bline)]
10592                };
10593                Ok(Expr {
10594                    kind: ExprKind::EveryBlock { interval, body },
10595                    line,
10596                })
10597            }
10598            "gen" => {
10599                if !matches!(self.peek(), Token::LBrace) {
10600                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
10601                }
10602                let body = self.parse_block()?;
10603                Ok(Expr {
10604                    kind: ExprKind::GenBlock { body },
10605                    line,
10606                })
10607            }
10608            "yield" => {
10609                let e = self.parse_assign_expr()?;
10610                Ok(Expr {
10611                    kind: ExprKind::Yield(Box::new(e)),
10612                    line,
10613                })
10614            }
10615            "await" => {
10616                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10617                    return Ok(e);
10618                }
10619                // `await` defaults to `$_` so `map { await } @tasks` works
10620                // (Perl-style topic-defaulting unary).
10621                let a = self.parse_one_arg_or_default()?;
10622                Ok(Expr {
10623                    kind: ExprKind::Await(Box::new(a)),
10624                    line,
10625                })
10626            }
10627            "slurp" | "cat" | "c" => {
10628                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10629                    return Ok(e);
10630                }
10631                let a = self.parse_one_arg_or_default()?;
10632                Ok(Expr {
10633                    kind: ExprKind::Slurp(Box::new(a)),
10634                    line,
10635                })
10636            }
10637            "capture" => {
10638                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10639                    return Ok(e);
10640                }
10641                let a = self.parse_one_arg()?;
10642                Ok(Expr {
10643                    kind: ExprKind::Capture(Box::new(a)),
10644                    line,
10645                })
10646            }
10647            "fetch_url" => {
10648                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10649                    return Ok(e);
10650                }
10651                let a = self.parse_one_arg()?;
10652                Ok(Expr {
10653                    kind: ExprKind::FetchUrl(Box::new(a)),
10654                    line,
10655                })
10656            }
10657            "pchannel" => {
10658                let capacity = if self.eat(&Token::LParen) {
10659                    if matches!(self.peek(), Token::RParen) {
10660                        self.advance();
10661                        None
10662                    } else {
10663                        let e = self.parse_expression()?;
10664                        self.expect(&Token::RParen)?;
10665                        Some(Box::new(e))
10666                    }
10667                } else {
10668                    None
10669                };
10670                Ok(Expr {
10671                    kind: ExprKind::Pchannel { capacity },
10672                    line,
10673                })
10674            }
10675            "psort" => {
10676                if matches!(self.peek(), Token::LBrace)
10677                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
10678                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
10679                {
10680                    let block = self.parse_block_or_bareword_cmp_block()?;
10681                    self.eat(&Token::Comma);
10682                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10683                    Ok(Expr {
10684                        kind: ExprKind::PSortExpr {
10685                            cmp: Some(block),
10686                            list: Box::new(list),
10687                            progress: progress.map(Box::new),
10688                        },
10689                        line,
10690                    })
10691                } else {
10692                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10693                    Ok(Expr {
10694                        kind: ExprKind::PSortExpr {
10695                            cmp: None,
10696                            list: Box::new(list),
10697                            progress: progress.map(Box::new),
10698                        },
10699                        line,
10700                    })
10701                }
10702            }
10703            "preduce" => {
10704                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10705                Ok(Expr {
10706                    kind: ExprKind::PReduceExpr {
10707                        block,
10708                        list: Box::new(list),
10709                        progress: progress.map(Box::new),
10710                    },
10711                    line,
10712                })
10713            }
10714            "preduce_init" => {
10715                let (init, block, list, progress) =
10716                    self.parse_init_block_then_list_optional_progress()?;
10717                Ok(Expr {
10718                    kind: ExprKind::PReduceInitExpr {
10719                        init: Box::new(init),
10720                        block,
10721                        list: Box::new(list),
10722                        progress: progress.map(Box::new),
10723                    },
10724                    line,
10725                })
10726            }
10727            "pmap_reduce" => {
10728                let map_block = self.parse_block_or_bareword_block()?;
10729                // After the map block, expect either a `{ REDUCE }` block, or
10730                // after an eaten comma, a blockless reduce expr (`$a + $b`).
10731                let reduce_block = if matches!(self.peek(), Token::LBrace) {
10732                    self.parse_block()?
10733                } else {
10734                    // comma separates blockless map from blockless reduce
10735                    self.expect(&Token::Comma)?;
10736                    self.parse_block_or_bareword_cmp_block()?
10737                };
10738                self.eat(&Token::Comma);
10739                let line = self.peek_line();
10740                if let Token::Ident(ref kw) = self.peek().clone() {
10741                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10742                        self.advance();
10743                        self.expect(&Token::FatArrow)?;
10744                        let prog = self.parse_assign_expr()?;
10745                        return Ok(Expr {
10746                            kind: ExprKind::PMapReduceExpr {
10747                                map_block,
10748                                reduce_block,
10749                                list: Box::new(Expr {
10750                                    kind: ExprKind::List(vec![]),
10751                                    line,
10752                                }),
10753                                progress: Some(Box::new(prog)),
10754                            },
10755                            line,
10756                        });
10757                    }
10758                }
10759                if matches!(
10760                    self.peek(),
10761                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10762                ) {
10763                    return Ok(Expr {
10764                        kind: ExprKind::PMapReduceExpr {
10765                            map_block,
10766                            reduce_block,
10767                            list: Box::new(Expr {
10768                                kind: ExprKind::List(vec![]),
10769                                line,
10770                            }),
10771                            progress: None,
10772                        },
10773                        line,
10774                    });
10775                }
10776                let mut parts = vec![self.parse_assign_expr()?];
10777                loop {
10778                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10779                        break;
10780                    }
10781                    if matches!(
10782                        self.peek(),
10783                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10784                    ) {
10785                        break;
10786                    }
10787                    if let Token::Ident(ref kw) = self.peek().clone() {
10788                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10789                            self.advance();
10790                            self.expect(&Token::FatArrow)?;
10791                            let prog = self.parse_assign_expr()?;
10792                            return Ok(Expr {
10793                                kind: ExprKind::PMapReduceExpr {
10794                                    map_block,
10795                                    reduce_block,
10796                                    list: Box::new(merge_expr_list(parts)),
10797                                    progress: Some(Box::new(prog)),
10798                                },
10799                                line,
10800                            });
10801                        }
10802                    }
10803                    parts.push(self.parse_assign_expr()?);
10804                }
10805                Ok(Expr {
10806                    kind: ExprKind::PMapReduceExpr {
10807                        map_block,
10808                        reduce_block,
10809                        list: Box::new(merge_expr_list(parts)),
10810                        progress: None,
10811                    },
10812                    line,
10813                })
10814            }
10815            "puniq" => {
10816                if self.pipe_supplies_slurped_list_operand() {
10817                    return Ok(Expr {
10818                        kind: ExprKind::FuncCall {
10819                            name: "puniq".to_string(),
10820                            args: vec![],
10821                        },
10822                        line,
10823                    });
10824                }
10825                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10826                let mut args = vec![list];
10827                if let Some(p) = progress {
10828                    args.push(p);
10829                }
10830                Ok(Expr {
10831                    kind: ExprKind::FuncCall {
10832                        name: "puniq".to_string(),
10833                        args,
10834                    },
10835                    line,
10836                })
10837            }
10838            "pfirst" => {
10839                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10840                let cr = Expr {
10841                    kind: ExprKind::CodeRef {
10842                        params: vec![],
10843                        body: block,
10844                    },
10845                    line,
10846                };
10847                let mut args = vec![cr, list];
10848                if let Some(p) = progress {
10849                    args.push(p);
10850                }
10851                Ok(Expr {
10852                    kind: ExprKind::FuncCall {
10853                        name: "pfirst".to_string(),
10854                        args,
10855                    },
10856                    line,
10857                })
10858            }
10859            "pany" => {
10860                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10861                let cr = Expr {
10862                    kind: ExprKind::CodeRef {
10863                        params: vec![],
10864                        body: block,
10865                    },
10866                    line,
10867                };
10868                let mut args = vec![cr, list];
10869                if let Some(p) = progress {
10870                    args.push(p);
10871                }
10872                Ok(Expr {
10873                    kind: ExprKind::FuncCall {
10874                        name: "pany".to_string(),
10875                        args,
10876                    },
10877                    line,
10878                })
10879            }
10880            "uniq" | "distinct" => {
10881                if self.pipe_supplies_slurped_list_operand() {
10882                    return Ok(Expr {
10883                        kind: ExprKind::FuncCall {
10884                            name: name.clone(),
10885                            args: vec![],
10886                        },
10887                        line,
10888                    });
10889                }
10890                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10891                if progress.is_some() {
10892                    return Err(self.syntax_err(
10893                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
10894                        line,
10895                    ));
10896                }
10897                Ok(Expr {
10898                    kind: ExprKind::FuncCall {
10899                        name: name.clone(),
10900                        args: vec![list],
10901                    },
10902                    line,
10903                })
10904            }
10905            "flatten" => {
10906                if self.pipe_supplies_slurped_list_operand() {
10907                    return Ok(Expr {
10908                        kind: ExprKind::FuncCall {
10909                            name: "flatten".to_string(),
10910                            args: vec![],
10911                        },
10912                        line,
10913                    });
10914                }
10915                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10916                if progress.is_some() {
10917                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
10918                }
10919                Ok(Expr {
10920                    kind: ExprKind::FuncCall {
10921                        name: "flatten".to_string(),
10922                        args: vec![list],
10923                    },
10924                    line,
10925                })
10926            }
10927            "set" => {
10928                if self.pipe_supplies_slurped_list_operand() {
10929                    return Ok(Expr {
10930                        kind: ExprKind::FuncCall {
10931                            name: "set".to_string(),
10932                            args: vec![],
10933                        },
10934                        line,
10935                    });
10936                }
10937                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10938                if progress.is_some() {
10939                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
10940                }
10941                Ok(Expr {
10942                    kind: ExprKind::FuncCall {
10943                        name: "set".to_string(),
10944                        args: vec![list],
10945                    },
10946                    line,
10947                })
10948            }
10949            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
10950            // Defaults to `$_` when no arg is given, like `length`. See
10951            // `builtin_file_size` in builtins.rs for the runtime behavior.
10952            "size" => {
10953                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10954                    return Ok(e);
10955                }
10956                if self.pipe_supplies_slurped_list_operand() {
10957                    return Ok(Expr {
10958                        kind: ExprKind::FuncCall {
10959                            name: "size".to_string(),
10960                            args: vec![],
10961                        },
10962                        line,
10963                    });
10964                }
10965                let a = self.parse_one_arg_or_default()?;
10966                Ok(Expr {
10967                    kind: ExprKind::FuncCall {
10968                        name: "size".to_string(),
10969                        args: vec![a],
10970                    },
10971                    line,
10972                })
10973            }
10974            "list_count" | "list_size" | "count" | "len" | "cnt" => {
10975                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10976                    return Ok(e);
10977                }
10978                if self.pipe_supplies_slurped_list_operand() {
10979                    return Ok(Expr {
10980                        kind: ExprKind::FuncCall {
10981                            name: name.clone(),
10982                            args: vec![],
10983                        },
10984                        line,
10985                    });
10986                }
10987                // `len(EXPR)` / `cnt(EXPR)` / `count(EXPR)` with a tight `(` —
10988                // the parens are function-call syntax, not a parenthesized
10989                // list: stop the argument at `)` so `len(@a) % 2 == 1` is
10990                // `(len(@a)) % 2 == 1`, not `len(@a % 2 == 1)`. Empty parens
10991                // `len()` collapse to a zero-arg call (use the piped operand
10992                // or `$_`). Bare `len` followed by a low-precedence operator
10993                // (`==`, `&&`, `?`, …) also defaults to a zero-arg call so
10994                // `{ len == 0 }` works as a block predicate on the topic.
10995                // Bare `len EXPR` (no parens, e.g. `len @arr`) goes through
10996                // the greedy list-arg parser; this means `len @a + len @b`
10997                // is `len(@a + len(@b))` (returning the length of the sum
10998                // string), not `(len @a) + (len @b)`. Use explicit parens
10999                // when combining `len` with `+`, `-`, comparisons, etc.
11000                let args = if matches!(self.peek(), Token::LParen) {
11001                    self.advance();
11002                    if matches!(self.peek(), Token::RParen) {
11003                        self.advance();
11004                        Vec::new()
11005                    } else {
11006                        let inner = self.parse_expression()?;
11007                        self.expect(&Token::RParen)?;
11008                        vec![inner]
11009                    }
11010                } else if self.peek_is_named_unary_terminator() {
11011                    Vec::new()
11012                } else {
11013                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11014                    if progress.is_some() {
11015                        return Err(self.syntax_err(
11016                            "`progress =>` is not supported for list_count / list_size / count / cnt",
11017                            line,
11018                        ));
11019                    }
11020                    vec![list]
11021                };
11022                Ok(Expr {
11023                    kind: ExprKind::FuncCall {
11024                        name: name.clone(),
11025                        args,
11026                    },
11027                    line,
11028                })
11029            }
11030            "shuffle" | "shuffled" => {
11031                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11032                    return Ok(e);
11033                }
11034                if self.pipe_supplies_slurped_list_operand() {
11035                    return Ok(Expr {
11036                        kind: ExprKind::FuncCall {
11037                            name: "shuffle".to_string(),
11038                            args: vec![],
11039                        },
11040                        line,
11041                    });
11042                }
11043                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11044                if progress.is_some() {
11045                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
11046                }
11047                Ok(Expr {
11048                    kind: ExprKind::FuncCall {
11049                        name: "shuffle".to_string(),
11050                        args: vec![list],
11051                    },
11052                    line,
11053                })
11054            }
11055            "chunked" => {
11056                let mut parts = Vec::new();
11057                if self.eat(&Token::LParen) {
11058                    if !matches!(self.peek(), Token::RParen) {
11059                        parts.push(self.parse_assign_expr()?);
11060                        while self.eat(&Token::Comma) {
11061                            if matches!(self.peek(), Token::RParen) {
11062                                break;
11063                            }
11064                            parts.push(self.parse_assign_expr()?);
11065                        }
11066                    }
11067                    self.expect(&Token::RParen)?;
11068                } else {
11069                    // Paren-less `chunked N`: `|>` is a hard terminator, not
11070                    // an operator inside the arg (see
11071                    // `parse_assign_expr_stop_at_pipe`).
11072                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
11073                    loop {
11074                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11075                            break;
11076                        }
11077                        if matches!(
11078                            self.peek(),
11079                            Token::Semicolon
11080                                | Token::RBrace
11081                                | Token::RParen
11082                                | Token::Eof
11083                                | Token::PipeForward
11084                        ) {
11085                            break;
11086                        }
11087                        if self.peek_is_postfix_stmt_modifier_keyword() {
11088                            break;
11089                        }
11090                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
11091                    }
11092                }
11093                if parts.len() == 1 {
11094                    let n = parts.pop().unwrap();
11095                    return Ok(Expr {
11096                        kind: ExprKind::FuncCall {
11097                            name: "chunked".to_string(),
11098                            args: vec![n],
11099                        },
11100                        line,
11101                    });
11102                }
11103                if parts.is_empty() {
11104                    return Ok(Expr {
11105                        kind: ExprKind::FuncCall {
11106                            name: "chunked".to_string(),
11107                            args: parts,
11108                        },
11109                        line,
11110                    });
11111                }
11112                if parts.len() == 2 {
11113                    let n = parts.pop().unwrap();
11114                    let list = parts.pop().unwrap();
11115                    return Ok(Expr {
11116                        kind: ExprKind::FuncCall {
11117                            name: "chunked".to_string(),
11118                            args: vec![list, n],
11119                        },
11120                        line,
11121                    });
11122                }
11123                Err(self.syntax_err(
11124                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
11125                    line,
11126                ))
11127            }
11128            "windowed" => {
11129                let mut parts = Vec::new();
11130                if self.eat(&Token::LParen) {
11131                    if !matches!(self.peek(), Token::RParen) {
11132                        parts.push(self.parse_assign_expr()?);
11133                        while self.eat(&Token::Comma) {
11134                            if matches!(self.peek(), Token::RParen) {
11135                                break;
11136                            }
11137                            parts.push(self.parse_assign_expr()?);
11138                        }
11139                    }
11140                    self.expect(&Token::RParen)?;
11141                } else {
11142                    // Paren-less `windowed N`: same `|>`-terminator rule as
11143                    // `chunked` above.
11144                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
11145                    loop {
11146                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11147                            break;
11148                        }
11149                        if matches!(
11150                            self.peek(),
11151                            Token::Semicolon
11152                                | Token::RBrace
11153                                | Token::RParen
11154                                | Token::Eof
11155                                | Token::PipeForward
11156                        ) {
11157                            break;
11158                        }
11159                        if self.peek_is_postfix_stmt_modifier_keyword() {
11160                            break;
11161                        }
11162                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
11163                    }
11164                }
11165                if parts.len() == 1 {
11166                    let n = parts.pop().unwrap();
11167                    return Ok(Expr {
11168                        kind: ExprKind::FuncCall {
11169                            name: "windowed".to_string(),
11170                            args: vec![n],
11171                        },
11172                        line,
11173                    });
11174                }
11175                if parts.is_empty() {
11176                    return Ok(Expr {
11177                        kind: ExprKind::FuncCall {
11178                            name: "windowed".to_string(),
11179                            args: parts,
11180                        },
11181                        line,
11182                    });
11183                }
11184                if parts.len() == 2 {
11185                    let n = parts.pop().unwrap();
11186                    let list = parts.pop().unwrap();
11187                    return Ok(Expr {
11188                        kind: ExprKind::FuncCall {
11189                            name: "windowed".to_string(),
11190                            args: vec![list, n],
11191                        },
11192                        line,
11193                    });
11194                }
11195                Err(self.syntax_err(
11196                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
11197                    line,
11198                ))
11199            }
11200            "any" | "all" | "none" => {
11201                // `any(CODEREF, LIST)` with parens — parse as normal call.
11202                if matches!(self.peek(), Token::LParen) {
11203                    self.advance();
11204                    let args = self.parse_arg_list()?;
11205                    self.expect(&Token::RParen)?;
11206                    return Ok(Expr {
11207                        kind: ExprKind::FuncCall {
11208                            name: name.clone(),
11209                            args,
11210                        },
11211                        line,
11212                    });
11213                }
11214                // `any BLOCK LIST` without parens.
11215                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11216                if progress.is_some() {
11217                    return Err(self.syntax_err(
11218                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
11219                        line,
11220                    ));
11221                }
11222                let cr = Expr {
11223                    kind: ExprKind::CodeRef {
11224                        params: vec![],
11225                        body: block,
11226                    },
11227                    line,
11228                };
11229                Ok(Expr {
11230                    kind: ExprKind::FuncCall {
11231                        name: name.clone(),
11232                        args: vec![cr, list],
11233                    },
11234                    line,
11235                })
11236            }
11237            // Ruby `detect` / `find` — same as `first` (first element matching block).
11238            "first" | "detect" | "find" => {
11239                // `first(CODEREF, LIST)` with parens — parse as normal call.
11240                if matches!(self.peek(), Token::LParen) {
11241                    self.advance();
11242                    let args = self.parse_arg_list()?;
11243                    self.expect(&Token::RParen)?;
11244                    return Ok(Expr {
11245                        kind: ExprKind::FuncCall {
11246                            name: "first".to_string(),
11247                            args,
11248                        },
11249                        line,
11250                    });
11251                }
11252                // `first BLOCK LIST` without parens.
11253                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11254                if progress.is_some() {
11255                    return Err(self.syntax_err(
11256                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
11257                        line,
11258                    ));
11259                }
11260                let cr = Expr {
11261                    kind: ExprKind::CodeRef {
11262                        params: vec![],
11263                        body: block,
11264                    },
11265                    line,
11266                };
11267                Ok(Expr {
11268                    kind: ExprKind::FuncCall {
11269                        name: "first".to_string(),
11270                        args: vec![cr, list],
11271                    },
11272                    line,
11273                })
11274            }
11275            "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
11276            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
11277                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11278                if progress.is_some() {
11279                    return Err(
11280                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
11281                    );
11282                }
11283                let cr = Expr {
11284                    kind: ExprKind::CodeRef {
11285                        params: vec![],
11286                        body: block,
11287                    },
11288                    line,
11289                };
11290                Ok(Expr {
11291                    kind: ExprKind::FuncCall {
11292                        name: name.to_string(),
11293                        args: vec![cr, list],
11294                    },
11295                    line,
11296                })
11297            }
11298            "group_by" | "chunk_by" => {
11299                if matches!(self.peek(), Token::LBrace) {
11300                    let (block, list) = self.parse_block_list()?;
11301                    let cr = Expr {
11302                        kind: ExprKind::CodeRef {
11303                            params: vec![],
11304                            body: block,
11305                        },
11306                        line,
11307                    };
11308                    Ok(Expr {
11309                        kind: ExprKind::FuncCall {
11310                            name: name.to_string(),
11311                            args: vec![cr, list],
11312                        },
11313                        line,
11314                    })
11315                } else {
11316                    let key_expr = self.parse_assign_expr()?;
11317                    self.expect(&Token::Comma)?;
11318                    let list_parts = self.parse_list_until_terminator()?;
11319                    let list_expr = if list_parts.len() == 1 {
11320                        list_parts.into_iter().next().unwrap()
11321                    } else {
11322                        Expr {
11323                            kind: ExprKind::List(list_parts),
11324                            line,
11325                        }
11326                    };
11327                    Ok(Expr {
11328                        kind: ExprKind::FuncCall {
11329                            name: name.to_string(),
11330                            args: vec![key_expr, list_expr],
11331                        },
11332                        line,
11333                    })
11334                }
11335            }
11336            "with_index" => {
11337                if self.pipe_supplies_slurped_list_operand() {
11338                    return Ok(Expr {
11339                        kind: ExprKind::FuncCall {
11340                            name: "with_index".to_string(),
11341                            args: vec![],
11342                        },
11343                        line,
11344                    });
11345                }
11346                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11347                if progress.is_some() {
11348                    return Err(
11349                        self.syntax_err("`progress =>` is not supported for with_index", line)
11350                    );
11351                }
11352                Ok(Expr {
11353                    kind: ExprKind::FuncCall {
11354                        name: "with_index".to_string(),
11355                        args: vec![list],
11356                    },
11357                    line,
11358                })
11359            }
11360            "pcache" => {
11361                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11362                Ok(Expr {
11363                    kind: ExprKind::PcacheExpr {
11364                        block,
11365                        list: Box::new(list),
11366                        progress: progress.map(Box::new),
11367                    },
11368                    line,
11369                })
11370            }
11371            "pselect" => {
11372                let paren = self.eat(&Token::LParen);
11373                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
11374                if paren {
11375                    self.expect(&Token::RParen)?;
11376                }
11377                if receivers.is_empty() {
11378                    return Err(self.syntax_err("pselect needs at least one receiver", line));
11379                }
11380                Ok(Expr {
11381                    kind: ExprKind::PselectExpr {
11382                        receivers,
11383                        timeout: timeout.map(Box::new),
11384                    },
11385                    line,
11386                })
11387            }
11388            "open" => {
11389                let paren = matches!(self.peek(), Token::LParen);
11390                if paren {
11391                    self.advance();
11392                }
11393                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
11394                    self.advance();
11395                    let name = self.parse_scalar_var_name()?;
11396                    self.expect(&Token::Comma)?;
11397                    let mode = self.parse_assign_expr()?;
11398                    let file = if self.eat(&Token::Comma) {
11399                        Some(self.parse_assign_expr()?)
11400                    } else {
11401                        None
11402                    };
11403                    if paren {
11404                        self.expect(&Token::RParen)?;
11405                    }
11406                    Ok(Expr {
11407                        kind: ExprKind::Open {
11408                            handle: Box::new(Expr {
11409                                kind: ExprKind::OpenMyHandle { name },
11410                                line,
11411                            }),
11412                            mode: Box::new(mode),
11413                            file: file.map(Box::new),
11414                        },
11415                        line,
11416                    })
11417                } else {
11418                    let args = if paren {
11419                        self.parse_arg_list()?
11420                    } else {
11421                        self.parse_list_until_terminator()?
11422                    };
11423                    if paren {
11424                        self.expect(&Token::RParen)?;
11425                    }
11426                    if args.len() < 2 {
11427                        return Err(self.syntax_err("open requires at least 2 arguments", line));
11428                    }
11429                    Ok(Expr {
11430                        kind: ExprKind::Open {
11431                            handle: Box::new(args[0].clone()),
11432                            mode: Box::new(args[1].clone()),
11433                            file: args.get(2).cloned().map(Box::new),
11434                        },
11435                        line,
11436                    })
11437                }
11438            }
11439            "close" => {
11440                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11441                    return Ok(e);
11442                }
11443                let a = self.parse_one_arg_or_default()?;
11444                Ok(Expr {
11445                    kind: ExprKind::Close(Box::new(a)),
11446                    line,
11447                })
11448            }
11449            "opendir" => {
11450                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11451                    return Ok(e);
11452                }
11453                let args = self.parse_builtin_args()?;
11454                if args.len() != 2 {
11455                    return Err(self.syntax_err("opendir requires two arguments", line));
11456                }
11457                Ok(Expr {
11458                    kind: ExprKind::Opendir {
11459                        handle: Box::new(args[0].clone()),
11460                        path: Box::new(args[1].clone()),
11461                    },
11462                    line,
11463                })
11464            }
11465            "readdir" => {
11466                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11467                    return Ok(e);
11468                }
11469                let a = self.parse_one_arg()?;
11470                Ok(Expr {
11471                    kind: ExprKind::Readdir(Box::new(a)),
11472                    line,
11473                })
11474            }
11475            "closedir" => {
11476                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11477                    return Ok(e);
11478                }
11479                let a = self.parse_one_arg()?;
11480                Ok(Expr {
11481                    kind: ExprKind::Closedir(Box::new(a)),
11482                    line,
11483                })
11484            }
11485            "rewinddir" => {
11486                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11487                    return Ok(e);
11488                }
11489                let a = self.parse_one_arg()?;
11490                Ok(Expr {
11491                    kind: ExprKind::Rewinddir(Box::new(a)),
11492                    line,
11493                })
11494            }
11495            "telldir" => {
11496                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11497                    return Ok(e);
11498                }
11499                let a = self.parse_one_arg()?;
11500                Ok(Expr {
11501                    kind: ExprKind::Telldir(Box::new(a)),
11502                    line,
11503                })
11504            }
11505            "seekdir" => {
11506                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11507                    return Ok(e);
11508                }
11509                let args = self.parse_builtin_args()?;
11510                if args.len() != 2 {
11511                    return Err(self.syntax_err("seekdir requires two arguments", line));
11512                }
11513                Ok(Expr {
11514                    kind: ExprKind::Seekdir {
11515                        handle: Box::new(args[0].clone()),
11516                        position: Box::new(args[1].clone()),
11517                    },
11518                    line,
11519                })
11520            }
11521            "eof" => {
11522                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11523                    return Ok(e);
11524                }
11525                if matches!(self.peek(), Token::LParen) {
11526                    self.advance();
11527                    if matches!(self.peek(), Token::RParen) {
11528                        self.advance();
11529                        Ok(Expr {
11530                            kind: ExprKind::Eof(None),
11531                            line,
11532                        })
11533                    } else {
11534                        let a = self.parse_expression()?;
11535                        self.expect(&Token::RParen)?;
11536                        Ok(Expr {
11537                            kind: ExprKind::Eof(Some(Box::new(a))),
11538                            line,
11539                        })
11540                    }
11541                } else {
11542                    Ok(Expr {
11543                        kind: ExprKind::Eof(None),
11544                        line,
11545                    })
11546                }
11547            }
11548            "system" => {
11549                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11550                    return Ok(e);
11551                }
11552                let args = self.parse_builtin_args()?;
11553                Ok(Expr {
11554                    kind: ExprKind::System(args),
11555                    line,
11556                })
11557            }
11558            "exec" => {
11559                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11560                    return Ok(e);
11561                }
11562                let args = self.parse_builtin_args()?;
11563                Ok(Expr {
11564                    kind: ExprKind::Exec(args),
11565                    line,
11566                })
11567            }
11568            "eval" => {
11569                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11570                    return Ok(e);
11571                }
11572                let a = if matches!(self.peek(), Token::LBrace) {
11573                    let block = self.parse_block()?;
11574                    Expr {
11575                        kind: ExprKind::CodeRef {
11576                            params: vec![],
11577                            body: block,
11578                        },
11579                        line,
11580                    }
11581                } else {
11582                    self.parse_one_arg_or_default()?
11583                };
11584                Ok(Expr {
11585                    kind: ExprKind::Eval(Box::new(a)),
11586                    line,
11587                })
11588            }
11589            "do" => {
11590                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11591                    return Ok(e);
11592                }
11593                let a = self.parse_one_arg()?;
11594                Ok(Expr {
11595                    kind: ExprKind::Do(Box::new(a)),
11596                    line,
11597                })
11598            }
11599            "require" => {
11600                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11601                    return Ok(e);
11602                }
11603                let a = self.parse_one_arg()?;
11604                Ok(Expr {
11605                    kind: ExprKind::Require(Box::new(a)),
11606                    line,
11607                })
11608            }
11609            "exit" => {
11610                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11611                    return Ok(e);
11612                }
11613                if matches!(
11614                    self.peek(),
11615                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11616                ) {
11617                    Ok(Expr {
11618                        kind: ExprKind::Exit(None),
11619                        line,
11620                    })
11621                } else {
11622                    let a = self.parse_one_arg()?;
11623                    Ok(Expr {
11624                        kind: ExprKind::Exit(Some(Box::new(a))),
11625                        line,
11626                    })
11627                }
11628            }
11629            "chdir" => {
11630                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11631                    return Ok(e);
11632                }
11633                let a = self.parse_one_arg_or_default()?;
11634                Ok(Expr {
11635                    kind: ExprKind::Chdir(Box::new(a)),
11636                    line,
11637                })
11638            }
11639            "mkdir" => {
11640                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11641                    return Ok(e);
11642                }
11643                let args = self.parse_builtin_args()?;
11644                Ok(Expr {
11645                    kind: ExprKind::Mkdir {
11646                        path: Box::new(args[0].clone()),
11647                        mode: args.get(1).cloned().map(Box::new),
11648                    },
11649                    line,
11650                })
11651            }
11652            "unlink" | "rm" => {
11653                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11654                    return Ok(e);
11655                }
11656                let args = self.parse_builtin_args()?;
11657                Ok(Expr {
11658                    kind: ExprKind::Unlink(args),
11659                    line,
11660                })
11661            }
11662            "rename" => {
11663                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11664                    return Ok(e);
11665                }
11666                let args = self.parse_builtin_args()?;
11667                if args.len() != 2 {
11668                    return Err(self.syntax_err("rename requires two arguments", line));
11669                }
11670                Ok(Expr {
11671                    kind: ExprKind::Rename {
11672                        old: Box::new(args[0].clone()),
11673                        new: Box::new(args[1].clone()),
11674                    },
11675                    line,
11676                })
11677            }
11678            "chmod" => {
11679                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11680                    return Ok(e);
11681                }
11682                let args = self.parse_builtin_args()?;
11683                if args.len() < 2 {
11684                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
11685                }
11686                Ok(Expr {
11687                    kind: ExprKind::Chmod(args),
11688                    line,
11689                })
11690            }
11691            "chown" => {
11692                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11693                    return Ok(e);
11694                }
11695                let args = self.parse_builtin_args()?;
11696                if args.len() < 3 {
11697                    return Err(
11698                        self.syntax_err("chown requires uid, gid, and at least one file", line)
11699                    );
11700                }
11701                Ok(Expr {
11702                    kind: ExprKind::Chown(args),
11703                    line,
11704                })
11705            }
11706            "stat" => {
11707                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11708                    return Ok(e);
11709                }
11710                let args = self.parse_builtin_args()?;
11711                let arg = if args.len() == 1 {
11712                    args[0].clone()
11713                } else if args.is_empty() {
11714                    Expr {
11715                        kind: ExprKind::ScalarVar("_".into()),
11716                        line,
11717                    }
11718                } else {
11719                    return Err(self.syntax_err("stat requires zero or one argument", line));
11720                };
11721                Ok(Expr {
11722                    kind: ExprKind::Stat(Box::new(arg)),
11723                    line,
11724                })
11725            }
11726            "lstat" => {
11727                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11728                    return Ok(e);
11729                }
11730                let args = self.parse_builtin_args()?;
11731                let arg = if args.len() == 1 {
11732                    args[0].clone()
11733                } else if args.is_empty() {
11734                    Expr {
11735                        kind: ExprKind::ScalarVar("_".into()),
11736                        line,
11737                    }
11738                } else {
11739                    return Err(self.syntax_err("lstat requires zero or one argument", line));
11740                };
11741                Ok(Expr {
11742                    kind: ExprKind::Lstat(Box::new(arg)),
11743                    line,
11744                })
11745            }
11746            "link" => {
11747                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11748                    return Ok(e);
11749                }
11750                let args = self.parse_builtin_args()?;
11751                if args.len() != 2 {
11752                    return Err(self.syntax_err("link requires two arguments", line));
11753                }
11754                Ok(Expr {
11755                    kind: ExprKind::Link {
11756                        old: Box::new(args[0].clone()),
11757                        new: Box::new(args[1].clone()),
11758                    },
11759                    line,
11760                })
11761            }
11762            "symlink" => {
11763                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11764                    return Ok(e);
11765                }
11766                let args = self.parse_builtin_args()?;
11767                if args.len() != 2 {
11768                    return Err(self.syntax_err("symlink requires two arguments", line));
11769                }
11770                Ok(Expr {
11771                    kind: ExprKind::Symlink {
11772                        old: Box::new(args[0].clone()),
11773                        new: Box::new(args[1].clone()),
11774                    },
11775                    line,
11776                })
11777            }
11778            "readlink" => {
11779                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11780                    return Ok(e);
11781                }
11782                let args = self.parse_builtin_args()?;
11783                let arg = if args.len() == 1 {
11784                    args[0].clone()
11785                } else if args.is_empty() {
11786                    Expr {
11787                        kind: ExprKind::ScalarVar("_".into()),
11788                        line,
11789                    }
11790                } else {
11791                    return Err(self.syntax_err("readlink requires zero or one argument", line));
11792                };
11793                Ok(Expr {
11794                    kind: ExprKind::Readlink(Box::new(arg)),
11795                    line,
11796                })
11797            }
11798            "files" => {
11799                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11800                    return Ok(e);
11801                }
11802                let args = self.parse_builtin_args()?;
11803                Ok(Expr {
11804                    kind: ExprKind::Files(args),
11805                    line,
11806                })
11807            }
11808            "filesf" | "f" => {
11809                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11810                    return Ok(e);
11811                }
11812                let args = self.parse_builtin_args()?;
11813                Ok(Expr {
11814                    kind: ExprKind::Filesf(args),
11815                    line,
11816                })
11817            }
11818            "fr" => {
11819                let args = self.parse_builtin_args()?;
11820                Ok(Expr {
11821                    kind: ExprKind::FilesfRecursive(args),
11822                    line,
11823                })
11824            }
11825            "dirs" | "d" => {
11826                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11827                    return Ok(e);
11828                }
11829                let args = self.parse_builtin_args()?;
11830                Ok(Expr {
11831                    kind: ExprKind::Dirs(args),
11832                    line,
11833                })
11834            }
11835            "dr" => {
11836                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11837                    return Ok(e);
11838                }
11839                let args = self.parse_builtin_args()?;
11840                Ok(Expr {
11841                    kind: ExprKind::DirsRecursive(args),
11842                    line,
11843                })
11844            }
11845            "sym_links" => {
11846                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11847                    return Ok(e);
11848                }
11849                let args = self.parse_builtin_args()?;
11850                Ok(Expr {
11851                    kind: ExprKind::SymLinks(args),
11852                    line,
11853                })
11854            }
11855            "sockets" => {
11856                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11857                    return Ok(e);
11858                }
11859                let args = self.parse_builtin_args()?;
11860                Ok(Expr {
11861                    kind: ExprKind::Sockets(args),
11862                    line,
11863                })
11864            }
11865            "pipes" => {
11866                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11867                    return Ok(e);
11868                }
11869                let args = self.parse_builtin_args()?;
11870                Ok(Expr {
11871                    kind: ExprKind::Pipes(args),
11872                    line,
11873                })
11874            }
11875            "block_devices" => {
11876                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11877                    return Ok(e);
11878                }
11879                let args = self.parse_builtin_args()?;
11880                Ok(Expr {
11881                    kind: ExprKind::BlockDevices(args),
11882                    line,
11883                })
11884            }
11885            "char_devices" => {
11886                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11887                    return Ok(e);
11888                }
11889                let args = self.parse_builtin_args()?;
11890                Ok(Expr {
11891                    kind: ExprKind::CharDevices(args),
11892                    line,
11893                })
11894            }
11895            "exe" | "executables" => {
11896                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11897                    return Ok(e);
11898                }
11899                let args = self.parse_builtin_args()?;
11900                Ok(Expr {
11901                    kind: ExprKind::Executables(args),
11902                    line,
11903                })
11904            }
11905            "glob" => {
11906                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11907                    return Ok(e);
11908                }
11909                let args = self.parse_builtin_args()?;
11910                Ok(Expr {
11911                    kind: ExprKind::Glob(args),
11912                    line,
11913                })
11914            }
11915            "glob_par" => {
11916                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11917                    return Ok(e);
11918                }
11919                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11920                Ok(Expr {
11921                    kind: ExprKind::GlobPar { args, progress },
11922                    line,
11923                })
11924            }
11925            "par_sed" => {
11926                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11927                    return Ok(e);
11928                }
11929                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11930                Ok(Expr {
11931                    kind: ExprKind::ParSed { args, progress },
11932                    line,
11933                })
11934            }
11935            "bless" => {
11936                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11937                    return Ok(e);
11938                }
11939                let args = self.parse_builtin_args()?;
11940                Ok(Expr {
11941                    kind: ExprKind::Bless {
11942                        ref_expr: Box::new(args[0].clone()),
11943                        class: args.get(1).cloned().map(Box::new),
11944                    },
11945                    line,
11946                })
11947            }
11948            "caller" => {
11949                if matches!(self.peek(), Token::LParen) {
11950                    self.advance();
11951                    if matches!(self.peek(), Token::RParen) {
11952                        self.advance();
11953                        Ok(Expr {
11954                            kind: ExprKind::Caller(None),
11955                            line,
11956                        })
11957                    } else {
11958                        let a = self.parse_expression()?;
11959                        self.expect(&Token::RParen)?;
11960                        Ok(Expr {
11961                            kind: ExprKind::Caller(Some(Box::new(a))),
11962                            line,
11963                        })
11964                    }
11965                } else {
11966                    Ok(Expr {
11967                        kind: ExprKind::Caller(None),
11968                        line,
11969                    })
11970                }
11971            }
11972            "wantarray" => {
11973                if matches!(self.peek(), Token::LParen) {
11974                    self.advance();
11975                    self.expect(&Token::RParen)?;
11976                }
11977                Ok(Expr {
11978                    kind: ExprKind::Wantarray,
11979                    line,
11980                })
11981            }
11982            "sub" => {
11983                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
11984                if crate::no_interop_mode() {
11985                    return Err(self.syntax_err(
11986                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
11987                        line,
11988                    ));
11989                }
11990                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
11991                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11992                let body = self.parse_block()?;
11993                Ok(Expr {
11994                    kind: ExprKind::CodeRef { params, body },
11995                    line,
11996                })
11997            }
11998            "fn" => {
11999                // Anonymous fn — stryke syntax for anonymous subroutines
12000                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
12001                self.parse_sub_attributes()?;
12002                let body = self.parse_fn_eq_body_or_block(false)?;
12003                Ok(Expr {
12004                    kind: ExprKind::CodeRef { params, body },
12005                    line,
12006                })
12007            }
12008            _ => {
12009                // Generic function call
12010                // Check for fat arrow (bareword string in hash) — except for
12011                // topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …), which must
12012                // resolve to the topic value, not the literal name.
12013                if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name)
12014                {
12015                    return Ok(Expr {
12016                        kind: ExprKind::String(name),
12017                        line,
12018                    });
12019                }
12020                // Bare `_` in expression position → topic variable `$_`.
12021                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
12022                // Also handles the outer-topic chain: `_<`, `_<<`, `_<<<`,
12023                // `_<<<<` for 1..4 frames up — and the positional matrix:
12024                // `_0<<<<`, `_1<<<<`, `_N<<<<` (N positionals × 5 levels).
12025                // `_0` is canonically aliased to `_` at every level (see
12026                // `Scope::set_closure_args`).
12027                //
12028                // Stryke string-index sugar: `_[N]` (bareword, no sigil) is
12029                // an alias for `_!N!` — char-of-topic substring. The sigil
12030                // form `$_[N]` keeps Perl's `@_`-access semantics (first
12031                // positional arg). We dispatch here, before the generic
12032                // ArrayElement path, so the AST for `_[N]` carries the
12033                // synthetic `__topicstr__$NAME` flag the interpreter / VM
12034                // strip and route to char-of-string.
12035                if Self::is_underscore_topic_slot(&name) {
12036                    if matches!(self.peek(), Token::LBracket) && self.peek_line() == line {
12037                        self.advance(); // [
12038                        let index = self.parse_expression()?;
12039                        self.expect(&Token::RBracket)?;
12040                        return Ok(Expr {
12041                            kind: ExprKind::ArrayElement {
12042                                array: format!("__topicstr__{}", name),
12043                                index: Box::new(index),
12044                            },
12045                            line,
12046                        });
12047                    }
12048                    return Ok(Expr {
12049                        kind: ExprKind::ScalarVar(name.clone()),
12050                        line,
12051                    });
12052                }
12053                // Function call with optional parens
12054                if matches!(self.peek(), Token::LParen) {
12055                    self.advance();
12056                    let args = self.parse_arg_list()?;
12057                    self.expect(&Token::RParen)?;
12058                    Ok(Expr {
12059                        kind: ExprKind::FuncCall { name, args },
12060                        line,
12061                    })
12062                } else if self.peek().is_term_start()
12063                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
12064                        && matches!(self.peek_at(1), Token::Ident(_)))
12065                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
12066                    && !(matches!(self.peek(), Token::LBrace)
12067                        && self.peek_line() > self.prev_line())
12068                    && !(matches!(self.peek(), Token::BitNot)
12069                        && self.suppress_tilde_range == 0
12070                        && matches!(
12071                            self.peek_at(1),
12072                            Token::Ident(_) | Token::Integer(_) | Token::Float(_)
12073                        ))
12074                {
12075                    // Perl allows func arg without parens
12076                    // Guard: `sub <name> { }` is a named sub declaration (new
12077                    // statement), not an argument to the preceding call.
12078                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
12079                    // barewords (used by thread macro so `t Color::Red p` treats
12080                    // `p` as a stage, not an argument to the enum variant), but
12081                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
12082                    // Guard: `{` on a new line is a new statement (hashref/block),
12083                    // not an argument to the preceding bareword call.
12084                    // Guard: `~Ident` / `~Integer` / `~Float` after a bareword is
12085                    // the universal-tilde range separator (`I~M~5`, `Mon~Fri`,
12086                    // `Jan~Dec~2`), not unary BitNot of an arg. Bail to Bareword
12087                    // so the outer `parse_range` consumes `~` as the range op.
12088                    let args = self.parse_list_until_terminator()?;
12089                    Ok(Expr {
12090                        kind: ExprKind::FuncCall { name, args },
12091                        line,
12092                    })
12093                } else {
12094                    // No parens, no visible arguments — emit a Bareword.
12095                    // At runtime, Bareword tries sub resolution first (zero-arg
12096                    // call) and falls back to a string value.  stryke extension
12097                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
12098                    // with `$_` injection separately.
12099                    Ok(Expr {
12100                        kind: ExprKind::Bareword(name),
12101                        line,
12102                    })
12103                }
12104            }
12105        }
12106    }
12107
12108    fn parse_print_like(
12109        &mut self,
12110        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
12111    ) -> PerlResult<Expr> {
12112        let line = self.peek_line();
12113        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
12114        let handle = if let Token::Ident(ref h) = self.peek().clone() {
12115            if h.chars().all(|c| c.is_uppercase() || c == '_')
12116                && !matches!(self.peek(), Token::LParen)
12117            {
12118                let h = h.clone();
12119                let saved = self.pos;
12120                self.advance();
12121                // Verify next token is a term start (not operator).
12122                // Guard: `~Ident` / `~Integer` / `~Float` is a universal-tilde
12123                // range separator (`p I~M~5`, `p Mon~Fri`), not unary BitNot of
12124                // an arg. Bail filehandle detection so the bareword `I` flows
12125                // into the regular expression path where `parse_range` consumes
12126                // `~` as the range op.
12127                let is_tilde_range_after = matches!(self.peek(), Token::BitNot)
12128                    && self.suppress_tilde_range == 0
12129                    && matches!(
12130                        self.peek_at(1),
12131                        Token::Ident(_) | Token::Integer(_) | Token::Float(_)
12132                    );
12133                if !is_tilde_range_after
12134                    && (self.peek().is_term_start()
12135                        || matches!(
12136                            self.peek(),
12137                            Token::DoubleString(_)
12138                                | Token::BacktickString(_)
12139                                | Token::SingleString(_)
12140                        ))
12141                {
12142                    Some(h)
12143                } else {
12144                    self.pos = saved;
12145                    None
12146                }
12147            } else {
12148                None
12149            }
12150        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
12151            // `print $fh "msg"` — scalar variable as indirect filehandle.
12152            // Treat as handle when the next token (after $var) is a term-start or
12153            // string literal *without* a preceding comma/operator, matching Perl's
12154            // indirect-object heuristic.
12155            // Exclude `$_` — it's virtually always the topic variable, not a handle.
12156            // Exclude `[` and `{` — those are array/hash subscripts on the variable
12157            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
12158            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
12159            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
12160            let v = v.clone();
12161            if v == "_" {
12162                None
12163            } else {
12164                let saved = self.pos;
12165                self.advance();
12166                let next = self.peek().clone();
12167                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
12168                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
12169                if !is_stmt_modifier
12170                    && !matches!(next, Token::LBracket | Token::LBrace)
12171                    && (next.is_term_start()
12172                        || matches!(
12173                            next,
12174                            Token::DoubleString(_)
12175                                | Token::BacktickString(_)
12176                                | Token::SingleString(_)
12177                        ))
12178                {
12179                    // Next token looks like a print argument — $var is the handle.
12180                    Some(format!("${v}"))
12181                } else {
12182                    self.pos = saved;
12183                    None
12184                }
12185            }
12186        } else {
12187            None
12188        };
12189        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
12190        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
12191        // are given, prints $_." (Same convention as the topic-default unary
12192        // builtins handled in `parse_one_arg_or_default`.)
12193        let args =
12194            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
12195                let line_topic = self.peek_line();
12196                self.advance(); // (
12197                self.advance(); // )
12198                vec![Expr {
12199                    kind: ExprKind::ScalarVar("_".into()),
12200                    line: line_topic,
12201                }]
12202            } else {
12203                self.parse_list_until_terminator()?
12204            };
12205        Ok(Expr {
12206            kind: make(handle, args),
12207            line,
12208        })
12209    }
12210
12211    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
12212        let block = self.parse_block()?;
12213        let block_end_line = self.prev_line();
12214        self.eat(&Token::Comma);
12215        // On the RHS of `|>`, the list operand is supplied by the piped LHS
12216        // and will be substituted at desugar time — accept a placeholder when
12217        // we're at a terminator here or on a new line (implicit semicolon).
12218        if self.in_pipe_rhs()
12219            && (matches!(
12220                self.peek(),
12221                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12222            ) || self.peek_line() > block_end_line)
12223        {
12224            let line = self.peek_line();
12225            return Ok((block, self.pipe_placeholder_list(line)));
12226        }
12227        let list = self.parse_expression()?;
12228        Ok((block, list))
12229    }
12230
12231    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
12232    /// When `paren` is true, stops at `)` as well as normal terminators.
12233    fn parse_comma_expr_list_with_timeout_tail(
12234        &mut self,
12235        paren: bool,
12236    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
12237        let mut parts = vec![self.parse_assign_expr()?];
12238        loop {
12239            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12240                break;
12241            }
12242            if paren && matches!(self.peek(), Token::RParen) {
12243                break;
12244            }
12245            if matches!(
12246                self.peek(),
12247                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12248            ) {
12249                break;
12250            }
12251            if self.peek_is_postfix_stmt_modifier_keyword() {
12252                break;
12253            }
12254            if let Token::Ident(ref kw) = self.peek().clone() {
12255                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
12256                    self.advance();
12257                    self.expect(&Token::FatArrow)?;
12258                    let t = self.parse_assign_expr()?;
12259                    return Ok((parts, Some(t)));
12260                }
12261            }
12262            parts.push(self.parse_assign_expr()?);
12263        }
12264        Ok((parts, None))
12265    }
12266
12267    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
12268    fn parse_init_block_then_list_optional_progress(
12269        &mut self,
12270    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
12271        let init = self.parse_assign_expr()?;
12272        self.expect(&Token::Comma)?;
12273        let block = self.parse_block_or_bareword_block()?;
12274        self.eat(&Token::Comma);
12275        let line = self.peek_line();
12276        if let Token::Ident(ref kw) = self.peek().clone() {
12277            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12278                self.advance();
12279                self.expect(&Token::FatArrow)?;
12280                let prog = self.parse_assign_expr()?;
12281                return Ok((
12282                    init,
12283                    block,
12284                    Expr {
12285                        kind: ExprKind::List(vec![]),
12286                        line,
12287                    },
12288                    Some(prog),
12289                ));
12290            }
12291        }
12292        if matches!(
12293            self.peek(),
12294            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12295        ) {
12296            return Ok((
12297                init,
12298                block,
12299                Expr {
12300                    kind: ExprKind::List(vec![]),
12301                    line,
12302                },
12303                None,
12304            ));
12305        }
12306        let mut parts = vec![self.parse_assign_expr()?];
12307        loop {
12308            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12309                break;
12310            }
12311            if matches!(
12312                self.peek(),
12313                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12314            ) {
12315                break;
12316            }
12317            if self.peek_is_postfix_stmt_modifier_keyword() {
12318                break;
12319            }
12320            if let Token::Ident(ref kw) = self.peek().clone() {
12321                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12322                    self.advance();
12323                    self.expect(&Token::FatArrow)?;
12324                    let prog = self.parse_assign_expr()?;
12325                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
12326                }
12327            }
12328            parts.push(self.parse_assign_expr()?);
12329        }
12330        Ok((init, block, merge_expr_list(parts), None))
12331    }
12332
12333    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
12334    fn parse_cluster_block_then_list_optional_progress(
12335        &mut self,
12336    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
12337        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
12338        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
12339        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
12340        let cluster = self.parse_assign_expr();
12341        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
12342        let cluster = cluster?;
12343        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
12344        self.eat(&Token::Comma);
12345        let block = self.parse_block_or_bareword_block()?;
12346        let block_end_line = self.prev_line();
12347        self.eat(&Token::Comma);
12348        let line = self.peek_line();
12349        if let Token::Ident(ref kw) = self.peek().clone() {
12350            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12351                self.advance();
12352                self.expect(&Token::FatArrow)?;
12353                let prog = self.parse_assign_expr_stop_at_pipe()?;
12354                return Ok((
12355                    cluster,
12356                    block,
12357                    Expr {
12358                        kind: ExprKind::List(vec![]),
12359                        line,
12360                    },
12361                    Some(prog),
12362                ));
12363            }
12364        }
12365        let empty_list_ok = matches!(
12366            self.peek(),
12367            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12368        ) || (self.in_pipe_rhs()
12369            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
12370        if empty_list_ok {
12371            return Ok((
12372                cluster,
12373                block,
12374                Expr {
12375                    kind: ExprKind::List(vec![]),
12376                    line,
12377                },
12378                None,
12379            ));
12380        }
12381        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
12382        loop {
12383            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12384                break;
12385            }
12386            if matches!(
12387                self.peek(),
12388                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12389            ) {
12390                break;
12391            }
12392            if self.peek_is_postfix_stmt_modifier_keyword() {
12393                break;
12394            }
12395            if let Token::Ident(ref kw) = self.peek().clone() {
12396                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12397                    self.advance();
12398                    self.expect(&Token::FatArrow)?;
12399                    let prog = self.parse_assign_expr_stop_at_pipe()?;
12400                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
12401                }
12402            }
12403            parts.push(self.parse_assign_expr_stop_at_pipe()?);
12404        }
12405        Ok((cluster, block, merge_expr_list(parts), None))
12406    }
12407
12408    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
12409    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
12410    ///
12411    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
12412    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
12413    /// stage — individual list parts and the progress value parse through
12414    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
12415    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
12416    fn parse_block_then_list_optional_progress(
12417        &mut self,
12418    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
12419        let block = self.parse_block_or_bareword_block()?;
12420        let block_end_line = self.prev_line();
12421        self.eat(&Token::Comma);
12422        let line = self.peek_line();
12423        if let Token::Ident(ref kw) = self.peek().clone() {
12424            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12425                self.advance();
12426                self.expect(&Token::FatArrow)?;
12427                let prog = self.parse_assign_expr_stop_at_pipe()?;
12428                return Ok((
12429                    block,
12430                    Expr {
12431                        kind: ExprKind::List(vec![]),
12432                        line,
12433                    },
12434                    Some(prog),
12435                ));
12436            }
12437        }
12438        // An empty list operand is allowed when the next token terminates the
12439        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
12440        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
12441        // terminator — left-associative chaining leaves the outer `|>` for
12442        // the enclosing pipe-forward loop. A newline after the block also
12443        // terminates in pipe-RHS — the LHS supplies the list, so we must NOT
12444        // greedily eat the next statement (matches `parse_block_list`).
12445        let empty_list_ok = matches!(
12446            self.peek(),
12447            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12448        ) || (self.in_pipe_rhs()
12449            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
12450        if empty_list_ok {
12451            return Ok((
12452                block,
12453                Expr {
12454                    kind: ExprKind::List(vec![]),
12455                    line,
12456                },
12457                None,
12458            ));
12459        }
12460        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
12461        loop {
12462            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12463                break;
12464            }
12465            if matches!(
12466                self.peek(),
12467                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12468            ) {
12469                break;
12470            }
12471            if self.peek_is_postfix_stmt_modifier_keyword() {
12472                break;
12473            }
12474            if let Token::Ident(ref kw) = self.peek().clone() {
12475                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12476                    self.advance();
12477                    self.expect(&Token::FatArrow)?;
12478                    let prog = self.parse_assign_expr_stop_at_pipe()?;
12479                    return Ok((block, merge_expr_list(parts), Some(prog)));
12480                }
12481            }
12482            parts.push(self.parse_assign_expr_stop_at_pipe()?);
12483        }
12484        Ok((block, merge_expr_list(parts), None))
12485    }
12486
12487    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
12488    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
12489        // `fan { BLOCK }` — no count
12490        if matches!(self.peek(), Token::LBrace) {
12491            let block = self.parse_block()?;
12492            return Ok((None, block));
12493        }
12494        let saved = self.pos;
12495        // Not a brace — first expr could be count or body
12496        let first = self.parse_postfix()?;
12497        if matches!(self.peek(), Token::LBrace) {
12498            // `fan COUNT { BLOCK }`
12499            let block = self.parse_block()?;
12500            Ok((Some(Box::new(first)), block))
12501        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
12502            || (matches!(self.peek(), Token::Comma)
12503                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
12504        {
12505            // `fan EXPR;` — no count, first is the body
12506            let block = self.bareword_to_no_arg_block(first);
12507            Ok((None, block))
12508        } else if matches!(first.kind, ExprKind::Integer(_)) {
12509            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
12510            self.eat(&Token::Comma);
12511            let body = self.parse_fan_blockless_body(line)?;
12512            Ok((Some(Box::new(first)), body))
12513        } else {
12514            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
12515            // — backtrack and re-parse as a full body expression.
12516            self.pos = saved;
12517            let body = self.parse_fan_blockless_body(line)?;
12518            Ok((None, body))
12519        }
12520    }
12521
12522    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
12523    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
12524        if matches!(self.peek(), Token::LBrace) {
12525            return self.parse_block();
12526        }
12527        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
12528        if let Token::Ident(ref name) = self.peek().clone() {
12529            if matches!(
12530                self.peek_at(1),
12531                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12532            ) {
12533                let name = name.clone();
12534                self.advance();
12535                let body = Expr {
12536                    kind: ExprKind::FuncCall { name, args: vec![] },
12537                    line,
12538                };
12539                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12540            }
12541        }
12542        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
12543        let expr = self.parse_assign_expr_stop_at_pipe()?;
12544        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12545    }
12546
12547    /// Wrap a parsed expression as a single-statement block, converting bare
12548    /// identifiers to zero-arg calls (`work` → `work()`).
12549    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
12550        let line = expr.line;
12551        let body = match &expr.kind {
12552            ExprKind::Bareword(name) => Expr {
12553                kind: ExprKind::FuncCall {
12554                    name: name.clone(),
12555                    args: vec![],
12556                },
12557                line,
12558            },
12559            _ => expr,
12560        };
12561        vec![Statement::new(StmtKind::Expression(body), line)]
12562    }
12563
12564    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
12565    ///
12566    /// When the next token is `{`, delegates to [`Self::parse_block`].
12567    /// Otherwise parses a single postfix expression and wraps it as a call
12568    /// with `$_` as argument (for barewords) or a plain expression statement:
12569    ///
12570    /// - Bareword `foo` → `{ foo($_) }`
12571    /// - Other expr     → `{ EXPR }`
12572    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
12573        if matches!(self.peek(), Token::LBrace) {
12574            return self.parse_block();
12575        }
12576        let line = self.peek_line();
12577        // A lone identifier followed by a list-terminator is a bare sub name:
12578        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
12579        if let Token::Ident(ref name) = self.peek().clone() {
12580            if matches!(
12581                self.peek_at(1),
12582                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12583            ) {
12584                let name = name.clone();
12585                self.advance();
12586                let body = Expr {
12587                    kind: ExprKind::FuncCall {
12588                        name,
12589                        args: vec![Expr {
12590                            kind: ExprKind::ScalarVar("_".to_string()),
12591                            line,
12592                        }],
12593                    },
12594                    line,
12595                };
12596                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12597            }
12598        }
12599        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
12600        let expr = self.parse_assign_expr_stop_at_pipe()?;
12601        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12602    }
12603
12604    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
12605    /// bare function takes no args (body runs stand-alone, not per-element).
12606    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
12607    /// greedily swallow subsequent tokens as function arguments.
12608    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
12609        if matches!(self.peek(), Token::LBrace) {
12610            return self.parse_block();
12611        }
12612        let line = self.peek_line();
12613        if let Token::Ident(ref name) = self.peek().clone() {
12614            if matches!(
12615                self.peek_at(1),
12616                Token::Comma
12617                    | Token::Semicolon
12618                    | Token::RBrace
12619                    | Token::Eof
12620                    | Token::PipeForward
12621                    | Token::Integer(_)
12622            ) {
12623                let name = name.clone();
12624                self.advance();
12625                let body = Expr {
12626                    kind: ExprKind::FuncCall { name, args: vec![] },
12627                    line,
12628                };
12629                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12630            }
12631        }
12632        let expr = self.parse_postfix()?;
12633        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12634    }
12635
12636    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
12637    /// treated as a bare sub name (e.g. inside `sort`).
12638    /// True for any bareword the parser treats as a known builtin / keyword —
12639    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
12640    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
12641    /// as a comparator name if it *isn't* a known bareword). Previously named
12642    /// `is_perl_keyword`, which was misleading.
12643    fn is_known_bareword(name: &str) -> bool {
12644        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
12645    }
12646
12647    /// True iff `name` appears as any spelling (primary *or* alias) in a
12648    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
12649    /// up in the parser-level keyword lists but are still callable at
12650    /// runtime — so `map { tj }` can default to `tj($_)` the same way
12651    /// `map { to_json }` does.
12652    fn is_try_builtin_name(name: &str) -> bool {
12653        crate::builtins::BUILTIN_ARMS
12654            .iter()
12655            .any(|arm| arm.contains(&name))
12656    }
12657
12658    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
12659    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
12660    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
12661    /// is derived from this list by `build.rs`.
12662    fn is_perl5_core(name: &str) -> bool {
12663        matches!(
12664            name,
12665            // ── array / list ────────────────────────────────────────────
12666            "map" | "grep" | "sort" | "reverse" | "join" | "split"
12667            | "push" | "pop" | "shift" | "unshift" | "splice"
12668            | "splice_last" | "splice1" | "spl_last"
12669            | "pack" | "unpack"
12670            | "unpack_first" | "unpack1" | "up1"
12671            // ── hash ────────────────────────────────────────────────────
12672            | "keys" | "values" | "each"
12673            // ── string ──────────────────────────────────────────────────
12674            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
12675            | "lc" | "uc" | "lcfirst" | "ucfirst"
12676            | "length" | "substr" | "index" | "rindex"
12677            | "sprintf" | "printf" | "print" | "say"
12678            | "pos" | "quotemeta" | "study"
12679            // ── numeric ─────────────────────────────────────────────────
12680            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
12681            | "exp" | "log" | "rand" | "srand"
12682            // ── time ────────────────────────────────────────────────────
12683            | "time" | "localtime" | "gmtime"
12684            // ── type / reflection ───────────────────────────────────────
12685            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
12686            | "caller" | "delete" | "exists" | "bless" | "prototype"
12687            | "tie" | "untie" | "tied"
12688            // ── io ──────────────────────────────────────────────────────
12689            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
12690            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
12691            | "format" | "formline" | "select" | "vec"
12692            | "sysopen" | "sysread" | "sysseek" | "syswrite"
12693            // ── filesystem ──────────────────────────────────────────────
12694            | "stat" | "lstat" | "rename" | "unlink" | "utime"
12695            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
12696            | "glob" | "opendir" | "readdir" | "closedir"
12697            | "link" | "readlink" | "symlink"
12698            // ── ipc ─────────────────────────────────────────────────────
12699            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
12700            // ── sysv ipc ────────────────────────────────────────────────
12701            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
12702            | "semctl" | "semget" | "semop"
12703            | "shmctl" | "shmget" | "shmread" | "shmwrite"
12704            // ── process / system ────────────────────────────────────────
12705            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
12706            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
12707            | "chroot" | "times" | "umask" | "reset"
12708            | "getpgrp" | "setpgrp" | "getppid"
12709            | "getpriority" | "setpriority"
12710            // ── socket ──────────────────────────────────────────────────
12711            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
12712            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
12713            | "getpeername" | "getsockname"
12714            // ── posix metadata ──────────────────────────────────────────
12715            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
12716            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
12717            | "getlogin"
12718            | "gethostbyname" | "gethostbyaddr" | "gethostent"
12719            | "getnetbyname" | "getnetent"
12720            | "getprotobyname" | "getprotoent"
12721            | "getservbyname" | "getservent"
12722            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
12723            | "endpwent" | "endgrent"
12724            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
12725            // ── control flow ────────────────────────────────────────────
12726            | "return" | "do" | "eval" | "require"
12727            | "my" | "our" | "local" | "use" | "no"
12728            | "sub" | "if" | "unless" | "while" | "until"
12729            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
12730            | "not" | "and" | "or"
12731            // ── quoting ─────────────────────────────────────────────────
12732            | "qw" | "qq" | "q"
12733            // ── phase blocks ────────────────────────────────────────────
12734            | "BEGIN" | "END"
12735        )
12736    }
12737
12738    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
12739    /// Used by `--compat` to reject extensions at parse time.
12740    fn stryke_extension_name(name: &str) -> Option<&str> {
12741        match name {
12742            // ── aop ────────────────────────────────────────────────────────
12743            | "proceed" | "intercept_list" | "intercept_remove" | "intercept_clear"
12744            // ── parallel ────────────────────────────────────────────────────
12745            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
12746            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
12747            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
12748            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
12749            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
12750            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
12751            | "pmaps" | "pflat_maps" | "pgreps"
12752            // ── functional / iterator ───────────────────────────────────────
12753            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
12754            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
12755            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "flatten" | "set"
12756            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
12757            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
12758            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
12759            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
12760            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
12761            | "zip_with" | "count_by" | "skip" | "first_or"
12762            // ── pipeline / string helpers ───────────────────────────────────
12763            | "input" | "lines" | "words" | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
12764            | "punctuation" | "punct"
12765            | "sentences" | "sents"
12766            | "paragraphs" | "paras" | "sections" | "sects"
12767            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
12768            | "trim" | "avg" | "stddev"
12769            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
12770            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
12771            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
12772            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
12773            | "to_html" | "to_markdown" | "to_table" | "xopen"
12774            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
12775            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
12776            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
12777            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
12778            | "to_hash" | "to_set"
12779            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
12780            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
12781            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
12782            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
12783            | "inc" | "dec" | "elapsed"
12784            // ── filesystem extensions ───────────────────────────────────────
12785            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
12786            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
12787            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
12788            | "copy" | "move" | "spurt" | "spit" | "read_bytes" | "which"
12789            | "getcwd" | "touch" | "gethostname" | "uname"
12790            // ── data / network ──────────────────────────────────────────────
12791            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
12792            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
12793            | "par_fetch" | "par_csv_read" | "par_pipeline"
12794            | "json_encode" | "json_decode" | "json_jq"
12795            | "http_request" | "serve" | "ssh"
12796            | "html_parse" | "css_select" | "xml_parse" | "xpath"
12797            | "smtp_send"
12798            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
12799            | "net_public_ip" | "net_dns" | "net_reverse_dns"
12800            | "net_ping" | "net_port_open" | "net_ports_scan"
12801            | "net_latency" | "net_download" | "net_headers"
12802            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
12803            // ── git ─────────────────────────────────────────────────────────
12804            | "git_log" | "git_status" | "git_diff" | "git_branches"
12805            | "git_tags" | "git_blame" | "git_authors" | "git_files"
12806            | "git_show" | "git_root"
12807            // ── audio / media ───────────────────────────────────────────────
12808            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
12809            // ── pdf ─────────────────────────────────────────────────────────
12810            | "to_pdf" | "pdf_text" | "pdf_pages"
12811            // ── serialization (stryke-only encoders) ────────────────────────
12812            | "toml_encode" | "toml_decode"
12813            | "yaml_encode" | "yaml_decode"
12814            | "xml_encode" | "xml_decode"
12815            // ── crypto / encoding ───────────────────────────────────────────
12816            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
12817            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
12818            | "shake128" | "shake256"
12819            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
12820            | "uuid" | "crc32"
12821            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
12822            | "ripemd160" | "rmd160" | "md4"
12823            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
12824            | "murmur3" | "murmur3_32" | "murmur3_128"
12825            | "siphash" | "siphash_keyed"
12826            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
12827            | "poly1305" | "poly1305_mac"
12828            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
12829            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
12830            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
12831            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
12832            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
12833            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
12834            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
12835            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
12836            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
12837            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
12838            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
12839            | "secretbox" | "secretbox_seal" | "secretbox_open"
12840            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
12841            | "nacl_box_open" | "box_open"
12842            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
12843            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
12844            | "barcode_ean13" | "ean13" | "barcode_svg"
12845            | "argon2_hash" | "argon2" | "argon2_verify"
12846            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
12847            | "scrypt_hash" | "scrypt" | "scrypt_verify"
12848            | "pbkdf2" | "pbkdf2_derive"
12849            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
12850            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
12851            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
12852            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
12853            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
12854            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
12855            | "ecdsa_p256_verify" | "p256_verify"
12856            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
12857            | "ecdsa_p384_verify" | "p384_verify"
12858            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
12859            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
12860            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
12861            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
12862            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
12863            | "ed25519_verify" | "ed_verify"
12864            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
12865            | "base64_encode" | "base64_decode"
12866            | "hex_encode" | "hex_decode"
12867            | "url_encode" | "url_decode"
12868            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
12869            | "brotli" | "br" | "brotli_decode" | "ubr"
12870            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
12871            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
12872            | "lz4" | "lz4_decode" | "unlz4"
12873            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
12874            | "lzw" | "lzw_decode" | "unlzw"
12875            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
12876            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
12877            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
12878            // ── special math functions ────────────────────────────────────────
12879            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
12880            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
12881            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
12882            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
12883            | "gammaincc_reg" | "gamma_ur"
12884            // ── date / time ─────────────────────────────────────────────────
12885            | "datetime_utc" | "datetime_now_tz"
12886            | "datetime_format_tz" | "datetime_add_seconds"
12887            | "datetime_from_epoch"
12888            | "datetime_parse_rfc3339" | "datetime_parse_local"
12889            | "datetime_strftime"
12890            | "dateseq" | "dategrep" | "dateround" | "datesort"
12891            // ── jwt ─────────────────────────────────────────────────────────
12892            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
12893            // ── logging ─────────────────────────────────────────────────────
12894            | "log_info" | "log_warn" | "log_error"
12895            | "log_debug" | "log_trace" | "log_json" | "log_level"
12896            // ── concurrency / timing ────────────────────────────────────────
12897            | "async" | "spawn" | "trace" | "timer" | "bench"
12898            | "eval_timeout" | "retry" | "rate_limit" | "every"
12899            | "gen" | "watch"
12900            // ── caching ────────────────────────────────────────────────────────
12901            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
12902            // ── testing framework ────────────────────────────────────────────
12903            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
12904            | "assert_true" | "assert_false"
12905            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
12906            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
12907            | "test_run" | "run_tests" | "test_skip" | "skip_test" | "skip_assert"
12908            // ── system info ─────────────────────────────────────────────────
12909            | "mounts" | "du" | "du_tree" | "process_list"
12910            | "thread_count" | "pool_info" | "par_bench"
12911            // ── stress testing ──────────────────────────────────────────────
12912            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
12913            | "stress_io" | "sio" | "stress_test" | "st"
12914            | "heat" | "fire" | "fire_and_forget" | "pin"
12915            // ── I/O extensions ──────────────────────────────────────────────
12916            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
12917            | "stdin"
12918            // ── internal ────────────────────────────────────────────────────
12919            | "__stryke_rust_compile"
12920            | "vec_set_value"
12921            // ── short aliases ───────────────────────────────────────────────
12922            | "p" | "rev"
12923            // ── trivial numeric / predicate builtins ────────────────────────
12924            | "even" | "odd" | "zero" | "nonzero"
12925            | "positive" | "pos_n" | "negative" | "neg_n"
12926            | "sign" | "negate" | "double" | "triple" | "half"
12927            | "identity" | "id"
12928            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
12929            | "gcd" | "lcm" | "min2" | "max2"
12930            | "log2" | "log10" | "hypot"
12931            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
12932            | "pow2" | "abs_diff"
12933            | "factorial" | "fact" | "fibonacci" | "fib"
12934            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
12935            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
12936            | "median" | "mode_val" | "variance"
12937            // ── trivial string ops ──────────────────────────────────────────
12938            | "is_empty" | "is_blank" | "is_numeric"
12939            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
12940            | "is_space" | "is_whitespace"
12941            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
12942            | "capitalize" | "cap" | "swap_case" | "repeat"
12943            | "title_case" | "title" | "squish"
12944            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
12945            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
12946            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
12947            // ── trivial type predicates ─────────────────────────────────────
12948            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
12949            | "is_code" | "is_coderef" | "is_ref"
12950            | "is_undef" | "is_defined" | "is_def"
12951            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
12952            // ── hash helpers ────────────────────────────────────────────────
12953            | "invert" | "merge_hash"
12954            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
12955            // ── boolean combinators ─────────────────────────────────────────
12956            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
12957            // ── collection helpers (trivial) ────────────────────────────────
12958            | "riffle" | "intersperse" | "every_nth"
12959            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
12960            // ── base conversion ─────────────────────────────────────────────
12961            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
12962            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
12963            | "bits_count" | "popcount" | "leading_zeros" | "lz"
12964            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
12965            // ── bit ops ─────────────────────────────────────────────────────
12966            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
12967            | "shift_left" | "shl" | "shift_right" | "shr"
12968            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
12969            // ── unit conversions: temperature ───────────────────────────────
12970            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
12971            // ── unit conversions: distance ──────────────────────────────────
12972            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
12973            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
12974            | "yards_to_m" | "m_to_yards"
12975            // ── unit conversions: mass ──────────────────────────────────────
12976            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
12977            | "stone_to_kg" | "kg_to_stone"
12978            // ── unit conversions: digital ───────────────────────────────────
12979            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
12980            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
12981            | "kb_to_mb" | "mb_to_gb"
12982            | "bits_to_bytes" | "bytes_to_bits"
12983            // ── unit conversions: time ──────────────────────────────────────
12984            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
12985            | "seconds_to_hours" | "hours_to_seconds"
12986            | "seconds_to_days" | "days_to_seconds"
12987            | "minutes_to_hours" | "hours_to_minutes"
12988            | "hours_to_days" | "days_to_hours"
12989            // ── date helpers ────────────────────────────────────────────────
12990            | "is_leap_year" | "is_leap" | "days_in_month"
12991            | "month_name" | "month_short"
12992            | "weekday_name" | "weekday_short" | "quarter_of"
12993            // ── now / timestamp ─────────────────────────────────────────────
12994            | "now_ms" | "now_us" | "now_ns"
12995            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
12996            // ── color / ANSI ────────────────────────────────────────────────
12997            | "rgb_to_hex" | "hex_to_rgb"
12998            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
12999            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
13000            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
13001            | "strip_ansi"
13002            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
13003            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
13004            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
13005            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
13006            | "bright_magenta" | "bright_cyan" | "bright_white"
13007            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
13008            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
13009            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
13010            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
13011            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
13012            | "white_bold" | "bold_white"
13013            | "blink" | "rapid_blink" | "hidden" | "overline"
13014            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
13015            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
13016            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
13017            // ── network / validation ────────────────────────────────────────
13018            | "ipv4_to_int" | "int_to_ipv4"
13019            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
13020            // ── path helpers ────────────────────────────────────────────────
13021            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
13022            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
13023            // ── functional primitives ───────────────────────────────────────
13024            | "const_fn" | "always_true" | "always_false"
13025            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
13026            // ── more list helpers ───────────────────────────────────────────
13027            | "count_eq" | "count_ne" | "all_eq"
13028            | "all_distinct" | "all_unique" | "has_duplicates"
13029            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
13030            // ── string quote / escape ───────────────────────────────────────
13031            | "quote" | "single_quote" | "unquote"
13032            | "extract_between" | "ellipsis"
13033            // ── random ──────────────────────────────────────────────────────
13034            | "coin_flip" | "dice_roll"
13035            | "random_int" | "random_float" | "random_bool"
13036            | "random_choice" | "random_between"
13037            | "random_string" | "random_alpha" | "random_digit"
13038            // ── symbol table ────────────────────────────────────────────────
13039            | "refresh_stashes"
13040            // ── system introspection ────────────────────────────────────────
13041            | "os_name" | "os_arch" | "num_cpus"
13042            | "pid" | "ppid" | "uid" | "gid"
13043            | "username" | "home_dir" | "temp_dir"
13044            | "mem_total" | "mem_free" | "mem_used"
13045            | "swap_total" | "swap_free" | "swap_used"
13046            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
13047            | "load_avg" | "sys_uptime" | "page_size"
13048            | "os_version" | "os_family" | "endianness" | "pointer_width"
13049            | "proc_mem" | "rss"
13050            // ── collection more ─────────────────────────────────────────────
13051            | "transpose" | "unzip"
13052            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
13053            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
13054            // ── trig / math (batch 2) ───────────────────────────────────────
13055            | "tan" | "asin" | "acos" | "atan"
13056            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
13057            | "sqr" | "cube_fn"
13058            | "mod_op" | "ceil_div" | "floor_div"
13059            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
13060            | "degrees" | "radians"
13061            | "min_abs" | "max_abs"
13062            | "saturate" | "sat01" | "wrap_around"
13063            // ── string (batch 2) ────────────────────────────────────────────
13064            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
13065            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
13066            | "first_word" | "last_word"
13067            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
13068            | "lowercase" | "uppercase"
13069            | "pascal_case" | "pc_case"
13070            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
13071            | "is_palindrome" | "hamming_distance"
13072            | "longest_common_prefix" | "lcp"
13073            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
13074            | "replace_first" | "replace_all_str"
13075            | "contains_any" | "contains_all"
13076            | "starts_with_any" | "ends_with_any"
13077            // ── predicates (batch 2) ────────────────────────────────────────
13078            | "is_pair" | "is_triple"
13079            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
13080            | "is_empty_arr" | "is_empty_hash"
13081            | "is_subset" | "is_superset" | "is_permutation"
13082            // ── collection (batch 2) ────────────────────────────────────────
13083            | "first_eq" | "last_eq"
13084            | "index_of" | "last_index_of" | "positions_of"
13085            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
13086            | "distinct_count" | "longest" | "shortest"
13087            | "array_union" | "list_union"
13088            | "array_intersection" | "list_intersection"
13089            | "array_difference" | "list_difference"
13090            | "symmetric_diff" | "group_of_n" | "chunk_n"
13091            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
13092            // ── hash ops (batch 2) ──────────────────────────────────────────
13093            | "pick_keys" | "pick" | "omit_keys" | "omit"
13094            | "map_keys_fn" | "map_values_fn"
13095            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
13096            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
13097            // ── date (batch 2) ──────────────────────────────────────────────
13098            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
13099            // ── json helpers ────────────────────────────────────────────────
13100            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
13101            // ── process / env ───────────────────────────────────────────────
13102            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
13103            | "argc" | "script_name"
13104            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
13105            // ── id helpers ──────────────────────────────────────────────────
13106            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
13107            // ── url / email parts ───────────────────────────────────────────
13108            | "email_domain" | "email_local"
13109            | "url_host" | "url_path" | "url_query" | "url_scheme"
13110            // ── file stat / path ────────────────────────────────────────────
13111            | "file_size" | "fsize" | "file_mtime" | "mtime"
13112            | "file_atime" | "atime" | "file_ctime" | "ctime"
13113            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
13114            | "path_is_abs" | "path_is_rel"
13115            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
13116            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
13117            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
13118            | "reverse_list" | "list_reverse"
13119            | "without" | "without_nth" | "take_last" | "drop_last"
13120            | "pairwise" | "zipmap"
13121            | "format_bytes" | "human_bytes"
13122            | "format_duration" | "human_duration"
13123            | "format_number" | "group_number"
13124            | "format_percent" | "pad_number"
13125            | "spaceship" | "cmp_num" | "cmp_str"
13126            | "compare_versions" | "version_cmp"
13127            | "hash_insert" | "hash_update" | "hash_delete"
13128            | "matches_regex" | "re_match"
13129            | "count_regex_matches" | "regex_extract"
13130            | "regex_split_str" | "regex_replace_str"
13131            | "shuffle_chars" | "random_char" | "nth_word"
13132            | "head_lines" | "tail_lines" | "count_substring"
13133            | "is_valid_hex" | "hex_upper" | "hex_lower"
13134            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
13135            | "us_to_ns" | "ns_to_us"
13136            | "liters_to_gallons" | "gallons_to_liters"
13137            | "liters_to_ml" | "ml_to_liters"
13138            | "cups_to_ml" | "ml_to_cups"
13139            | "newtons_to_lbf" | "lbf_to_newtons"
13140            | "joules_to_cal" | "cal_to_joules"
13141            | "watts_to_hp" | "hp_to_watts"
13142            | "pascals_to_psi" | "psi_to_pascals"
13143            | "bar_to_pascals" | "pascals_to_bar"
13144            // ── algebraic match ─────────────────────────────────────────────
13145            | "match"
13146            // ── clojure stdlib (only names not matched above) ─────────────────
13147            | "fst" | "rest" | "rst" | "second" | "snd"
13148            | "last_clj" | "lastc" | "butlast" | "bl"
13149            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
13150            | "cons" | "conj"
13151            | "peek_clj" | "pkc" | "pop_clj" | "popc"
13152            | "some" | "not_any" | "not_every"
13153            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
13154            | "fnil" | "juxt"
13155            | "memoize" | "memo" | "curry" | "once"
13156            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
13157            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
13158            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
13159            | "reductions" | "rdcs"
13160            | "partition_by" | "pby" | "partition_all" | "pall"
13161            | "split_at" | "spat" | "split_with" | "spw"
13162            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
13163            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
13164            | "apply" | "appl"
13165            // ── python/ruby stdlib ───────────────────────────────────────────
13166            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
13167            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
13168            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
13169            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
13170            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
13171            | "each_slice" | "eslice" | "each_cons" | "econs"
13172            | "one" | "none_match" | "nonem"
13173            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
13174            | "minmax" | "mmx" | "minmax_by" | "mmxb"
13175            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
13176            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
13177            | "sum_by" | "sumb" | "uniq_by" | "uqb"
13178            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
13179            | "step" | "upto" | "downto"
13180            // ── javascript array/object methods ─────────────────────────────
13181            | "find_last" | "fndl" | "find_last_index" | "fndli"
13182            | "at_index" | "ati" | "replace_at" | "repa"
13183            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
13184            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
13185            | "object_keys" | "okeys" | "object_values" | "ovals"
13186            | "object_entries" | "oents" | "object_from_entries" | "ofents"
13187            // ── haskell list functions ──────────────────────────────────────
13188            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
13189            | "nub" | "sort_on" | "srton"
13190            | "intersperse_val" | "isp" | "intercalate" | "ical"
13191            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
13192            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
13193            // ── rust iterator methods ───────────────────────────────────────
13194            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
13195            | "partition_either" | "peith" | "try_fold" | "tfld"
13196            | "map_while" | "mapw" | "inspect" | "insp"
13197            // ── ruby enumerable extras ──────────────────────────────────────
13198            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
13199            // ── go/general functional utilities ─────────────────────────────
13200            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
13201            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
13202            | "lines_from" | "lfrm" | "unlines" | "unlns"
13203            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
13204            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
13205            | "interpose" | "ipos" | "partition_n" | "partn"
13206            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
13207            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
13208            // ── additional missing stdlib functions ─────────────────────────
13209            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
13210            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
13211            | "each_with_object" | "ewo" | "reduce_right" | "redr"
13212            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
13213            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
13214            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
13215            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
13216            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
13217            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
13218            | "union_list" | "unionl" | "intersect_list" | "intl"
13219            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
13220            // ── Extended stdlib: Text Processing ─────────────────────────────
13221            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
13222            | "split_regex" | "splre" | "replace_regex" | "replre"
13223            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
13224            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
13225            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
13226            | "pluralize" | "plur" | "ordinalize" | "ordn"
13227            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
13228            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
13229            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
13230            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
13231            // ── Extended stdlib: Advanced Numeric ────────────────────────────
13232            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
13233            | "dot_product" | "dotp" | "cross_product" | "crossp"
13234            | "matrix_mul" | "matmul" | "mm"
13235            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
13236            | "distance" | "dist" | "manhattan_distance" | "mdist"
13237            | "covariance" | "cov" | "correlation" | "corr"
13238            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
13239            | "in_range" | "inrng" | "wrap_range" | "wrprng"
13240            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
13241            // ── Extended stdlib: Date/Time ───────────────────────────────────
13242            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
13243            | "diff_days" | "diffd" | "diff_hours" | "diffh"
13244            | "start_of_day" | "sod" | "end_of_day" | "eod"
13245            | "start_of_hour" | "soh" | "start_of_minute" | "som"
13246            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
13247            | "urle" | "urld"
13248            | "html_encode" | "htmle" | "html_decode" | "htmld"
13249            | "adler32" | "adl32" | "fnv1a" | "djb2"
13250            // ── Extended stdlib: Validation ──────────────────────────────────
13251            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
13252            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
13253            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
13254            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
13255            // ── Extended stdlib: Collection Advanced ─────────────────────────
13256            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
13257            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
13258            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
13259            | "partition_point" | "ppt" | "lower_bound" | "lbound"
13260            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
13261            // ── Extended stdlib: Matrix Operations ───────────────────────────
13262            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
13263            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
13264            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
13265            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
13266            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
13267            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
13268            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
13269            // ── Extended stdlib: Graph Algorithms ────────────────────────────
13270            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
13271            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
13272            | "connected_components_graph" | "ccgraph"
13273            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
13274            // ── Extended stdlib: Data Validation ─────────────────────────────
13275            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
13276            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
13277            | "is_hostname_valid" | "ishost"
13278            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
13279            | "is_iso_datetime" | "isisodtm"
13280            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
13281            // ── Extended stdlib: String Utilities Novel ──────────────────────
13282            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
13283            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
13284            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
13285            | "find_all_indices" | "fndalli"
13286            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
13287            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
13288            // ── Extended stdlib: Math Novel ──────────────────────────────────
13289            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
13290            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
13291            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
13292            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
13293            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
13294            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
13295            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
13296            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
13297            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
13298            | "longest_run" | "lrun" | "longest_increasing" | "linc"
13299            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
13300            | "majority_element" | "majority" | "kth_largest" | "kthl"
13301            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
13302            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
13303            // ── Extended stdlib batch 3: Set Operations ──────────────────────
13304            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
13305            | "overlap_coefficient" | "overlapcoef"
13306            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
13307            // ── Extended stdlib batch 3: Advanced String ─────────────────────
13308            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
13309            | "hamdist" | "jaro_similarity" | "jarosim"
13310            | "longest_common_substring" | "lcsub"
13311            | "longest_common_subsequence" | "lcseq"
13312            | "count_words" | "wcount" | "count_lines" | "lcount"
13313            | "count_chars" | "ccount" | "count_bytes" | "bcount"
13314            // ── Extended stdlib batch 3: More Math ───────────────────────────
13315            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
13316            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
13317            | "mobius" | "mob" | "is_squarefree" | "issqfr"
13318            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
13319            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
13320            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
13321            | "day_of_year" | "doy" | "week_of_year" | "woy"
13322            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
13323            | "age_in_years" | "ageyrs"
13324            // ── functional combinators ──────────────────────────────────────
13325
13326            | "when_true" | "when_false" | "if_else" | "clamp_fn"
13327            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
13328            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
13329            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
13330            | "coalesce" | "default_to" | "fallback"
13331            | "apply_list" | "zip_apply" | "scan"
13332            | "keep_if" | "reject_if" | "group_consecutive"
13333            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
13334
13335            // ── matrix / linear algebra ─────────────────────────────────────
13336
13337
13338            | "matrix_multiply" | "mat_mul"
13339            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
13340
13341
13342
13343            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
13344            | "linspace" | "arange"
13345            // ── more regex ──────────────────────────────────────────────────
13346            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
13347            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
13348            // ── more process / system ───────────────────────────────────────
13349            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
13350            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
13351            // ── data structure helpers ───────────────────────────────────────
13352            | "stack_new" | "queue_new" | "lru_new"
13353            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
13354            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
13355            // ── trivial numeric helpers (batch 4) ─────────────────────────────
13356            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
13357            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
13358            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
13359            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
13360            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
13361            // ── math / physics constants ──────────────────────────────────────
13362            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
13363            | "planck" | "speed_of_light" | "sqrt2"
13364            // ── physics formulas ──────────────────────────────────────────────
13365            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
13366            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
13367            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
13368            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
13369            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
13370            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
13371            // ── math functions ────────────────────────────────────────────────
13372            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
13373            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
13374            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
13375            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
13376            | "sigmoid" | "signum" | "square_root"
13377            // ── sequences ─────────────────────────────────────────────────────
13378            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
13379            | "squares_seq" | "triangular_seq"
13380            // ── string helpers (batch 4) ──────────────────────────────────────
13381            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
13382            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
13383            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
13384            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
13385            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
13386            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
13387            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
13388            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
13389            | "xor_strings"
13390            // ── list helpers (batch 4) ─────────────────────────────────────────
13391            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
13392            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
13393            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
13394            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
13395            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
13396            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
13397            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
13398            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
13399            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
13400            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
13401            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
13402            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
13403            | "wrap_index" | "digits_of"
13404            // ── predicates (batch 4) ──────────────────────────────────────────
13405            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
13406            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
13407            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
13408            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
13409            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
13410            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
13411            // ── counters (batch 4) ────────────────────────────────────────────
13412            | "count_digits" | "count_letters" | "count_lower" | "count_match"
13413            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
13414            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
13415            | "truthy_count" | "undef_count"
13416            // ── conversion / utility (batch 4) ────────────────────────────────
13417            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
13418            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
13419            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
13420            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
13421            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
13422            | "range_exclusive" | "range_inclusive"
13423            // ── math / numeric (uncategorized batch) ────────────────────────────
13424            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
13425            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
13426            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
13427            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
13428            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
13429            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
13430            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
13431            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
13432            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
13433            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
13434            | "tribonacci" | "weighted_mean" | "winsorize"
13435            // ── statistics (extended) ─────────────────────────────────────────
13436            | "chi_square_stat" | "describe" | "five_number_summary"
13437            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
13438            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
13439            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
13440            | "z_score" | "z_scores"
13441            // ── number theory / primes ──────────────────────────────────────────
13442            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
13443            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
13444            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
13445            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
13446            // ── geometry / physics ──────────────────────────────────────────────
13447            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
13448            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
13449            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
13450            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
13451            // ── geometry (extended) ───────────────────────────────────────────
13452            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
13453            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
13454            | "frustum_volume" | "haversine_distance" | "line_intersection"
13455            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
13456            | "reflect_point" | "scale_point" | "sector_area"
13457            | "torus_surface" | "torus_volume" | "translate_point"
13458            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
13459            // ── constants ───────────────────────────────────────────────────────
13460            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
13461            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
13462            | "sol" | "tau"
13463            // ── finance ─────────────────────────────────────────────────────────
13464            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
13465            // ── finance (extended) ────────────────────────────────────────────
13466            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
13467            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
13468            | "discounted_payback" | "duration" | "irr"
13469            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
13470            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
13471            | "wacc" | "xirr"
13472            // ── string processing (uncategorized batch) ─────────────────────────
13473            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
13474            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
13475            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
13476            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
13477            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
13478            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
13479            // ── encoding / phonetics ────────────────────────────────────────────
13480            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
13481            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
13482            | "to_emoji_num"
13483            // ── roman numerals ──────────────────────────────────────────────────
13484            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
13485            // ── base / gray code ────────────────────────────────────────────────
13486            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
13487            // ── color operations ────────────────────────────────────────────────
13488            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
13489            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
13490            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
13491            | "rgb_to_hsl" | "rgb_to_hsv"
13492            // ── matrix operations (uncategorized batch) ─────────────────────────
13493            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
13494            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
13495            | "matrix_transpose"
13496            // ── array / list operations (uncategorized batch) ───────────────────
13497            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
13498            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
13499            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
13500            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
13501            | "zero_crossings"
13502            // ── DSP / signal (extended) ───────────────────────────────────────
13503            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
13504            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
13505            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
13506            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
13507            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
13508            // ── validation predicates (uncategorized batch) ─────────────────────
13509            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
13510            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
13511            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
13512            // ── algorithms / puzzles ────────────────────────────────────────────
13513            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
13514            | "sierpinski" | "tower_of_hanoi" | "truth_table"
13515            // ── misc / utility ──────────────────────────────────────────────────
13516            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
13517            // ── math formulas ───────────────────────────────────────────────────
13518            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
13519            | "geometric_series" | "stirling_approx"
13520            | "double_factorial" | "rising_factorial" | "falling_factorial"
13521            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
13522            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
13523            | "map_range"
13524            // ── physics formulas ────────────────────────────────────────────────
13525            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
13526            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
13527            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
13528            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
13529            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
13530            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
13531            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
13532            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
13533            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
13534            | "projectile_range" | "projectile_max_height" | "projectile_time"
13535            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
13536            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
13537            | "lens_power" | "thin_lens" | "magnification_lens"
13538            // ── math constants ──────────────────────────────────────────────────
13539            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
13540            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
13541            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
13542            // ── physics constants ───────────────────────────────────────────────
13543            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
13544            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
13545            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
13546            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
13547            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
13548            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
13549            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
13550            // ── linear algebra (extended) ──────────────────────────────────
13551            | "matrix_solve" | "msolve" | "solve"
13552            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
13553            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
13554            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
13555            | "matrix_pinv" | "mpinv" | "pinv"
13556            | "matrix_cholesky" | "mchol" | "cholesky"
13557            | "matrix_det_general" | "mdetg" | "det"
13558            // ── statistics tests (extended) ────────────────────────────────
13559            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
13560            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
13561            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
13562            | "confidence_interval" | "ci"
13563            // ── distributions (extended) ──────────────────────────────────
13564            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
13565            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
13566            | "t_pdf" | "tpdf" | "student_pdf"
13567            | "f_pdf" | "fpdf" | "fisher_pdf"
13568            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
13569            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
13570            | "pareto_pdf" | "paretopdf"
13571            // ── interpolation & curve fitting ─────────────────────────────
13572            | "lagrange_interp" | "lagrange" | "linterp"
13573            | "cubic_spline" | "cspline" | "spline"
13574            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
13575            // ── numerical integration & differentiation ───────────────────
13576            | "trapz" | "trapezoid" | "simpson" | "simps"
13577            | "numerical_diff" | "numdiff" | "diff_array"
13578            | "cumtrapz" | "cumulative_trapz"
13579            // ── optimization / root finding ────────────────────────────────
13580            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
13581            | "golden_section" | "golden" | "gss"
13582            // ── ODE solvers ───────────────────────────────────────────────
13583            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
13584            // ── graph algorithms (extended) ────────────────────────────────
13585            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
13586            | "floyd_warshall" | "floydwarshall" | "apsp"
13587            | "prim_mst" | "mst" | "prim"
13588            // ── trig extensions ───────────────────────────────────────────
13589            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
13590            // ── ML activation functions ───────────────────────────────────
13591            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
13592            | "silu" | "swish" | "mish" | "softplus"
13593            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
13594            // ── special functions ─────────────────────────────────────────
13595            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
13596            | "lambert_w" | "lambertw" | "productlog"
13597            // ── number theory (extended) ──────────────────────────────────
13598            | "mod_exp" | "modexp" | "powmod"
13599            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
13600            | "miller_rabin" | "millerrabin" | "is_probable_prime"
13601            // ── combinatorics (extended) ──────────────────────────────────
13602            | "derangements" | "stirling2" | "stirling_second"
13603            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
13604            // ── physics (new) ─────────────────────────────────────────────
13605            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
13606            // ── financial greeks & risk ───────────────────────────────────
13607            | "bs_delta" | "bsdelta" | "option_delta"
13608            | "bs_gamma" | "bsgamma" | "option_gamma"
13609            | "bs_vega" | "bsvega" | "option_vega"
13610            | "bs_theta" | "bstheta" | "option_theta"
13611            | "bs_rho" | "bsrho" | "option_rho"
13612            | "bond_duration" | "mac_duration"
13613            // ── DSP extensions ────────────────────────────────────────────
13614            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
13615            // ── encoding extensions ───────────────────────────────────────
13616            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
13617            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
13618            // ── R base: distributions ─────────────────────────────────────
13619            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
13620            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
13621            // ── R base: matrix ops ────────────────────────────────────────
13622            | "rbind" | "cbind"
13623            | "row_sums" | "rowSums" | "col_sums" | "colSums"
13624            | "row_means" | "rowMeans" | "col_means" | "colMeans"
13625            | "outer_product" | "outer" | "crossprod" | "tcrossprod"
13626            | "nrow" | "ncol" | "prop_table" | "proptable"
13627            // ── R base: vector ops ────────────────────────────────────────
13628            | "cummax" | "cummin" | "scale_vec" | "scale"
13629            | "which_fn" | "tabulate"
13630            | "duplicated" | "duped" | "rev_vec"
13631            | "seq_fn" | "rep_fn" | "rep"
13632            | "cut_bins" | "cut" | "find_interval" | "findInterval"
13633            | "ecdf_fn" | "ecdf" | "density_est" | "density"
13634            | "embed_ts" | "embed"
13635            // ── R base: stats tests ───────────────────────────────────────
13636            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
13637            | "wilcox_test" | "wilcox" | "mann_whitney"
13638            | "prop_test" | "proptest" | "binom_test" | "binomtest"
13639            // ── R base: apply / functional ────────────────────────────────
13640            | "sapply" | "tapply" | "do_call" | "docall"
13641            // ── R base: ML / clustering ───────────────────────────────────
13642            | "kmeans" | "prcomp" | "pca"
13643            // ── R base: random generators ─────────────────────────────────
13644            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
13645            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
13646            | "rweibull" | "rlnorm" | "rcauchy"
13647            // ── R base: quantile functions ────────────────────────────────
13648            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
13649            // ── R base: additional CDFs ───────────────────────────────────
13650            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
13651            // ── R base: additional PMFs ───────────────────────────────────
13652            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
13653            // ── R base: smoothing / interpolation ─────────────────────────
13654            | "lowess" | "loess" | "approx_fn" | "approx"
13655            // ── R base: linear models ─────────────────────────────────────
13656            | "lm_fit" | "lm"
13657            // ── R base: remaining quantiles ───────────────────────────────
13658            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
13659            | "qbinom" | "qpois"
13660            // ── R base: time series ───────────────────────────────────────
13661            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
13662            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
13663            // ── R base: regression diagnostics ────────────────────────────
13664            | "predict_lm" | "predict" | "confint_lm" | "confint"
13665            // ── R base: multivariate stats ────────────────────────────────
13666            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
13667            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
13668            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
13669            // ── SVG plotting ──────────────────────────────────────────────
13670            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
13671            | "plot_svg" | "hist_svg" | "histogram_svg"
13672            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
13673            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
13674            | "donut_svg" | "donut" | "area_svg" | "area_chart"
13675            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
13676            | "candlestick_svg" | "candlestick" | "ohlc"
13677            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
13678            | "stacked_bar_svg" | "stacked_bar"
13679            | "wordcloud_svg" | "wordcloud" | "wcloud"
13680            | "treemap_svg" | "treemap"
13681            | "pvw"
13682            // ── Cyberpunk terminal art ────────────────────────────────
13683            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
13684            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
13685            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
13686            // ── AI primitives (docs/AI_PRIMITIVES.md) ─────────────────
13687            | "ai" | "ai_agent" | "prompt" | "stream_prompt" | "stream_prompt_cb"
13688            | "tokens_of"
13689            | "ai_estimate" | "ai_cost" | "ai_history" | "ai_history_clear"
13690            | "ai_cache_clear" | "ai_cache_size"
13691            | "ai_mock_install" | "ai_mock_clear"
13692            | "ai_config_get" | "ai_config_set" | "ai_routing_get" | "ai_routing_set"
13693            | "ai_register_tool" | "ai_unregister_tool" | "ai_clear_tools" | "ai_tools_list"
13694            | "ai_filter" | "ai_map" | "ai_classify" | "ai_match" | "ai_sort" | "ai_dedupe"
13695            | "ai_extract" | "ai_summarize" | "ai_translate" | "ai_template"
13696            | "ai_session_new" | "ai_session_send" | "ai_session_history"
13697            | "ai_session_close" | "ai_session_reset"
13698            | "ai_session_export" | "ai_session_import"
13699            | "ai_memory_save" | "ai_memory_recall" | "ai_memory_forget"
13700            | "ai_memory_count" | "ai_memory_clear"
13701            | "ai_vision" | "ai_pdf" | "ai_grounded" | "ai_citations"
13702            | "ai_transcribe" | "ai_speak" | "ai_image" | "ai_image_edit" | "ai_image_variation"
13703            | "ai_models" | "ai_describe" | "ai_pricing" | "ai_dashboard"
13704            | "ai_moderate" | "ai_chunk" | "ai_warm" | "ai_compare"
13705            | "ai_last_thinking" | "ai_budget" | "ai_batch" | "ai_pmap"
13706            | "ai_file_upload" | "ai_file_list" | "ai_file_get" | "ai_file_delete"
13707            | "ai_file_anthropic_upload" | "ai_file_anthropic_list" | "ai_file_anthropic_delete"
13708            | "vec_cosine" | "vec_search" | "vec_topk"
13709            // ── AI tool specs ────────────────────────────────────────
13710            | "web_search_tool" | "fetch_url_tool" | "read_file_tool" | "run_code_tool"
13711            // ── MCP (Model Context Protocol) ─────────────────────────
13712            | "mcp_connect" | "mcp_close" | "mcp_tools" | "mcp_call"
13713            | "mcp_resource" | "mcp_resources" | "mcp_prompt" | "mcp_prompts"
13714            | "mcp_attach_to_ai" | "mcp_detach_from_ai" | "mcp_attached"
13715            | "mcp_server_start" | "mcp_serve_registered_tools"
13716            // ── PTY / expect (docs/expect-feature-idea.md) ────────────
13717            | "pty_spawn" | "pty_send" | "pty_read" | "pty_expect" | "pty_expect_table"
13718            | "pty_buffer" | "pty_alive" | "pty_eof" | "pty_close" | "pty_interact"
13719            | "pty_strip_ansi" | "pty_after_eof" | "pty_pending_events"
13720            // ── Stress / telemetry extensions ─────────────────────────
13721            | "stress_fp" | "stress_int" | "stress_cache" | "stress_branch"
13722            | "stress_sort" | "stress_alloc" | "stress_mmap" | "stress_disk"
13723            | "stress_iops" | "stress_net" | "stress_http" | "stress_dns"
13724            | "stress_fork" | "stress_thread" | "stress_aes" | "stress_compress"
13725            | "stress_regex" | "stress_json" | "stress_burst" | "stress_ramp"
13726            | "stress_oscillate" | "stress_all" | "stress_temp" | "stress_thermal_zones"
13727            | "stress_freq" | "stress_throttled" | "stress_load" | "stress_meminfo"
13728            | "stress_cores" | "stress_arm_kill_switch" | "stress_killed"
13729            | "stress_disarm_kill_switch"
13730            | "stress_metrics_record" | "stress_metrics_clear" | "stress_metrics_count"
13731            | "stress_metrics_export" | "stress_metrics_prometheus"
13732            | "stress_metrics_json" | "stress_metrics_csv" | "stress_metrics_watch"
13733            // ── Compliance / secrets ─────────────────────────────────
13734            | "audit_log" | "audit_log_path"
13735            | "secrets_encrypt" | "secrets_decrypt" | "secrets_random_key" | "secrets_kdf"
13736            // ── Web framework (docs/WEB_FRAMEWORK.md) ─────────────────
13737            | "web_route" | "web_resources" | "web_root" | "web_routes_table"
13738            | "web_application_config" | "web_boot_application"
13739            | "web_render" | "web_render_partial" | "web_redirect"
13740            | "web_json" | "web_text" | "web_csv" | "web_markdown"
13741            | "web_params" | "web_request" | "web_set_header" | "web_status"
13742            | "web_before_action" | "web_after_action"
13743            | "web_session" | "web_session_set" | "web_session_get" | "web_session_clear"
13744            | "web_signed" | "web_unsigned"
13745            | "web_cookies" | "web_set_cookie"
13746            | "web_flash" | "web_flash_set" | "web_flash_get"
13747            | "web_validate" | "web_permit"
13748            | "web_password_hash" | "web_password_verify"
13749            | "web_token_for" | "web_token_consume" | "web_csrf_meta_tag"
13750            | "web_security_headers" | "web_can"
13751            | "web_h" | "web_truncate" | "web_pluralize" | "web_time_ago_in_words"
13752            | "web_image_tag" | "web_link_to" | "web_button_to"
13753            | "web_form_with" | "web_form_close"
13754            | "web_text_field" | "web_text_area" | "web_check_box"
13755            | "web_stylesheet_link_tag" | "web_javascript_link_tag"
13756            | "web_yield_content" | "web_content_for"
13757            | "web_etag" | "web_cache_get" | "web_cache_set"
13758            | "web_cache_delete" | "web_cache_clear"
13759            | "web_db_connect" | "web_db_execute" | "web_db_query"
13760            | "web_db_begin" | "web_db_commit" | "web_db_rollback"
13761            | "web_create_table" | "web_drop_table"
13762            | "web_add_column" | "web_remove_column"
13763            | "web_migrate" | "web_rollback"
13764            | "web_model_all" | "web_model_find" | "web_model_first" | "web_model_last"
13765            | "web_model_where" | "web_model_create" | "web_model_update"
13766            | "web_model_destroy" | "web_model_count" | "web_model_increment"
13767            | "web_model_paginate" | "web_model_search" | "web_model_soft_destroy"
13768            | "web_model_with"
13769            | "web_jobs_init" | "web_job_enqueue" | "web_job_dequeue"
13770            | "web_job_complete" | "web_job_fail"
13771            | "web_jobs_list" | "web_jobs_stats" | "web_job_purge"
13772            | "web_jsonapi_resource" | "web_jsonapi_collection" | "web_jsonapi_error"
13773            | "web_bearer_token" | "web_jwt_encode" | "web_jwt_decode"
13774            | "web_otp_secret" | "web_otp_generate" | "web_otp_verify"
13775            | "web_uuid" | "web_now" | "web_log" | "web_rate_limit"
13776            | "web_t" | "web_load_locale" | "web_openapi"
13777            | "web_faker_int" | "web_faker_email" | "web_faker_name"
13778            | "web_faker_sentence" | "web_faker_paragraph"
13779            => Some(name),
13780            _ => None,
13781        }
13782    }
13783
13784    /// Reserved hash names that cannot be shadowed by user declarations.
13785    /// These are stryke's reflection hashes populated from builtins metadata.
13786    fn is_reserved_hash_name(name: &str) -> bool {
13787        matches!(
13788            name,
13789            "b" | "pc"
13790                | "e"
13791                | "a"
13792                | "d"
13793                | "c"
13794                | "p"
13795                | "all"
13796                | "stryke::builtins"
13797                | "stryke::perl_compats"
13798                | "stryke::extensions"
13799                | "stryke::aliases"
13800                | "stryke::descriptions"
13801                | "stryke::categories"
13802                | "stryke::primaries"
13803                | "stryke::all"
13804        )
13805    }
13806
13807    /// Check if a UDF name shadows a stryke builtin and error if so.
13808    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
13809    /// Reserved words that cannot be used as function names because they are
13810    /// lexer-level operators or language keywords that would be mis-tokenized.
13811    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
13812        "y",
13813        "tr",
13814        "s",
13815        "m",
13816        "q",
13817        "qq",
13818        "qw",
13819        "qx",
13820        "qr",
13821        "if",
13822        "unless",
13823        "while",
13824        "until",
13825        "for",
13826        "foreach",
13827        "given",
13828        "when",
13829        "else",
13830        "elsif",
13831        "do",
13832        "eval",
13833        "return",
13834        "last",
13835        "next",
13836        "redo",
13837        "goto",
13838        "my",
13839        "our",
13840        "local",
13841        "state",
13842        "sub",
13843        "fn",
13844        "class",
13845        "struct",
13846        "enum",
13847        "trait",
13848        "use",
13849        "no",
13850        "require",
13851        "package",
13852        "BEGIN",
13853        "END",
13854        "CHECK",
13855        "INIT",
13856        "UNITCHECK",
13857        "and",
13858        "or",
13859        "not",
13860        "x",
13861        "eq",
13862        "ne",
13863        "lt",
13864        "gt",
13865        "le",
13866        "ge",
13867        "cmp",
13868    ];
13869
13870    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> PerlResult<()> {
13871        // Only check bare names, not namespaced ones (Foo::y is allowed)
13872        if !name.contains("::") {
13873            if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
13874                return Err(self.syntax_err(
13875                    format!("`{name}` is a reserved word and cannot be used as a function name"),
13876                    line,
13877                ));
13878            }
13879            if Self::is_known_bareword(name)
13880                || Self::is_try_builtin_name(name)
13881                || crate::list_builtins::is_list_builtin_name(name)
13882            {
13883                return Err(self.syntax_err(
13884                    format!(
13885"`{name}` is a stryke builtin and cannot be redefined (this is not Perl 5; use `fn` not `sub`, or pass --compat)"
13886                    ),
13887                    line,
13888                ));
13889            }
13890        }
13891        Ok(())
13892    }
13893
13894    /// Check if a hash name shadows a reserved stryke hash and error if so.
13895    /// Called only in non-compat mode.
13896    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> PerlResult<()> {
13897        if Self::is_reserved_hash_name(name) {
13898            return Err(self.syntax_err(
13899                format!(
13900"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
13901                ),
13902                line,
13903            ));
13904        }
13905        Ok(())
13906    }
13907
13908    /// Validate assignment to %hash in non-compat mode.
13909    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
13910    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
13911        match &value.kind {
13912            ExprKind::Integer(_) | ExprKind::Float(_) => {
13913                return Err(self.syntax_err(
13914                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
13915                    line,
13916                ));
13917            }
13918            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
13919                return Err(self.syntax_err(
13920                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
13921                    line,
13922                ));
13923            }
13924            ExprKind::ArrayRef(_) => {
13925                return Err(self.syntax_err(
13926                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
13927                    line,
13928                ));
13929            }
13930            ExprKind::ScalarRef(inner) => {
13931                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
13932                    return Err(self.syntax_err(
13933                        "cannot assign \\@array to hash — use %h = @array for even-length list",
13934                        line,
13935                    ));
13936                }
13937                if matches!(inner.kind, ExprKind::HashVar(_)) {
13938                    return Err(self.syntax_err(
13939                        "cannot assign \\%hash to hash — use %h = %other directly",
13940                        line,
13941                    ));
13942                }
13943            }
13944            ExprKind::HashRef(_) => {
13945                return Err(self.syntax_err(
13946                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
13947                    line,
13948                ));
13949            }
13950            ExprKind::CodeRef { .. } => {
13951                return Err(self.syntax_err("cannot assign coderef to hash", line));
13952            }
13953            ExprKind::Undef => {
13954                return Err(
13955                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
13956                );
13957            }
13958            ExprKind::List(items)
13959                if items.len() % 2 != 0
13960                    && !items.iter().any(|e| {
13961                        matches!(
13962                            e.kind,
13963                            ExprKind::ArrayVar(_)
13964                                | ExprKind::HashVar(_)
13965                                | ExprKind::FuncCall { .. }
13966                                | ExprKind::Deref { .. }
13967                                | ExprKind::ScalarVar(_)
13968                        )
13969                    }) =>
13970            {
13971                return Err(self.syntax_err(
13972                        format!(
13973                            "odd-length list ({} elements) in hash assignment — missing value for last key",
13974                            items.len()
13975                        ),
13976                        line,
13977                    ));
13978            }
13979            _ => {}
13980        }
13981        Ok(())
13982    }
13983
13984    /// Validate assignment to @array in non-compat mode.
13985    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
13986    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
13987    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
13988    fn validate_array_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
13989        if let ExprKind::Undef = &value.kind {
13990            return Err(
13991                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
13992            );
13993        }
13994        Ok(())
13995    }
13996
13997    /// Validate assignment to $scalar in non-compat mode.
13998    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
13999    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
14000        if let ExprKind::List(items) = &value.kind {
14001            if items.len() > 1 {
14002                return Err(self.syntax_err(
14003                    format!(
14004                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
14005                        items.len()
14006                    ),
14007                    line,
14008                ));
14009            }
14010        }
14011        Ok(())
14012    }
14013
14014    /// Validate an assignment based on target type (in non-compat mode only).
14015    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> PerlResult<()> {
14016        if crate::compat_mode() {
14017            return Ok(());
14018        }
14019        match &target.kind {
14020            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
14021            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
14022            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
14023            _ => Ok(()),
14024        }
14025    }
14026
14027    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
14028    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
14029    /// Also accepts a bare function name: `psort my_cmp, @list`.
14030    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
14031        if matches!(self.peek(), Token::LBrace) {
14032            return self.parse_block();
14033        }
14034        let line = self.peek_line();
14035        // Bare sub name: `psort my_cmp, @list`
14036        if let Token::Ident(ref name) = self.peek().clone() {
14037            if matches!(
14038                self.peek_at(1),
14039                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
14040            ) {
14041                let name = name.clone();
14042                self.advance();
14043                let body = Expr {
14044                    kind: ExprKind::FuncCall {
14045                        name,
14046                        args: vec![
14047                            Expr {
14048                                kind: ExprKind::ScalarVar("a".to_string()),
14049                                line,
14050                            },
14051                            Expr {
14052                                kind: ExprKind::ScalarVar("b".to_string()),
14053                                line,
14054                            },
14055                        ],
14056                    },
14057                    line,
14058                };
14059                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
14060            }
14061        }
14062        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
14063        let expr = self.parse_assign_expr_stop_at_pipe()?;
14064        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
14065    }
14066
14067    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
14068    fn parse_fan_optional_progress(
14069        &mut self,
14070        which: &'static str,
14071    ) -> PerlResult<Option<Box<Expr>>> {
14072        let line = self.peek_line();
14073        if self.eat(&Token::Comma) {
14074            match self.peek() {
14075                Token::Ident(ref kw)
14076                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
14077                {
14078                    self.advance();
14079                    self.expect(&Token::FatArrow)?;
14080                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
14081                }
14082                _ => {
14083                    return Err(self.syntax_err(
14084                        format!("{which}: expected `progress => EXPR` after comma"),
14085                        line,
14086                    ));
14087                }
14088            }
14089        }
14090        if let Token::Ident(ref kw) = self.peek().clone() {
14091            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14092                self.advance();
14093                self.expect(&Token::FatArrow)?;
14094                return Ok(Some(Box::new(self.parse_assign_expr()?)));
14095            }
14096        }
14097        Ok(None)
14098    }
14099
14100    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
14101    /// (for `pmap_chunked`, `psort`, etc.).
14102    ///
14103    /// Paren-less — individual parts parse through
14104    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
14105    /// the enclosing pipe-forward loop (left-associative chaining).
14106    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
14107        // On the RHS of `|>`, list-taking builtins may be written bare with no
14108        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
14109        // When the next token is a list-terminator, yield an empty placeholder
14110        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
14111        // desugar time, so the placeholder is never evaluated.
14112        if self.in_pipe_rhs()
14113            && matches!(
14114                self.peek(),
14115                Token::Semicolon
14116                    | Token::RBrace
14117                    | Token::RParen
14118                    | Token::Eof
14119                    | Token::PipeForward
14120                    | Token::Comma
14121            )
14122        {
14123            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
14124        }
14125        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
14126        loop {
14127            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
14128                break;
14129            }
14130            if matches!(
14131                self.peek(),
14132                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14133            ) {
14134                break;
14135            }
14136            if self.peek_is_postfix_stmt_modifier_keyword() {
14137                break;
14138            }
14139            if let Token::Ident(ref kw) = self.peek().clone() {
14140                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14141                    self.advance();
14142                    self.expect(&Token::FatArrow)?;
14143                    let prog = self.parse_assign_expr_stop_at_pipe()?;
14144                    return Ok((merge_expr_list(parts), Some(prog)));
14145                }
14146            }
14147            parts.push(self.parse_assign_expr_stop_at_pipe()?);
14148        }
14149        Ok((merge_expr_list(parts), None))
14150    }
14151
14152    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
14153        if matches!(self.peek(), Token::LParen) {
14154            self.advance();
14155            let expr = self.parse_expression()?;
14156            self.expect(&Token::RParen)?;
14157            Ok(expr)
14158        } else {
14159            self.parse_assign_expr_stop_at_pipe()
14160        }
14161    }
14162
14163    /// Bare argument for a Perl-5 named unary operator (`defined`, `length`,
14164    /// `abs`, `scalar`, `ref`, `keys`, `values`, etc.). Named unary precedence
14165    /// sits between shift (`<<`/`>>`) and comparison (`<`/`>`), so we parse
14166    /// only down to shift level. The surrounding `&&` / `||` / `==` / `<` /
14167    /// equality / logical / ternary stay outside the unary's argument.
14168    /// Without this `defined $x && Y` mis-parsed as `defined($x && Y)` and
14169    /// silently returned true whenever `$x` was defined — see the skip-list
14170    /// debugging write-up. Same scope rule for `length` etc.
14171    fn parse_named_unary_arg(&mut self) -> PerlResult<Expr> {
14172        if matches!(self.peek(), Token::LParen) {
14173            self.advance();
14174            let expr = self.parse_expression()?;
14175            self.expect(&Token::RParen)?;
14176            Ok(expr)
14177        } else {
14178            self.parse_shift()
14179        }
14180    }
14181
14182    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
14183        // Default to `$_` when the next token cannot start an argument expression
14184        // because it has lower precedence than a named unary operator. Perl 5
14185        // named unary precedence sits above ternary / comparison / logical / bitwise
14186        // / assignment / list ops; everything below should terminate the implicit
14187        // argument and let the surrounding expression continue.
14188        // See `perldoc perlop` ("Named Unary Operators").
14189        if matches!(
14190            self.peek(),
14191            // Statement / list / call boundaries
14192            Token::Semicolon
14193                | Token::RBrace
14194                | Token::RParen
14195                | Token::RBracket
14196                | Token::Eof
14197                | Token::Comma
14198                | Token::FatArrow
14199                | Token::PipeForward
14200            // Ternary `? :`
14201                | Token::Question
14202                | Token::Colon
14203            // Comparison / equality (numeric + string)
14204                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
14205                | Token::NumLe | Token::NumGe | Token::Spaceship
14206                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
14207                | Token::StrLe | Token::StrGe | Token::StrCmp
14208            // Logical (symbolic and word forms) + defined-or
14209                | Token::LogAnd | Token::LogOr | Token::LogNot
14210                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
14211                | Token::DefinedOr
14212            // Range (lower precedence than named unary)
14213                | Token::Range | Token::RangeExclusive
14214            // Assignment (any compound form)
14215                | Token::Assign | Token::PlusAssign | Token::MinusAssign
14216                | Token::MulAssign | Token::DivAssign | Token::ModAssign
14217                | Token::PowAssign | Token::DotAssign | Token::AndAssign
14218                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
14219                | Token::ShiftLeftAssign | Token::ShiftRightAssign
14220                | Token::BitAndAssign | Token::BitOrAssign
14221        ) {
14222            return Ok(Expr {
14223                kind: ExprKind::ScalarVar("_".into()),
14224                line: self.peek_line(),
14225            });
14226        }
14227        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
14228        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
14229        // Perl accepts both `length` and `length()` as `length($_)`.
14230        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
14231            let line = self.peek_line();
14232            self.advance(); // (
14233            self.advance(); // )
14234            return Ok(Expr {
14235                kind: ExprKind::ScalarVar("_".into()),
14236                line,
14237            });
14238        }
14239        // Named-unary precedence: parenless arg only goes down to shift level,
14240        // so surrounding `eq` / `==` / `?:` / `&&` / `||` stay outside. Without
14241        // this, `ref $x eq "FOO"` mis-parses as `ref ($x eq "FOO")`.
14242        // (PARITY-016 — also fixes `length $s == 3 ? "Y" : "N"` etc.)
14243        self.parse_named_unary_arg()
14244    }
14245
14246    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
14247    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
14248        let line = self.prev_line(); // line where shift/pop keyword was
14249        if matches!(self.peek(), Token::LParen) {
14250            self.advance();
14251            if matches!(self.peek(), Token::RParen) {
14252                self.advance();
14253                return Ok(Expr {
14254                    kind: ExprKind::ArrayVar("_".into()),
14255                    line: self.peek_line(),
14256                });
14257            }
14258            let expr = self.parse_expression()?;
14259            self.expect(&Token::RParen)?;
14260            return Ok(expr);
14261        }
14262        // Implicit semicolon: if next token is on a different line, don't consume it
14263        if matches!(
14264            self.peek(),
14265            Token::Semicolon
14266                | Token::RBrace
14267                | Token::RParen
14268                | Token::Eof
14269                | Token::Comma
14270                | Token::PipeForward
14271        ) || self.peek_line() > line
14272        {
14273            Ok(Expr {
14274                kind: ExprKind::ArrayVar("_".into()),
14275                line,
14276            })
14277        } else {
14278            self.parse_assign_expr()
14279        }
14280    }
14281
14282    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
14283        if matches!(self.peek(), Token::LParen) {
14284            self.advance();
14285            let args = self.parse_arg_list()?;
14286            self.expect(&Token::RParen)?;
14287            Ok(args)
14288        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
14289            // In thread context, don't consume barewords as arguments
14290            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
14291            Ok(vec![])
14292        } else {
14293            self.parse_list_until_terminator()
14294        }
14295    }
14296
14297    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
14298    /// should be treated as an auto-quoted string (hash key), not a function call.
14299    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
14300    #[inline]
14301    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
14302        if matches!(self.peek(), Token::FatArrow) {
14303            Some(Expr {
14304                kind: ExprKind::String(name.to_string()),
14305                line,
14306            })
14307        } else {
14308            None
14309        }
14310    }
14311
14312    /// Parse a hash subscript key inside `{…}`.
14313    ///
14314    /// Perl auto-quotes a single bareword before `}`, even for keywords:
14315    /// `$h{print}`, `$r->{f}` etc. all yield the string key. Stryke also
14316    /// auto-quotes the string-comparison and word-logical operator tokens
14317    /// (`eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`, `and`, `or`, `not`, `x`)
14318    /// here — the lexer eagerly converts those identifiers to operator tokens,
14319    /// but inside `{…}` followed by `}` they're plainly hash keys.
14320    /// Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …)
14321    /// resolve to the topic value, not the literal name — `$h{_<}` ≡ `$h{$_<}`.
14322    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
14323        let line = self.peek_line();
14324        if let Token::Ident(ref k) = self.peek().clone() {
14325            if matches!(self.peek_at(1), Token::RBrace) && !Self::is_underscore_topic_slot(k) {
14326                let s = k.clone();
14327                self.advance();
14328                return Ok(Expr {
14329                    kind: ExprKind::String(s),
14330                    line,
14331                });
14332            }
14333        }
14334        if matches!(self.peek_at(1), Token::RBrace) {
14335            if let Some(s) = Self::operator_keyword_to_ident_str(self.peek()) {
14336                self.advance();
14337                return Ok(Expr {
14338                    kind: ExprKind::String(s.to_string()),
14339                    line,
14340                });
14341            }
14342        }
14343        self.parse_expression()
14344    }
14345
14346    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
14347    #[inline]
14348    fn peek_is_glob_par_progress_kw(&self) -> bool {
14349        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
14350            && matches!(self.peek_at(1), Token::FatArrow)
14351    }
14352
14353    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
14354    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
14355        let mut args = Vec::new();
14356        loop {
14357            if matches!(self.peek(), Token::RParen | Token::Eof) {
14358                break;
14359            }
14360            if self.peek_is_glob_par_progress_kw() {
14361                break;
14362            }
14363            args.push(self.parse_assign_expr()?);
14364            match self.peek() {
14365                Token::RParen => break,
14366                Token::Comma => {
14367                    self.advance();
14368                    if matches!(self.peek(), Token::RParen) {
14369                        break;
14370                    }
14371                    if self.peek_is_glob_par_progress_kw() {
14372                        break;
14373                    }
14374                }
14375                _ => {
14376                    return Err(self.syntax_err(
14377                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
14378                        self.peek_line(),
14379                    ));
14380                }
14381            }
14382        }
14383        Ok(args)
14384    }
14385
14386    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
14387    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
14388        let mut args = Vec::new();
14389        loop {
14390            if matches!(
14391                self.peek(),
14392                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
14393            ) {
14394                break;
14395            }
14396            if self.peek_is_postfix_stmt_modifier_keyword() {
14397                break;
14398            }
14399            if self.peek_is_glob_par_progress_kw() {
14400                break;
14401            }
14402            args.push(self.parse_assign_expr()?);
14403            if !self.eat(&Token::Comma) {
14404                break;
14405            }
14406            if self.peek_is_glob_par_progress_kw() {
14407                break;
14408            }
14409        }
14410        Ok(args)
14411    }
14412
14413    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
14414    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
14415        if matches!(self.peek(), Token::LParen) {
14416            self.advance();
14417            let args = self.parse_pattern_list_until_rparen_or_progress()?;
14418            let progress = if self.peek_is_glob_par_progress_kw() {
14419                self.advance();
14420                self.expect(&Token::FatArrow)?;
14421                Some(Box::new(self.parse_assign_expr()?))
14422            } else {
14423                None
14424            };
14425            self.expect(&Token::RParen)?;
14426            Ok((args, progress))
14427        } else {
14428            let args = self.parse_pattern_list_glob_par_bare()?;
14429            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
14430            let progress = if self.peek_is_glob_par_progress_kw() {
14431                self.advance();
14432                self.expect(&Token::FatArrow)?;
14433                Some(Box::new(self.parse_assign_expr()?))
14434            } else {
14435                None
14436            };
14437            Ok((args, progress))
14438        }
14439    }
14440
14441    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
14442        let mut args = Vec::new();
14443        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
14444        // so shadow any outer paren-less-arg suppression from
14445        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
14446        let saved_no_pf = self.no_pipe_forward_depth;
14447        self.no_pipe_forward_depth = 0;
14448        while !matches!(
14449            self.peek(),
14450            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
14451        ) {
14452            let arg = match self.parse_assign_expr() {
14453                Ok(e) => e,
14454                Err(err) => {
14455                    self.no_pipe_forward_depth = saved_no_pf;
14456                    return Err(err);
14457                }
14458            };
14459            args.push(arg);
14460            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
14461                break;
14462            }
14463        }
14464        self.no_pipe_forward_depth = saved_no_pf;
14465        Ok(args)
14466    }
14467
14468    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
14469    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
14470    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
14471    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
14472    ///
14473    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
14474    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
14475    /// use `func():other` if you actually want to invoke).
14476    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> PerlResult<Vec<Expr>> {
14477        let mut args = Vec::new();
14478        let saved_no_pf = self.no_pipe_forward_depth;
14479        self.no_pipe_forward_depth = 0;
14480        while !matches!(
14481            self.peek(),
14482            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
14483        ) {
14484            let arg = match self.parse_slice_arg(is_hash) {
14485                Ok(e) => e,
14486                Err(err) => {
14487                    self.no_pipe_forward_depth = saved_no_pf;
14488                    return Err(err);
14489                }
14490            };
14491            args.push(arg);
14492            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
14493                break;
14494            }
14495        }
14496        self.no_pipe_forward_depth = saved_no_pf;
14497        Ok(args)
14498    }
14499
14500    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
14501    fn parse_slice_arg(&mut self, is_hash: bool) -> PerlResult<Expr> {
14502        let line = self.peek_line();
14503
14504        // Open-start: `:` or `::` immediately
14505        if matches!(self.peek(), Token::Colon) {
14506            self.advance();
14507            return self.finish_slice_range(None, false, is_hash, line);
14508        }
14509        if matches!(self.peek(), Token::PackageSep) {
14510            self.advance();
14511            return self.finish_slice_range(None, true, is_hash, line);
14512        }
14513
14514        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
14515        // consumed as a colon-range there — we want to handle the colon ourselves.
14516        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
14517        let result = self.parse_slice_endpoint(is_hash);
14518        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
14519        let from_expr = result?;
14520
14521        // Trailing `:` or `::` after the FROM endpoint?
14522        if matches!(self.peek(), Token::Colon) {
14523            self.advance();
14524            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
14525        }
14526        if matches!(self.peek(), Token::PackageSep) {
14527            self.advance();
14528            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
14529        }
14530
14531        Ok(from_expr)
14532    }
14533
14534    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
14535    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
14536    /// expression (if any) is STEP.
14537    ///
14538    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
14539    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
14540    fn finish_slice_range(
14541        &mut self,
14542        from: Option<Box<Expr>>,
14543        double: bool,
14544        is_hash: bool,
14545        line: usize,
14546    ) -> PerlResult<Expr> {
14547        let (to, step) = if double {
14548            // `::` so TO is implicit; STEP is whatever (if anything) follows.
14549            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
14550            (None, step_v)
14551        } else {
14552            // single `:` — parse TO, then optional `:STEP`.
14553            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
14554            let step_v = if matches!(self.peek(), Token::Colon) {
14555                self.advance();
14556                self.parse_slice_optional_endpoint(is_hash)?
14557            } else if matches!(self.peek(), Token::PackageSep) {
14558                return Err(
14559                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
14560                );
14561            } else {
14562                None
14563            };
14564            (to_v, step_v)
14565        };
14566
14567        // Closed form (both endpoints present) — produce a regular `Range` so the
14568        // rest of the compiler/VM keeps reusing existing range-expansion paths.
14569        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
14570            return Ok(Expr {
14571                kind: ExprKind::Range {
14572                    from: f.clone(),
14573                    to: t.clone(),
14574                    exclusive: false,
14575                    step,
14576                },
14577                line,
14578            });
14579        }
14580
14581        Ok(Expr {
14582            kind: ExprKind::SliceRange { from, to, step },
14583            line,
14584        })
14585    }
14586
14587    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
14588    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
14589    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> PerlResult<Option<Box<Expr>>> {
14590        if matches!(
14591            self.peek(),
14592            Token::Colon
14593                | Token::PackageSep
14594                | Token::Comma
14595                | Token::RBracket
14596                | Token::RBrace
14597                | Token::Eof
14598        ) {
14599            return Ok(None);
14600        }
14601        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
14602        let r = self.parse_slice_endpoint(is_hash);
14603        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
14604        Ok(Some(Box::new(r?)))
14605    }
14606
14607    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
14608    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
14609    /// fall through to standard expression parsing. For array slices, no auto-quote.
14610    fn parse_slice_endpoint(&mut self, is_hash: bool) -> PerlResult<Expr> {
14611        if is_hash {
14612            if let Token::Ident(name) = self.peek().clone() {
14613                if matches!(
14614                    self.peek_at(1),
14615                    Token::Colon
14616                        | Token::PackageSep
14617                        | Token::Comma
14618                        | Token::RBracket
14619                        | Token::RBrace
14620                ) {
14621                    let line = self.peek_line();
14622                    self.advance();
14623                    return Ok(Expr {
14624                        kind: ExprKind::String(name),
14625                        line,
14626                    });
14627                }
14628            }
14629        }
14630        self.parse_assign_expr()
14631    }
14632
14633    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
14634    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
14635    /// no-arg method call; we must not consume that `+` as the start of a first argument.
14636    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
14637        let mut args = Vec::new();
14638        let call_line = self.prev_line();
14639        loop {
14640            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
14641            // hash argument to `next` (paren-less method call has no args here).
14642            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
14643                break;
14644            }
14645            if matches!(
14646                self.peek(),
14647                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14648            ) {
14649                break;
14650            }
14651            if let Token::Ident(ref kw) = self.peek().clone() {
14652                if matches!(
14653                    kw.as_str(),
14654                    "if" | "unless" | "while" | "until" | "for" | "foreach"
14655                ) {
14656                    break;
14657                }
14658            }
14659            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
14660            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
14661            if args.is_empty()
14662                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
14663            {
14664                break;
14665            }
14666            // Implicit semicolon: if no args collected yet and next token is on a different
14667            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
14668            if args.is_empty() && self.peek_line() > call_line {
14669                break;
14670            }
14671            args.push(self.parse_assign_expr()?);
14672            if !self.eat(&Token::Comma) {
14673                break;
14674            }
14675        }
14676        Ok(args)
14677    }
14678
14679    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
14680    /// the whole `->meth` expression).
14681    fn peek_method_arg_infix_terminator(&self) -> bool {
14682        matches!(
14683            self.peek(),
14684            Token::Plus
14685                | Token::Minus
14686                | Token::Star
14687                | Token::Slash
14688                | Token::Percent
14689                | Token::Power
14690                | Token::Dot
14691                | Token::X
14692                | Token::NumEq
14693                | Token::NumNe
14694                | Token::NumLt
14695                | Token::NumGt
14696                | Token::NumLe
14697                | Token::NumGe
14698                | Token::Spaceship
14699                | Token::StrEq
14700                | Token::StrNe
14701                | Token::StrLt
14702                | Token::StrGt
14703                | Token::StrLe
14704                | Token::StrGe
14705                | Token::StrCmp
14706                | Token::LogAnd
14707                | Token::LogOr
14708                | Token::LogAndWord
14709                | Token::LogOrWord
14710                | Token::DefinedOr
14711                | Token::BitAnd
14712                | Token::BitOr
14713                | Token::BitXor
14714                | Token::ShiftLeft
14715                | Token::ShiftRight
14716                | Token::Range
14717                | Token::RangeExclusive
14718                | Token::BindMatch
14719                | Token::BindNotMatch
14720                | Token::Arrow
14721                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
14722                | Token::Question
14723                | Token::Colon
14724                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
14725                | Token::Assign
14726                | Token::PlusAssign
14727                | Token::MinusAssign
14728                | Token::MulAssign
14729                | Token::DivAssign
14730                | Token::ModAssign
14731                | Token::PowAssign
14732                | Token::DotAssign
14733                | Token::AndAssign
14734                | Token::OrAssign
14735                | Token::XorAssign
14736                | Token::DefinedOrAssign
14737                | Token::ShiftLeftAssign
14738                | Token::ShiftRightAssign
14739                | Token::BitAndAssign
14740                | Token::BitOrAssign
14741        )
14742    }
14743
14744    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
14745        let mut args = Vec::new();
14746        // Line of the last consumed token (the keyword / function name that
14747        // triggered this arg parse).  Used for implicit-semicolon: if no args
14748        // have been parsed yet and the next token is on a *different* line,
14749        // treat the newline as a statement boundary and stop.
14750        let call_line = self.prev_line();
14751        loop {
14752            if matches!(
14753                self.peek(),
14754                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14755            ) {
14756                break;
14757            }
14758            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
14759            if let Token::Ident(ref kw) = self.peek().clone() {
14760                if matches!(
14761                    kw.as_str(),
14762                    "if" | "unless" | "while" | "until" | "for" | "foreach"
14763                ) {
14764                    break;
14765                }
14766            }
14767            // Implicit semicolons: if no args have been collected yet and the
14768            // next token is on a different line from the call keyword, treat
14769            // the newline as a statement boundary.  This prevents paren-less
14770            // calls (`say`, `print`, user subs) from greedily swallowing the
14771            // *next* statement when the author omitted a semicolon.
14772            // After a comma continuation, multi-line arg lists still work.
14773            if args.is_empty() && self.peek_line() > call_line {
14774                break;
14775            }
14776            // Paren-less builtin args: `|>` terminates the whole call list, so
14777            // individual args must not absorb a following `|>`.
14778            args.push(self.parse_assign_expr_stop_at_pipe()?);
14779            if !self.eat(&Token::Comma) {
14780                break;
14781            }
14782        }
14783        Ok(args)
14784    }
14785
14786    /// Body of `+{ ... }` — Perl's force-hashref idiom. The opening `+` and `{`
14787    /// have already been consumed. Tries the normal `KEY => VAL, …` shape first
14788    /// (so `+{ a => 1, b => 2 }` is identical to `{ a => 1, b => 2 }`); on
14789    /// failure falls back to "single list-yielding expression treated as a
14790    /// flat key/value spread" so `+{ map { (k, v) } LIST }` works without
14791    /// the user needing a temp `my %h = ...; \%h` shuffle.
14792    fn parse_forced_hashref_body(&mut self, line: usize) -> PerlResult<Expr> {
14793        let saved = self.pos;
14794        if let Ok(pairs) = self.try_parse_hash_ref() {
14795            return Ok(Expr {
14796                kind: ExprKind::HashRef(pairs),
14797                line,
14798            });
14799        }
14800        // Empty `+{}` is the empty hashref.
14801        self.pos = saved;
14802        if matches!(self.peek(), Token::RBrace) {
14803            self.advance();
14804            return Ok(Expr {
14805                kind: ExprKind::HashRef(vec![]),
14806                line,
14807            });
14808        }
14809        // Single expression — eval as list, flatten into key/value pairs via the
14810        // existing __HASH_SPREAD__ sentinel that `ExprKind::HashRef` already
14811        // handles in [`Interpreter::eval_expr`].
14812        let inner = self.parse_expression()?;
14813        self.expect(&Token::RBrace)?;
14814        let sentinel_key = Expr {
14815            kind: ExprKind::String("__HASH_SPREAD__".into()),
14816            line,
14817        };
14818        Ok(Expr {
14819            kind: ExprKind::HashRef(vec![(sentinel_key, inner)]),
14820            line,
14821        })
14822    }
14823
14824    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
14825        let mut pairs = Vec::new();
14826        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
14827            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
14828            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
14829            // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, `_!N!`, …)
14830            // resolve to the topic value, not the literal name — `{ _ => 1 }` ≡ `{ $_ => 1 }`.
14831            let line = self.peek_line();
14832            let key = if let Token::Ident(ref name) = self.peek().clone() {
14833                if matches!(self.peek_at(1), Token::FatArrow)
14834                    && !Self::is_underscore_topic_slot(name)
14835                {
14836                    self.advance();
14837                    Expr {
14838                        kind: ExprKind::String(name.clone()),
14839                        line,
14840                    }
14841                } else {
14842                    self.parse_assign_expr()?
14843                }
14844            } else {
14845                self.parse_assign_expr()?
14846            };
14847            // If the key expression is a hash/array variable and is followed by `}` or `,`
14848            // with no `=>`, treat the whole thing as a hash-from-expression construction.
14849            // This handles `{ %a }`, `{ %a, key => val }`, etc.
14850            if matches!(self.peek(), Token::RBrace | Token::Comma)
14851                && matches!(
14852                    key.kind,
14853                    ExprKind::HashVar(_)
14854                        | ExprKind::Deref {
14855                            kind: Sigil::Hash,
14856                            ..
14857                        }
14858                )
14859            {
14860                // Synthesize a pair whose key/value is spread from the hash expression.
14861                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
14862                // The evaluator will flatten this.
14863                let sentinel_key = Expr {
14864                    kind: ExprKind::String("__HASH_SPREAD__".into()),
14865                    line,
14866                };
14867                pairs.push((sentinel_key, key));
14868                self.eat(&Token::Comma);
14869                continue;
14870            }
14871            // Expect => or , after key
14872            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
14873                let val = self.parse_assign_expr()?;
14874                pairs.push((key, val));
14875                self.eat(&Token::Comma);
14876            } else {
14877                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
14878            }
14879        }
14880        self.expect(&Token::RBrace)?;
14881        Ok(pairs)
14882    }
14883
14884    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
14885    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
14886    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
14887    /// navigates. Caller expects and consumes `term` itself.
14888    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
14889        let mut pairs = Vec::new();
14890        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
14891            && !matches!(self.peek(), Token::Eof)
14892        {
14893            let line = self.peek_line();
14894            let key = if let Token::Ident(ref name) = self.peek().clone() {
14895                if matches!(self.peek_at(1), Token::FatArrow)
14896                    && !Self::is_underscore_topic_slot(name)
14897                {
14898                    self.advance();
14899                    Expr {
14900                        kind: ExprKind::String(name.clone()),
14901                        line,
14902                    }
14903                } else {
14904                    self.parse_assign_expr()?
14905                }
14906            } else {
14907                self.parse_assign_expr()?
14908            };
14909            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
14910                let val = self.parse_assign_expr()?;
14911                pairs.push((key, val));
14912                self.eat(&Token::Comma);
14913            } else {
14914                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
14915            }
14916        }
14917        Ok(pairs)
14918    }
14919
14920    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
14921    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
14922    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
14923    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
14924    /// Each step wraps the current expression in an `ArrowDeref`.
14925    fn interp_chain_subscripts(
14926        &self,
14927        chars: &[char],
14928        i: &mut usize,
14929        mut base: Expr,
14930        line: usize,
14931    ) -> Expr {
14932        loop {
14933            // Optional `->` connector
14934            let (after, requires_subscript) =
14935                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
14936                    (*i + 2, true)
14937                } else {
14938                    (*i, false)
14939                };
14940            if after >= chars.len() {
14941                break;
14942            }
14943            match chars[after] {
14944                '[' => {
14945                    *i = after + 1;
14946                    let mut idx_str = String::new();
14947                    while *i < chars.len() && chars[*i] != ']' {
14948                        idx_str.push(chars[*i]);
14949                        *i += 1;
14950                    }
14951                    if *i < chars.len() {
14952                        *i += 1;
14953                    }
14954                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
14955                        Expr {
14956                            kind: ExprKind::ScalarVar(rest.to_string()),
14957                            line,
14958                        }
14959                    } else if let Ok(n) = idx_str.parse::<i64>() {
14960                        Expr {
14961                            kind: ExprKind::Integer(n),
14962                            line,
14963                        }
14964                    } else {
14965                        Expr {
14966                            kind: ExprKind::String(idx_str),
14967                            line,
14968                        }
14969                    };
14970                    base = Expr {
14971                        kind: ExprKind::ArrowDeref {
14972                            expr: Box::new(base),
14973                            index: Box::new(idx_expr),
14974                            kind: DerefKind::Array,
14975                        },
14976                        line,
14977                    };
14978                }
14979                '{' => {
14980                    *i = after + 1;
14981                    let mut key = String::new();
14982                    let mut depth = 1usize;
14983                    while *i < chars.len() && depth > 0 {
14984                        if chars[*i] == '{' {
14985                            depth += 1;
14986                        } else if chars[*i] == '}' {
14987                            depth -= 1;
14988                            if depth == 0 {
14989                                break;
14990                            }
14991                        }
14992                        key.push(chars[*i]);
14993                        *i += 1;
14994                    }
14995                    if *i < chars.len() {
14996                        *i += 1;
14997                    }
14998                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
14999                        Expr {
15000                            kind: ExprKind::ScalarVar(rest.to_string()),
15001                            line,
15002                        }
15003                    } else {
15004                        Expr {
15005                            kind: ExprKind::String(key),
15006                            line,
15007                        }
15008                    };
15009                    base = Expr {
15010                        kind: ExprKind::ArrowDeref {
15011                            expr: Box::new(base),
15012                            index: Box::new(key_expr),
15013                            kind: DerefKind::Hash,
15014                        },
15015                        line,
15016                    };
15017                }
15018                _ => {
15019                    if requires_subscript {
15020                        // `->method()` etc — not interpolated, leave for literal output.
15021                    }
15022                    break;
15023                }
15024            }
15025        }
15026        base
15027    }
15028
15029    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
15030    /// outside double-quoted strings; this catches the in-string interpolation
15031    /// path which has its own parser bypassing `Token::ScalarVar`).
15032    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> PerlResult<()> {
15033        if crate::no_interop_mode() && (name == "a" || name == "b") {
15034            return Err(self.syntax_err(
15035                format!(
15036                    "stryke uses `$_0` / `$_1` instead of `${}` (--no-interop is active)",
15037                    name
15038                ),
15039                line,
15040            ));
15041        }
15042        Ok(())
15043    }
15044
15045    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
15046        // Parse $var and @var inside double-quoted strings
15047        let mut parts = Vec::new();
15048        let mut literal = String::new();
15049        let chars: Vec<char> = s.chars().collect();
15050        let mut i = 0;
15051
15052        'istr: while i < chars.len() {
15053            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
15054                literal.push('$');
15055                i += 1;
15056                continue;
15057            }
15058            if chars[i] == LITERAL_AT_IN_DQUOTE {
15059                literal.push('@');
15060                i += 1;
15061                continue;
15062            }
15063            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
15064            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
15065                literal.push('\\');
15066                i += 1;
15067                // i now points at '$' — fall through to $ handling below
15068            }
15069            if chars[i] == '$' && i + 1 < chars.len() {
15070                if !literal.is_empty() {
15071                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
15072                }
15073                i += 1; // past `$`
15074                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
15075                while i < chars.len() && chars[i].is_whitespace() {
15076                    i += 1;
15077                }
15078                if i >= chars.len() {
15079                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
15080                }
15081                // `$#name` — last index of `@name` (Perl `$#array`).
15082                if chars[i] == '#' {
15083                    i += 1;
15084                    let mut sname = String::from("#");
15085                    while i < chars.len()
15086                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
15087                    {
15088                        sname.push(chars[i]);
15089                        i += 1;
15090                    }
15091                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
15092                        sname.push_str("::");
15093                        i += 2;
15094                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
15095                            sname.push(chars[i]);
15096                            i += 1;
15097                        }
15098                    }
15099                    self.no_interop_check_scalar_var_name(&sname, line)?;
15100                    parts.push(StringPart::ScalarVar(sname));
15101                    continue;
15102                }
15103                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
15104                // between) and the second `$` is not followed by a word character or digit (`$$x`
15105                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
15106                if chars[i] == '$' {
15107                    let next_c = chars.get(i + 1).copied();
15108                    let is_pid = match next_c {
15109                        None => true,
15110                        Some(c)
15111                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
15112                        {
15113                            true
15114                        }
15115                        _ => false,
15116                    };
15117                    if is_pid {
15118                        parts.push(StringPart::ScalarVar("$$".to_string()));
15119                        i += 1; // consume second `$`
15120                        continue;
15121                    }
15122                    i += 1; // skip second `$` — same as a single `$` before the identifier
15123                }
15124                if chars[i] == '{' {
15125                    // `${…}` — braced variable OR expression interpolation.
15126                    //   `${name}`              → ScalarVar(name)        (Perl standard)
15127                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
15128                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
15129                    // stryke's prior `#{expr}` form remains supported elsewhere.
15130                    i += 1;
15131                    let mut inner = String::new();
15132                    let mut depth = 1usize;
15133                    while i < chars.len() && depth > 0 {
15134                        match chars[i] {
15135                            '{' => depth += 1,
15136                            '}' => {
15137                                depth -= 1;
15138                                if depth == 0 {
15139                                    break;
15140                                }
15141                            }
15142                            _ => {}
15143                        }
15144                        inner.push(chars[i]);
15145                        i += 1;
15146                    }
15147                    if i < chars.len() {
15148                        i += 1; // skip closing }
15149                    }
15150
15151                    // Distinguish "name" from "expression". If trimmed inner starts with
15152                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
15153                    // expression and emit a scalar deref. Otherwise, plain variable name.
15154                    let trimmed = inner.trim();
15155                    let is_expr = trimmed.starts_with('$')
15156                        || trimmed.starts_with('\\')
15157                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
15158                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
15159                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
15160                    let mut base: Expr = if is_expr {
15161                        // Re-parse the inner content as a Perl expression. Wrap in
15162                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
15163                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
15164                        match parse_expression_from_str(trimmed, "<interp>") {
15165                            Ok(e) => Expr {
15166                                kind: ExprKind::Deref {
15167                                    expr: Box::new(e),
15168                                    kind: Sigil::Scalar,
15169                                },
15170                                line,
15171                            },
15172                            Err(_) => Expr {
15173                                kind: ExprKind::ScalarVar(inner.clone()),
15174                                line,
15175                            },
15176                        }
15177                    } else {
15178                        // Treat as a plain (possibly qualified) variable name.
15179                        self.no_interop_check_scalar_var_name(&inner, line)?;
15180                        Expr {
15181                            kind: ExprKind::ScalarVar(inner),
15182                            line,
15183                        }
15184                    };
15185
15186                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
15187                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
15188                    // chains thereafter.
15189                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
15190                    parts.push(StringPart::Expr(base));
15191                } else if chars[i] == '^' {
15192                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
15193                    let mut name = String::from("^");
15194                    i += 1;
15195                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
15196                        name.push(chars[i]);
15197                        i += 1;
15198                    }
15199                    if i < chars.len() && chars[i] == '{' {
15200                        i += 1; // skip {
15201                        let mut key = String::new();
15202                        let mut depth = 1;
15203                        while i < chars.len() && depth > 0 {
15204                            if chars[i] == '{' {
15205                                depth += 1;
15206                            } else if chars[i] == '}' {
15207                                depth -= 1;
15208                                if depth == 0 {
15209                                    break;
15210                                }
15211                            }
15212                            key.push(chars[i]);
15213                            i += 1;
15214                        }
15215                        if i < chars.len() {
15216                            i += 1;
15217                        }
15218                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
15219                            Expr {
15220                                kind: ExprKind::ScalarVar(rest.to_string()),
15221                                line,
15222                            }
15223                        } else {
15224                            Expr {
15225                                kind: ExprKind::String(key),
15226                                line,
15227                            }
15228                        };
15229                        parts.push(StringPart::Expr(Expr {
15230                            kind: ExprKind::HashElement {
15231                                hash: name,
15232                                key: Box::new(key_expr),
15233                            },
15234                            line,
15235                        }));
15236                    } else if i < chars.len() && chars[i] == '[' {
15237                        i += 1;
15238                        let mut idx_str = String::new();
15239                        while i < chars.len() && chars[i] != ']' {
15240                            idx_str.push(chars[i]);
15241                            i += 1;
15242                        }
15243                        if i < chars.len() {
15244                            i += 1;
15245                        }
15246                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
15247                            Expr {
15248                                kind: ExprKind::ScalarVar(rest.to_string()),
15249                                line,
15250                            }
15251                        } else if let Ok(n) = idx_str.parse::<i64>() {
15252                            Expr {
15253                                kind: ExprKind::Integer(n),
15254                                line,
15255                            }
15256                        } else {
15257                            Expr {
15258                                kind: ExprKind::String(idx_str),
15259                                line,
15260                            }
15261                        };
15262                        parts.push(StringPart::Expr(Expr {
15263                            kind: ExprKind::ArrayElement {
15264                                array: name,
15265                                index: Box::new(idx_expr),
15266                            },
15267                            line,
15268                        }));
15269                    } else {
15270                        self.no_interop_check_scalar_var_name(&name, line)?;
15271                        parts.push(StringPart::ScalarVar(name));
15272                    }
15273                } else if chars[i].is_alphabetic() || chars[i] == '_' {
15274                    let mut name = String::new();
15275                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
15276                        name.push(chars[i]);
15277                        i += 1;
15278                    }
15279                    // `$_<`, `$_<<`, … — outer topic (stryke extension); only for bare `_`.
15280                    if name == "_" {
15281                        while i < chars.len() && chars[i] == '<' {
15282                            name.push('<');
15283                            i += 1;
15284                        }
15285                    }
15286                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
15287                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
15288                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
15289                    // `name` to build the expression.
15290                    self.no_interop_check_scalar_var_name(&name, line)?;
15291                    // Build the base expression, then thread arrow-deref chains
15292                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
15293                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
15294                    // correctly inside double-quoted strings (Perl convention).
15295                    let mut base = if i < chars.len() && chars[i] == '{' {
15296                        // $hash{key}
15297                        i += 1; // skip {
15298                        let mut key = String::new();
15299                        let mut depth = 1;
15300                        while i < chars.len() && depth > 0 {
15301                            if chars[i] == '{' {
15302                                depth += 1;
15303                            } else if chars[i] == '}' {
15304                                depth -= 1;
15305                                if depth == 0 {
15306                                    break;
15307                                }
15308                            }
15309                            key.push(chars[i]);
15310                            i += 1;
15311                        }
15312                        if i < chars.len() {
15313                            i += 1;
15314                        } // skip }
15315                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
15316                            Expr {
15317                                kind: ExprKind::ScalarVar(rest.to_string()),
15318                                line,
15319                            }
15320                        } else {
15321                            Expr {
15322                                kind: ExprKind::String(key),
15323                                line,
15324                            }
15325                        };
15326                        Expr {
15327                            kind: ExprKind::HashElement {
15328                                hash: name,
15329                                key: Box::new(key_expr),
15330                            },
15331                            line,
15332                        }
15333                    } else if i < chars.len() && chars[i] == '[' {
15334                        // $array[idx]
15335                        i += 1;
15336                        let mut idx_str = String::new();
15337                        while i < chars.len() && chars[i] != ']' {
15338                            idx_str.push(chars[i]);
15339                            i += 1;
15340                        }
15341                        if i < chars.len() {
15342                            i += 1;
15343                        }
15344                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
15345                            Expr {
15346                                kind: ExprKind::ScalarVar(rest.to_string()),
15347                                line,
15348                            }
15349                        } else if let Ok(n) = idx_str.parse::<i64>() {
15350                            Expr {
15351                                kind: ExprKind::Integer(n),
15352                                line,
15353                            }
15354                        } else {
15355                            Expr {
15356                                kind: ExprKind::String(idx_str),
15357                                line,
15358                            }
15359                        };
15360                        Expr {
15361                            kind: ExprKind::ArrayElement {
15362                                array: name,
15363                                index: Box::new(idx_expr),
15364                            },
15365                            line,
15366                        }
15367                    } else {
15368                        // Bare $name — defer to the chain-extension loop below.
15369                        Expr {
15370                            kind: ExprKind::ScalarVar(name),
15371                            line,
15372                        }
15373                    };
15374
15375                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
15376                    // implies `->` between consecutive subscripts (`$m[1][2]`
15377                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
15378                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
15379                    parts.push(StringPart::Expr(base));
15380                } else if chars[i].is_ascii_digit() {
15381                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
15382                    if chars[i] == '0' {
15383                        i += 1;
15384                        if i < chars.len() && chars[i].is_ascii_digit() {
15385                            return Err(self.syntax_err(
15386                                "Numeric variables with more than one digit may not start with '0'",
15387                                line,
15388                            ));
15389                        }
15390                        parts.push(StringPart::ScalarVar("0".into()));
15391                    } else {
15392                        let start = i;
15393                        while i < chars.len() && chars[i].is_ascii_digit() {
15394                            i += 1;
15395                        }
15396                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
15397                    }
15398                } else {
15399                    let c = chars[i];
15400                    let probe = c.to_string();
15401                    if VMHelper::is_special_scalar_name_for_get(&probe)
15402                        || matches!(c, '\'' | '`')
15403                    {
15404                        i += 1;
15405                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
15406                        if i < chars.len() && chars[i] == '{' {
15407                            i += 1; // skip {
15408                            let mut key = String::new();
15409                            let mut depth = 1;
15410                            while i < chars.len() && depth > 0 {
15411                                if chars[i] == '{' {
15412                                    depth += 1;
15413                                } else if chars[i] == '}' {
15414                                    depth -= 1;
15415                                    if depth == 0 {
15416                                        break;
15417                                    }
15418                                }
15419                                key.push(chars[i]);
15420                                i += 1;
15421                            }
15422                            if i < chars.len() {
15423                                i += 1;
15424                            } // skip }
15425                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
15426                                Expr {
15427                                    kind: ExprKind::ScalarVar(rest.to_string()),
15428                                    line,
15429                                }
15430                            } else {
15431                                Expr {
15432                                    kind: ExprKind::String(key),
15433                                    line,
15434                                }
15435                            };
15436                            let mut base = Expr {
15437                                kind: ExprKind::HashElement {
15438                                    hash: probe,
15439                                    key: Box::new(key_expr),
15440                                },
15441                                line,
15442                            };
15443                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
15444                            parts.push(StringPart::Expr(base));
15445                        } else {
15446                            // Check for arrow deref chain: `$@->{key}`, etc.
15447                            let mut base = Expr {
15448                                kind: ExprKind::ScalarVar(probe),
15449                                line,
15450                            };
15451                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
15452                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
15453                                // No chain extension — use the simpler ScalarVar part
15454                                if let ExprKind::ScalarVar(name) = base.kind {
15455                                    self.no_interop_check_scalar_var_name(&name, line)?;
15456                                    parts.push(StringPart::ScalarVar(name));
15457                                }
15458                            } else {
15459                                parts.push(StringPart::Expr(base));
15460                            }
15461                        }
15462                    } else {
15463                        literal.push('$');
15464                        literal.push(c);
15465                        i += 1;
15466                    }
15467                }
15468            } else if chars[i] == '@' && i + 1 < chars.len() {
15469                let next = chars[i + 1];
15470                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
15471                if next == '$' {
15472                    if !literal.is_empty() {
15473                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
15474                    }
15475                    i += 1; // past `@`
15476                    debug_assert_eq!(chars[i], '$');
15477                    i += 1; // past `$`
15478                    while i < chars.len() && chars[i].is_whitespace() {
15479                        i += 1;
15480                    }
15481                    if i >= chars.len() {
15482                        return Err(self.syntax_err(
15483                            "Expected variable or block after `@$` in double-quoted string",
15484                            line,
15485                        ));
15486                    }
15487                    let inner_expr = if chars[i] == '{' {
15488                        i += 1;
15489                        let start = i;
15490                        let mut depth = 1usize;
15491                        while i < chars.len() && depth > 0 {
15492                            match chars[i] {
15493                                '{' => depth += 1,
15494                                '}' => {
15495                                    depth -= 1;
15496                                    if depth == 0 {
15497                                        break;
15498                                    }
15499                                }
15500                                _ => {}
15501                            }
15502                            i += 1;
15503                        }
15504                        if depth != 0 {
15505                            return Err(self.syntax_err(
15506                                "Unterminated `${ ... }` after `@` in double-quoted string",
15507                                line,
15508                            ));
15509                        }
15510                        let inner: String = chars[start..i].iter().collect();
15511                        i += 1; // closing `}`
15512                        parse_expression_from_str(inner.trim(), "-e")?
15513                    } else {
15514                        let mut name = String::new();
15515                        if chars[i] == '^' {
15516                            name.push('^');
15517                            i += 1;
15518                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
15519                            {
15520                                name.push(chars[i]);
15521                                i += 1;
15522                            }
15523                        } else {
15524                            while i < chars.len()
15525                                && (chars[i].is_alphanumeric()
15526                                    || chars[i] == '_'
15527                                    || chars[i] == ':')
15528                            {
15529                                name.push(chars[i]);
15530                                i += 1;
15531                            }
15532                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
15533                                name.push_str("::");
15534                                i += 2;
15535                                while i < chars.len()
15536                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
15537                                {
15538                                    name.push(chars[i]);
15539                                    i += 1;
15540                                }
15541                            }
15542                        }
15543                        if name.is_empty() {
15544                            return Err(self.syntax_err(
15545                                "Expected identifier after `@$` in double-quoted string",
15546                                line,
15547                            ));
15548                        }
15549                        Expr {
15550                            kind: ExprKind::ScalarVar(name),
15551                            line,
15552                        }
15553                    };
15554                    parts.push(StringPart::Expr(Expr {
15555                        kind: ExprKind::Deref {
15556                            expr: Box::new(inner_expr),
15557                            kind: Sigil::Array,
15558                        },
15559                        line,
15560                    }));
15561                    continue 'istr;
15562                }
15563                if next == '{' {
15564                    if !literal.is_empty() {
15565                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
15566                    }
15567                    i += 2; // `@{`
15568                    let start = i;
15569                    let mut depth = 1usize;
15570                    while i < chars.len() && depth > 0 {
15571                        match chars[i] {
15572                            '{' => depth += 1,
15573                            '}' => {
15574                                depth -= 1;
15575                                if depth == 0 {
15576                                    break;
15577                                }
15578                            }
15579                            _ => {}
15580                        }
15581                        i += 1;
15582                    }
15583                    if depth != 0 {
15584                        return Err(
15585                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
15586                        );
15587                    }
15588                    let inner: String = chars[start..i].iter().collect();
15589                    i += 1; // closing `}`
15590                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
15591                    parts.push(StringPart::Expr(Expr {
15592                        kind: ExprKind::Deref {
15593                            expr: Box::new(inner_expr),
15594                            kind: Sigil::Array,
15595                        },
15596                        line,
15597                    }));
15598                    continue 'istr;
15599                }
15600                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
15601                    literal.push(chars[i]);
15602                    i += 1;
15603                } else {
15604                    if !literal.is_empty() {
15605                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
15606                    }
15607                    i += 1;
15608                    let mut name = String::new();
15609                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
15610                        name.push(chars[i]);
15611                        i += 1;
15612                    } else {
15613                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
15614                            name.push(chars[i]);
15615                            i += 1;
15616                        }
15617                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
15618                            name.push_str("::");
15619                            i += 2;
15620                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
15621                            {
15622                                name.push(chars[i]);
15623                                i += 1;
15624                            }
15625                        }
15626                    }
15627                    if i < chars.len() && chars[i] == '[' {
15628                        i += 1;
15629                        let start_inner = i;
15630                        let mut depth = 1usize;
15631                        while i < chars.len() && depth > 0 {
15632                            match chars[i] {
15633                                '[' => depth += 1,
15634                                ']' => depth -= 1,
15635                                _ => {}
15636                            }
15637                            if depth == 0 {
15638                                let inner: String = chars[start_inner..i].iter().collect();
15639                                i += 1; // closing ]
15640                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
15641                                parts.push(StringPart::Expr(Expr {
15642                                    kind: ExprKind::ArraySlice {
15643                                        array: name.clone(),
15644                                        indices,
15645                                    },
15646                                    line,
15647                                }));
15648                                continue 'istr;
15649                            }
15650                            i += 1;
15651                        }
15652                        return Err(self.syntax_err(
15653                            "Unterminated [ in array slice inside quoted string",
15654                            line,
15655                        ));
15656                    }
15657                    parts.push(StringPart::ArrayVar(name));
15658                }
15659            } else if chars[i] == '#'
15660                && i + 1 < chars.len()
15661                && chars[i + 1] == '{'
15662                && !crate::compat_mode()
15663            {
15664                // #{expr} — Ruby-style expression interpolation (stryke extension).
15665                if !literal.is_empty() {
15666                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
15667                }
15668                i += 2; // skip `#{`
15669                let mut inner = String::new();
15670                let mut depth = 1usize;
15671                while i < chars.len() && depth > 0 {
15672                    match chars[i] {
15673                        '{' => depth += 1,
15674                        '}' => {
15675                            depth -= 1;
15676                            if depth == 0 {
15677                                break;
15678                            }
15679                        }
15680                        _ => {}
15681                    }
15682                    inner.push(chars[i]);
15683                    i += 1;
15684                }
15685                if i < chars.len() {
15686                    i += 1; // skip closing `}`
15687                }
15688                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
15689                parts.push(StringPart::Expr(expr));
15690            } else {
15691                literal.push(chars[i]);
15692                i += 1;
15693            }
15694        }
15695        if !literal.is_empty() {
15696            parts.push(StringPart::Literal(literal));
15697        }
15698
15699        if parts.len() == 1 {
15700            if let StringPart::Literal(s) = &parts[0] {
15701                return Ok(Expr {
15702                    kind: ExprKind::String(s.clone()),
15703                    line,
15704                });
15705            }
15706        }
15707        if parts.is_empty() {
15708            return Ok(Expr {
15709                kind: ExprKind::String(String::new()),
15710                line,
15711            });
15712        }
15713
15714        Ok(Expr {
15715            kind: ExprKind::InterpolatedString(parts),
15716            line,
15717        })
15718    }
15719
15720    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
15721        match &e.kind {
15722            ExprKind::String(s) => Ok(s.clone()),
15723            _ => Err(self.syntax_err(
15724                "overload key must be a string literal (e.g. '\"\"' or '+')",
15725                e.line,
15726            )),
15727        }
15728    }
15729
15730    fn expr_to_overload_sub(&mut self, e: &Expr) -> PerlResult<String> {
15731        match &e.kind {
15732            ExprKind::String(s) => Ok(s.clone()),
15733            ExprKind::Integer(n) => Ok(n.to_string()),
15734            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
15735            // Anonymous sub: `use overload "+" => sub { ... };` — promote the
15736            // anon body into a synthetic top-level SubDecl so the overload
15737            // table can hold the name like the named-sub case. (PARITY-012)
15738            ExprKind::CodeRef { params, body } => {
15739                let id = self.next_overload_anon_id;
15740                self.next_overload_anon_id = self.next_overload_anon_id.saturating_add(1);
15741                let name = format!("__overload_anon_{}", id);
15742                self.pending_synthetic_subs.push(Statement {
15743                    label: None,
15744                    kind: StmtKind::SubDecl {
15745                        name: name.clone(),
15746                        params: params.clone(),
15747                        body: body.clone(),
15748                        prototype: None,
15749                    },
15750                    line: e.line,
15751                });
15752                Ok(name)
15753            }
15754            _ => Err(self.syntax_err(
15755                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
15756                e.line,
15757            )),
15758        }
15759    }
15760}
15761
15762fn merge_expr_list(parts: Vec<Expr>) -> Expr {
15763    if parts.len() == 1 {
15764        parts.into_iter().next().unwrap()
15765    } else {
15766        let line = parts.first().map(|e| e.line).unwrap_or(0);
15767        Expr {
15768            kind: ExprKind::List(parts),
15769            line,
15770        }
15771    }
15772}
15773
15774/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
15775pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
15776    let mut lexer = Lexer::new_with_file(s, file);
15777    let tokens = lexer.tokenize()?;
15778    let mut parser = Parser::new_with_file(tokens, file);
15779    let e = parser.parse_expression()?;
15780    if !parser.at_eof() {
15781        return Err(parser.syntax_err(
15782            "Extra tokens in embedded string expression",
15783            parser.peek_line(),
15784        ));
15785    }
15786    Ok(e)
15787}
15788
15789/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
15790pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
15791    let mut lexer = Lexer::new_with_file(s, file);
15792    let tokens = lexer.tokenize()?;
15793    let mut parser = Parser::new_with_file(tokens, file);
15794    let stmts = parser.parse_statements()?;
15795    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
15796    let inner = Expr {
15797        kind: ExprKind::CodeRef {
15798            params: vec![],
15799            body: stmts,
15800        },
15801        line: inner_line,
15802    };
15803    Ok(Expr {
15804        kind: ExprKind::Do(Box::new(inner)),
15805        line,
15806    })
15807}
15808
15809/// Comma-separated expressions on a `format` value line (below a picture line).
15810/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
15811pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
15812    let mut lexer = Lexer::new_with_file(s, file);
15813    let tokens = lexer.tokenize()?;
15814    let mut parser = Parser::new_with_file(tokens, file);
15815    parser.parse_arg_list()
15816}
15817
15818pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
15819    let trimmed = line.trim();
15820    if trimmed.is_empty() {
15821        return Ok(vec![]);
15822    }
15823    let mut lexer = Lexer::new(trimmed);
15824    let tokens = lexer.tokenize()?;
15825    let mut parser = Parser::new(tokens);
15826    let mut exprs = Vec::new();
15827    loop {
15828        if parser.at_eof() {
15829            break;
15830        }
15831        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
15832        exprs.push(parser.parse_assign_expr()?);
15833        if parser.eat(&Token::Comma) {
15834            continue;
15835        }
15836        if !parser.at_eof() {
15837            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
15838        }
15839        break;
15840    }
15841    Ok(exprs)
15842}
15843
15844#[cfg(test)]
15845mod tests {
15846    use super::*;
15847
15848    fn parse_ok(code: &str) -> Program {
15849        let mut lexer = Lexer::new(code);
15850        let tokens = lexer.tokenize().expect("tokenize");
15851        let mut parser = Parser::new(tokens);
15852        parser.parse_program().expect("parse")
15853    }
15854
15855    fn parse_err(code: &str) -> String {
15856        let mut lexer = Lexer::new(code);
15857        let tokens = match lexer.tokenize() {
15858            Ok(t) => t,
15859            Err(e) => return e.message,
15860        };
15861        let mut parser = Parser::new(tokens);
15862        parser.parse_program().unwrap_err().message
15863    }
15864
15865    #[test]
15866    fn parse_empty_program() {
15867        let p = parse_ok("");
15868        assert!(p.statements.is_empty());
15869    }
15870
15871    #[test]
15872    fn parse_semicolons_only() {
15873        let p = parse_ok(";;");
15874        assert!(p.statements.len() <= 3);
15875    }
15876
15877    #[test]
15878    fn parse_simple_scalar_assignment() {
15879        let p = parse_ok("$x = 1");
15880        assert_eq!(p.statements.len(), 1);
15881    }
15882
15883    #[test]
15884    fn parse_simple_array_assignment() {
15885        let p = parse_ok("@arr = (1, 2, 3)");
15886        assert_eq!(p.statements.len(), 1);
15887    }
15888
15889    #[test]
15890    fn parse_simple_hash_assignment() {
15891        let p = parse_ok("%h = (a => 1, b => 2)");
15892        assert_eq!(p.statements.len(), 1);
15893    }
15894
15895    #[test]
15896    fn parse_subroutine_decl() {
15897        let p = parse_ok("fn foo { 1 }");
15898        assert_eq!(p.statements.len(), 1);
15899        match &p.statements[0].kind {
15900            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
15901            _ => panic!("expected SubDecl"),
15902        }
15903    }
15904
15905    #[test]
15906    fn parse_subroutine_with_prototype() {
15907        let p = parse_ok("fn foo ($$) { 1 }");
15908        assert_eq!(p.statements.len(), 1);
15909        match &p.statements[0].kind {
15910            StmtKind::SubDecl { prototype, .. } => {
15911                assert!(prototype.is_some());
15912            }
15913            _ => panic!("expected SubDecl"),
15914        }
15915    }
15916
15917    #[test]
15918    fn parse_anonymous_fn() {
15919        let p = parse_ok("my $f = fn { 1 }");
15920        assert_eq!(p.statements.len(), 1);
15921    }
15922
15923    #[test]
15924    fn parse_if_statement() {
15925        let p = parse_ok("if (1) { 2 }");
15926        assert_eq!(p.statements.len(), 1);
15927        matches!(&p.statements[0].kind, StmtKind::If { .. });
15928    }
15929
15930    #[test]
15931    fn parse_if_elsif_else() {
15932        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
15933        assert_eq!(p.statements.len(), 1);
15934    }
15935
15936    #[test]
15937    fn parse_unless_statement() {
15938        let p = parse_ok("unless (0) { 1 }");
15939        assert_eq!(p.statements.len(), 1);
15940    }
15941
15942    #[test]
15943    fn parse_while_loop() {
15944        let p = parse_ok("while ($x) { $x-- }");
15945        assert_eq!(p.statements.len(), 1);
15946    }
15947
15948    #[test]
15949    fn parse_until_loop() {
15950        let p = parse_ok("until ($x) { $x++ }");
15951        assert_eq!(p.statements.len(), 1);
15952    }
15953
15954    #[test]
15955    fn parse_for_c_style() {
15956        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
15957        assert_eq!(p.statements.len(), 1);
15958    }
15959
15960    #[test]
15961    fn parse_foreach_loop() {
15962        let p = parse_ok("foreach my $x (@arr) { 1 }");
15963        assert_eq!(p.statements.len(), 1);
15964    }
15965
15966    #[test]
15967    fn parse_loop_with_label() {
15968        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
15969        assert_eq!(p.statements.len(), 1);
15970        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
15971    }
15972
15973    #[test]
15974    fn parse_begin_block() {
15975        let p = parse_ok("BEGIN { 1 }");
15976        assert_eq!(p.statements.len(), 1);
15977        matches!(&p.statements[0].kind, StmtKind::Begin(_));
15978    }
15979
15980    #[test]
15981    fn parse_end_block() {
15982        let p = parse_ok("END { 1 }");
15983        assert_eq!(p.statements.len(), 1);
15984        matches!(&p.statements[0].kind, StmtKind::End(_));
15985    }
15986
15987    #[test]
15988    fn parse_package_statement() {
15989        let p = parse_ok("package Foo::Bar");
15990        assert_eq!(p.statements.len(), 1);
15991        match &p.statements[0].kind {
15992            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
15993            _ => panic!("expected Package"),
15994        }
15995    }
15996
15997    #[test]
15998    fn parse_use_statement() {
15999        let p = parse_ok("use strict");
16000        assert_eq!(p.statements.len(), 1);
16001    }
16002
16003    #[test]
16004    fn parse_no_statement() {
16005        let p = parse_ok("no warnings");
16006        assert_eq!(p.statements.len(), 1);
16007    }
16008
16009    #[test]
16010    fn parse_require_bareword() {
16011        let p = parse_ok("require Foo::Bar");
16012        assert_eq!(p.statements.len(), 1);
16013    }
16014
16015    #[test]
16016    fn parse_require_string() {
16017        let p = parse_ok(r#"require "foo.pl""#);
16018        assert_eq!(p.statements.len(), 1);
16019    }
16020
16021    #[test]
16022    fn parse_eval_block() {
16023        let p = parse_ok("eval { 1 }");
16024        assert_eq!(p.statements.len(), 1);
16025    }
16026
16027    #[test]
16028    fn parse_eval_string() {
16029        let p = parse_ok(r#"eval "1 + 2""#);
16030        assert_eq!(p.statements.len(), 1);
16031    }
16032
16033    #[test]
16034    fn parse_qw_word_list() {
16035        let p = parse_ok("my @a = qw(foo bar baz)");
16036        assert_eq!(p.statements.len(), 1);
16037    }
16038
16039    #[test]
16040    fn parse_q_string() {
16041        let p = parse_ok("my $s = q{hello}");
16042        assert_eq!(p.statements.len(), 1);
16043    }
16044
16045    #[test]
16046    fn parse_qq_string() {
16047        let p = parse_ok(r#"my $s = qq(hello $x)"#);
16048        assert_eq!(p.statements.len(), 1);
16049    }
16050
16051    #[test]
16052    fn parse_regex_match() {
16053        let p = parse_ok(r#"$x =~ /foo/"#);
16054        assert_eq!(p.statements.len(), 1);
16055    }
16056
16057    #[test]
16058    fn parse_regex_substitution() {
16059        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
16060        assert_eq!(p.statements.len(), 1);
16061    }
16062
16063    #[test]
16064    fn parse_transliterate() {
16065        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
16066        assert_eq!(p.statements.len(), 1);
16067    }
16068
16069    #[test]
16070    fn parse_ternary_operator() {
16071        let p = parse_ok("my $x = $a ? 1 : 2");
16072        assert_eq!(p.statements.len(), 1);
16073    }
16074
16075    #[test]
16076    fn parse_arrow_method_call() {
16077        let p = parse_ok("$obj->method()");
16078        assert_eq!(p.statements.len(), 1);
16079    }
16080
16081    #[test]
16082    fn parse_arrow_deref_hash() {
16083        let p = parse_ok("$r->{key}");
16084        assert_eq!(p.statements.len(), 1);
16085    }
16086
16087    #[test]
16088    fn parse_arrow_deref_array() {
16089        let p = parse_ok("$r->[0]");
16090        assert_eq!(p.statements.len(), 1);
16091    }
16092
16093    #[test]
16094    fn parse_chained_arrow_deref() {
16095        let p = parse_ok("$r->{a}[0]{b}");
16096        assert_eq!(p.statements.len(), 1);
16097    }
16098
16099    #[test]
16100    fn parse_my_multiple_vars() {
16101        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
16102        assert_eq!(p.statements.len(), 1);
16103    }
16104
16105    #[test]
16106    fn parse_our_scalar() {
16107        let p = parse_ok("our $VERSION = '1.0'");
16108        assert_eq!(p.statements.len(), 1);
16109    }
16110
16111    #[test]
16112    fn parse_local_scalar() {
16113        let p = parse_ok("local $/ = undef");
16114        assert_eq!(p.statements.len(), 1);
16115    }
16116
16117    #[test]
16118    fn parse_state_variable() {
16119        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
16120        assert_eq!(p.statements.len(), 1);
16121    }
16122
16123    #[test]
16124    fn parse_postfix_if() {
16125        let p = parse_ok("print 1 if $x");
16126        assert_eq!(p.statements.len(), 1);
16127    }
16128
16129    #[test]
16130    fn parse_postfix_unless() {
16131        let p = parse_ok("die 'error' unless $ok");
16132        assert_eq!(p.statements.len(), 1);
16133    }
16134
16135    #[test]
16136    fn parse_postfix_while() {
16137        let p = parse_ok("$x++ while $x < 10");
16138        assert_eq!(p.statements.len(), 1);
16139    }
16140
16141    #[test]
16142    fn parse_postfix_for() {
16143        let p = parse_ok("print for @arr");
16144        assert_eq!(p.statements.len(), 1);
16145    }
16146
16147    #[test]
16148    fn parse_last_next_redo() {
16149        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
16150        assert_eq!(p.statements.len(), 1);
16151    }
16152
16153    #[test]
16154    fn parse_return_statement() {
16155        let p = parse_ok("fn foo { return 42 }");
16156        assert_eq!(p.statements.len(), 1);
16157    }
16158
16159    #[test]
16160    fn parse_wantarray() {
16161        let p = parse_ok("fn foo { wantarray ? @a : $a }");
16162        assert_eq!(p.statements.len(), 1);
16163    }
16164
16165    #[test]
16166    fn parse_caller_builtin() {
16167        let p = parse_ok("my @c = caller");
16168        assert_eq!(p.statements.len(), 1);
16169    }
16170
16171    #[test]
16172    fn parse_ref_to_array() {
16173        let p = parse_ok("my $r = \\@arr");
16174        assert_eq!(p.statements.len(), 1);
16175    }
16176
16177    #[test]
16178    fn parse_ref_to_hash() {
16179        let p = parse_ok("my $r = \\%hash");
16180        assert_eq!(p.statements.len(), 1);
16181    }
16182
16183    #[test]
16184    fn parse_ref_to_scalar() {
16185        let p = parse_ok("my $r = \\$x");
16186        assert_eq!(p.statements.len(), 1);
16187    }
16188
16189    #[test]
16190    fn parse_deref_scalar() {
16191        let p = parse_ok("my $v = $$r");
16192        assert_eq!(p.statements.len(), 1);
16193    }
16194
16195    #[test]
16196    fn parse_deref_array() {
16197        let p = parse_ok("my @a = @$r");
16198        assert_eq!(p.statements.len(), 1);
16199    }
16200
16201    #[test]
16202    fn parse_deref_hash() {
16203        let p = parse_ok("my %h = %$r");
16204        assert_eq!(p.statements.len(), 1);
16205    }
16206
16207    #[test]
16208    fn parse_blessed_ref() {
16209        let p = parse_ok("bless $r, 'Foo'");
16210        assert_eq!(p.statements.len(), 1);
16211    }
16212
16213    #[test]
16214    fn parse_heredoc_basic() {
16215        let p = parse_ok("my $s = <<END;\nfoo\nEND");
16216        assert_eq!(p.statements.len(), 1);
16217    }
16218
16219    #[test]
16220    fn parse_heredoc_quoted() {
16221        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
16222        assert_eq!(p.statements.len(), 1);
16223    }
16224
16225    #[test]
16226    fn parse_do_block() {
16227        let p = parse_ok("my $x = do { 1 + 2 }");
16228        assert_eq!(p.statements.len(), 1);
16229    }
16230
16231    #[test]
16232    fn parse_do_file() {
16233        let p = parse_ok(r#"do "foo.pl""#);
16234        assert_eq!(p.statements.len(), 1);
16235    }
16236
16237    #[test]
16238    fn parse_map_expression() {
16239        let p = parse_ok("my @b = map { $_ * 2 } @a");
16240        assert_eq!(p.statements.len(), 1);
16241    }
16242
16243    #[test]
16244    fn parse_grep_expression() {
16245        let p = parse_ok("my @b = grep { $_ > 0 } @a");
16246        assert_eq!(p.statements.len(), 1);
16247    }
16248
16249    #[test]
16250    fn parse_sort_expression() {
16251        let p = parse_ok("my @b = sort { $a <=> $b } @a");
16252        assert_eq!(p.statements.len(), 1);
16253    }
16254
16255    #[test]
16256    fn parse_pipe_forward() {
16257        let p = parse_ok("@a |> map { $_ * 2 }");
16258        assert_eq!(p.statements.len(), 1);
16259    }
16260
16261    #[test]
16262    fn parse_expression_from_str_simple() {
16263        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
16264        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
16265    }
16266
16267    #[test]
16268    fn parse_expression_from_str_extra_tokens_error() {
16269        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
16270        assert!(err.message.contains("Extra tokens"));
16271    }
16272
16273    #[test]
16274    fn parse_slice_indices_from_str_basic() {
16275        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
16276        assert_eq!(indices.len(), 3);
16277    }
16278
16279    #[test]
16280    fn parse_format_value_line_empty() {
16281        let exprs = parse_format_value_line("").unwrap();
16282        assert!(exprs.is_empty());
16283    }
16284
16285    #[test]
16286    fn parse_format_value_line_single() {
16287        let exprs = parse_format_value_line("$x").unwrap();
16288        assert_eq!(exprs.len(), 1);
16289    }
16290
16291    #[test]
16292    fn parse_format_value_line_multiple() {
16293        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
16294        assert_eq!(exprs.len(), 3);
16295    }
16296
16297    #[test]
16298    fn parse_unclosed_brace_error() {
16299        let err = parse_err("fn foo {");
16300        assert!(!err.is_empty());
16301    }
16302
16303    #[test]
16304    fn parse_unclosed_paren_error() {
16305        let err = parse_err("print (1, 2");
16306        assert!(!err.is_empty());
16307    }
16308
16309    #[test]
16310    fn parse_invalid_statement_error() {
16311        let err = parse_err("???");
16312        assert!(!err.is_empty());
16313    }
16314
16315    #[test]
16316    fn merge_expr_list_single() {
16317        let e = Expr {
16318            kind: ExprKind::Integer(1),
16319            line: 1,
16320        };
16321        let merged = merge_expr_list(vec![e.clone()]);
16322        matches!(merged.kind, ExprKind::Integer(1));
16323    }
16324
16325    #[test]
16326    fn merge_expr_list_multiple() {
16327        let e1 = Expr {
16328            kind: ExprKind::Integer(1),
16329            line: 1,
16330        };
16331        let e2 = Expr {
16332            kind: ExprKind::Integer(2),
16333            line: 1,
16334        };
16335        let merged = merge_expr_list(vec![e1, e2]);
16336        matches!(merged.kind, ExprKind::List(_));
16337    }
16338}